QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142) getrackt, ohne .gitmodules und ohne erreichbares Remote. Der Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen. Umstellung: - Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6) - Gitlink aus dem Index entfernt - Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
201 lines
11 KiB
C#
201 lines
11 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<TimerBillingSettingsDTO> configure, params int[] timerI3Ds)
|
|
{
|
|
using (var session = new BLSession())
|
|
{
|
|
var user = session.GetBL<AppUserBL>().GetAppUser(f => f.I3D == 11);
|
|
var timers = timerI3Ds.Length > 0 ? timerI3Ds : new[] { HelpdeskTimerI3D };
|
|
var invoiceResult = session.GetBL<ReceiptWebServiceBL>()
|
|
.CreateNewReceiptForHelpdekTimers(timers, CreateSettings(configure), user);
|
|
|
|
var verifierSettings = new CentronVerifierSettings();
|
|
verifierSettings.IgnoreProperty<ReceiptInvoiceItemDTO>(f => f.BalanceID);
|
|
|
|
this.Verifier.Verify(name, invoiceResult, verifierSettings);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
private void SaveInvoiceAndVerifyPositions(string name, Action<TimerBillingSettingsDTO> configure, params int[] timerI3Ds)
|
|
{
|
|
int positionCutoffI3D = this.Sql<int>("SELECT ISNULL(MAX(I3D), 0) FROM dbo.RechPos");
|
|
|
|
using (var session = new BLSession())
|
|
{
|
|
var user = session.GetBL<AppUserBL>().GetAppUser(f => f.I3D == 11);
|
|
var timers = timerI3Ds.Length > 0 ? timerI3Ds : new[] { HelpdeskTimerI3D };
|
|
var invoiceResult = session.GetBL<ReceiptWebServiceBL>()
|
|
.CreateNewReceiptForHelpdekTimers(timers, CreateSettings(configure), user);
|
|
|
|
if (invoiceResult.Status == ResultStatus.Error)
|
|
throw new InvalidOperationException($"{name}: creating the invoice failed - {invoiceResult.Message}");
|
|
|
|
var saveResult = session.GetBL<ReceiptWebServiceBL>().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<TimerBillingSettingsDTO> 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;
|
|
}
|
|
}
|
|
}
|