Files
Masterarbeit/QuellCode/CentronERP/tests/Centron.Tests.EndToEnd/Tests/TimerBilling/TimerBillingPartListTransferTest.cs
T
Christoph Schwörer f045b99a25 Codebasis als Dateien ins Arbeitsrepo statt als Gitlink
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
2026-08-26 07:43:51 +02:00

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;
}
}
}