using System;
using Centron.BusinessLogic;
using Centron.BusinessLogic.Administration.Settings;
using Centron.BusinessLogic.EmployeeArea;
using Centron.BusinessLogic.Sales.Receipts.DataAndResults;
using Centron.BusinessLogic.WebServices.Sales.Receipts;
using Centron.Data.WebServices.Sales.Receipts.Invoices;
using Centron.Interfaces;
using Centron.Interfaces.Administration.Environment;
using Centron.Interfaces.BL;
using Centron.Tests.EndToEnd.Infrastructure;
using Centron.Tests.EndToEnd.Infrastructure.Verifier;
using CentronSoftware.Centron.WebServices.Entities.Sales.CustomerAssets.TimerBilling;
using Xunit.Abstractions;
namespace Centron.Tests.EndToEnd.Tests.TimerBilling
{
///
/// Covers the "transfer ticket times into a billing partlist" feature and every related setting:
/// the two transfer modes (TransferAllTimesToPartsList / TransferTimesWithSamePriceToPartsList),
/// ReplacePartListArticles, ReplacePartListText, SortPartListByTicket, and the special-article
/// handling introduced by Ticket 161840 (TransferSpecialArticlesToPartsList). It also includes the
/// control case where partlist transfer is OFF (nothing is transferred).
///
/// Uses the Ticket87016 seed data: helpdesk timer 10509 (service article 94, 30 EUR) carrying two
/// travel/Anfahrt special articles - 56 "Fahrtkosten" (fixed 35) and 1184 "Fahrtkosten" (x44).
/// The billing partlist header is article 16 (a parts-list article).
///
/// SortPartListByTicket is exercised with timers from two different tickets (10509 on ticket 12475
/// and 227 on ticket 12184 - same customer/address) so the per-ticket grouping is actually visible
/// when compared against the combined variant.
///
public class TimerBillingPartListTransferTest : EndToEndTest
{
private static readonly int HelpdeskTimerI3D = 10509; // ticket 12475, with Remote/Anfahrt special articles
private static readonly int SecondHelpdeskTimerI3D = 227; // ticket 12184 (same customer), service article 94, no contract
private static readonly int PartListHeaderArticleI3D = 16;
public TimerBillingPartListTransferTest(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}
public override void Execute()
{
this.PrepareDatabase();
// Control: partlist transfer not enabled -> times and special articles are billed as normal positions.
this.CreateInvoice("NotTransferred", _ => { });
// Mode 1: all times into a single billing partlist; special articles out (default) vs in (Ticket 161840).
this.CreateInvoice("AllTimes_SpecialArticlesExcluded", s => s.TransferAllTimesToPartsList = true);
this.CreateInvoice("AllTimes_SpecialArticlesIncluded", s =>
{
s.TransferAllTimesToPartsList = true;
s.TransferSpecialArticlesToPartsList = true;
});
// Mode 2: times grouped by equal price into billing partlists; special articles out vs in.
this.CreateInvoice("SamePrice_SpecialArticlesExcluded", s => s.TransferTimesWithSamePriceToPartsList = true);
this.CreateInvoice("SamePrice_SpecialArticlesIncluded", s =>
{
s.TransferTimesWithSamePriceToPartsList = true;
s.TransferSpecialArticlesToPartsList = true;
});
// ReplacePartListArticles = false -> the header article keeps its own components instead of
// being replaced by the transferred timer items.
this.CreateInvoice("AllTimes_KeepExistingPartListArticles", s =>
{
s.TransferAllTimesToPartsList = true;
s.ReplacePartListArticles = false;
});
// ReplacePartListText = true -> the partlist head text is replaced by the joined item texts.
this.CreateInvoice("AllTimes_ReplacePartListText", s =>
{
s.TransferAllTimesToPartsList = true;
s.ReplacePartListText = true;
});
// SortPartListByTicket: two timers from two different tickets, combined into one partlist...
this.CreateInvoice("MultiTicket_AllTimes_Combined", s => s.TransferAllTimesToPartsList = true,
HelpdeskTimerI3D, SecondHelpdeskTimerI3D);
// ...vs. a separate partlist per ticket.
this.CreateInvoice("MultiTicket_AllTimes_SortedByTicket", s =>
{
s.TransferAllTimesToPartsList = true;
s.SortPartListByTicket = true;
}, HelpdeskTimerI3D, SecondHelpdeskTimerI3D);
// Saved last, because saving allocates a receipt number and writes rows the other scenarios must
// not see. This is the path the wizard takes with "Vorschau anzeigen" switched off.
this.SaveInvoiceAndVerifyPositions("AllTimes_SpecialArticlesIncluded_Saved", s =>
{
s.TransferAllTimesToPartsList = true;
s.TransferSpecialArticlesToPartsList = true;
});
}
private void PrepareDatabase()
{
this.Sql($"UPDATE dbo.Stammdat SET WertText = 'g' WHERE I3D = {(int)AppSettingsConst.HelpdeskTimerStartTimeFormat}");
this.Sql($"UPDATE dbo.Stammdat SET WertText = 'g' WHERE I3D = {(int)AppSettingsConst.HelpdeskTimerEndTimeFormat}");
}
private void CreateInvoice(string name, Action configure, params int[] timerI3Ds)
{
using (var session = new BLSession())
{
var user = session.GetBL().GetAppUser(f => f.I3D == 11);
var timers = timerI3Ds.Length > 0 ? timerI3Ds : new[] { HelpdeskTimerI3D };
var invoiceResult = session.GetBL()
.CreateNewReceiptForHelpdekTimers(timers, CreateSettings(configure), user);
var verifierSettings = new CentronVerifierSettings();
verifierSettings.IgnoreProperty(f => f.BalanceID);
this.Verifier.Verify(name, invoiceResult, verifierSettings);
}
}
///
/// Creates and saves the invoice the way the timer-billing wizard does when "Vorschau anzeigen" is
/// switched off: the backend result is saved directly, without passing through the WPF position grid.
/// Verifies the persisted positions, because reports and the ZUGFeRD export read the partlist head
/// quantity and price from the database - QS found Stk = 1 on the head while the user interface
/// displayed the summed quantity (Ticket 163721).
///
private void SaveInvoiceAndVerifyPositions(string name, Action configure, params int[] timerI3Ds)
{
int positionCutoffI3D = this.Sql("SELECT ISNULL(MAX(I3D), 0) FROM dbo.RechPos");
using (var session = new BLSession())
{
var user = session.GetBL().GetAppUser(f => f.I3D == 11);
var timers = timerI3Ds.Length > 0 ? timerI3Ds : new[] { HelpdeskTimerI3D };
var invoiceResult = session.GetBL()
.CreateNewReceiptForHelpdekTimers(timers, CreateSettings(configure), user);
if (invoiceResult.Status == ResultStatus.Error)
throw new InvalidOperationException($"{name}: creating the invoice failed - {invoiceResult.Message}");
var saveResult = session.GetBL().SaveReceipt(
invoiceResult.Data, user, new SaveReceiptData { IgnoreCallbacks = true }, false,
CreatedThroughApplication.CentronNet, null);
if (saveResult.Status == ResultStatus.Error)
throw new InvalidOperationException($"{name}: saving the invoice failed - {saveResult.Message}");
}
// The billing partlist head replaces its children on the document - the children are hidden
// (Visible 0) while the collapsed head (Expanded 0) carries the quantity and the unit price.
// CalculatedNetPriceTotalFCComplete is the amount reports and the ZUGFeRD export read, so the head
// amount and the sum of its children are both visible here.
this.Verifier.VerifySql($"{name}_DbPositions", $@"
SELECT Pos, ArtikelI3D, Indent, Expanded, Visible,
ISNULL(CAST(IsBillingPartList AS int), 0) AS IsBillingPartList,
Stk, VKBasisWert, EK, ISNULL(Rabatt, 0) AS Rabatt,
CalculatedNetPriceTotalFCComplete
FROM dbo.RechPos
WHERE I3D > {positionCutoffI3D}
ORDER BY Pos");
// The collapsed head stands in for its hidden children on the document, so both amounts have to
// agree. They can differ by cents when the head unit price (total / quantity) does not divide
// evenly at the head article's number of decimals - the difference is asserted, not assumed.
this.Verifier.VerifySql($"{name}_HeadAmountVersusChildren", $@"
SELECT Head.HeadAmount, Children.ChildAmount, Head.HeadAmount - Children.ChildAmount AS Difference
FROM (SELECT ISNULL(SUM(CalculatedNetPriceTotalFCComplete), 0) AS HeadAmount
FROM dbo.RechPos
WHERE I3D > {positionCutoffI3D} AND ISNULL(CAST(IsBillingPartList AS int), 0) = 1) Head
CROSS JOIN (SELECT ISNULL(SUM(CalculatedNetPriceTotalFCComplete), 0) AS ChildAmount
FROM dbo.RechPos
WHERE I3D > {positionCutoffI3D} AND Indent > 0 AND Expanded IS NULL) Children");
}
private static TimerBillingSettingsDTO CreateSettings(Action configure)
{
var settings = new TimerBillingSettingsDTO
{
InsertHelpdeskDescription = true,
InsertHelpdeskShortDescription = true,
InsertTimerText = true,
InsertTimerTime = true,
InsertTheFollowingAreTimersFromHelpdeskText = true,
Sorting = Interfaces.Sales.Receipts.TimerBilling.TimerSorting.ChronologicalUp,
ReceiptKind = CentronObjectKindNumeric.InvoiceClass,
// Partlist defaults; individual scenarios override what they test.
DefaultPartListHeaderI3D = PartListHeaderArticleI3D,
ReplacePartListArticles = true,
};
configure(settings);
return settings;
}
}
}