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
312 lines
12 KiB
C#
312 lines
12 KiB
C#
using System.Linq;
|
|
using Centron.BusinessLogic.DataExchange.EDI;
|
|
using Centron.BusinessLogic.DataExchange.EDI.SaleInvoices;
|
|
using Centron.Data.Entities.Sales.Receipts.Invoices;
|
|
using Centron.Interfaces.DataExchange.BookKeeping;
|
|
using JetBrains.Annotations;
|
|
|
|
namespace Centron.Tests.BL.DataExchange.EDI.SaleInvoices;
|
|
|
|
/// <summary>
|
|
/// Tests for the contract-contingent balance merge in <see cref="InvoiceZugferdBL.MergeBalanceCompensationItems"/>.
|
|
/// Service and compensation items share a <see cref="ZugferdExportPositionItem.BalanceID"/>; the merge folds the
|
|
/// compensation amount into the service item and removes the compensation line from the export.
|
|
/// On invoices the service item is positive and the compensation is negative; credit vouchers store the same
|
|
/// pair with inverted signs (Ticket 164110 — without the sign-aware merge the wrong line would be removed
|
|
/// and the position total would no longer match the receipt header).
|
|
/// </summary>
|
|
[TestSubject(typeof(InvoiceZugferdBL))]
|
|
public class InvoiceZugferdBalanceMergeTests
|
|
{
|
|
private const string BalanceA = "BAL-A";
|
|
private const string BalanceB = "BAL-B";
|
|
|
|
private static ZugferdExportPositionItem Service(string balanceId, decimal netTotal, decimal netUnit, decimal quantity)
|
|
=> new()
|
|
{
|
|
BalanceID = balanceId,
|
|
NetPriceTotalFC = netTotal,
|
|
NetPrice = netUnit,
|
|
Quantity = quantity,
|
|
TaxPriceTotalFC = 0m,
|
|
ArticleCode = "SERPELISTD",
|
|
Text = "Servicedienstleistung",
|
|
};
|
|
|
|
private static ZugferdExportPositionItem Compensation(string balanceId, decimal netTotal)
|
|
=> new()
|
|
{
|
|
BalanceID = balanceId,
|
|
NetPriceTotalFC = netTotal,
|
|
NetPrice = -netTotal,
|
|
Quantity = 0m,
|
|
TaxPriceTotalFC = 0m,
|
|
ArticleCode = "AUSGLEICH",
|
|
Text = "Ausgleichsartikel für Kontingente",
|
|
};
|
|
|
|
private static ZugferdExportItem ItemWith(params ZugferdExportPositionItem[] positions)
|
|
=> new() { Positions = positions.ToList() };
|
|
|
|
[Fact]
|
|
public void Invoice_FullyCoveredByContingent_ReducesServiceToZeroAndKeepsBasisQuantity()
|
|
{
|
|
var service = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
|
|
var compensation = Compensation(BalanceA, netTotal: -150m);
|
|
var export = ItemWith(service, compensation);
|
|
|
|
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
|
|
|
|
Assert.Single(export.Positions);
|
|
Assert.Same(service, export.Positions[0]);
|
|
Assert.Equal(0m, service.NetPriceTotalFC);
|
|
Assert.Equal(0m, service.Quantity);
|
|
Assert.Equal(1m, service.BasisQuantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void Invoice_PartiallyCovered_ReducesServiceProportionally()
|
|
{
|
|
var service = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
|
|
var compensation = Compensation(BalanceA, netTotal: -90m);
|
|
var export = ItemWith(service, compensation);
|
|
|
|
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
|
|
|
|
Assert.Single(export.Positions);
|
|
Assert.Equal(60m, service.NetPriceTotalFC);
|
|
Assert.Equal(0.4m, service.Quantity);
|
|
Assert.Equal(1m, service.BasisQuantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreditVoucher_FullyCoveredByContingent_ReducesServiceToZeroAndKeepsRealServiceLine()
|
|
{
|
|
// Credit voucher: signs are inverted compared to the source invoice
|
|
var service = Service(BalanceA, netTotal: -150m, netUnit: -150m, quantity: 1m);
|
|
var compensation = Compensation(BalanceA, netTotal: 150m);
|
|
var export = ItemWith(service, compensation);
|
|
|
|
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.CreditVoucher);
|
|
|
|
Assert.Single(export.Positions);
|
|
Assert.Same(service, export.Positions[0]);
|
|
Assert.Equal("SERPELISTD", export.Positions[0].ArticleCode);
|
|
Assert.Equal(0m, service.NetPriceTotalFC);
|
|
Assert.Equal(0m, service.Quantity);
|
|
Assert.Equal(1m, service.BasisQuantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreditVoucher_PartiallyCovered_ReducesServiceProportionally()
|
|
{
|
|
var service = Service(BalanceA, netTotal: -150m, netUnit: -150m, quantity: 1m);
|
|
var compensation = Compensation(BalanceA, netTotal: 90m);
|
|
var export = ItemWith(service, compensation);
|
|
|
|
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.CreditVoucher);
|
|
|
|
Assert.Single(export.Positions);
|
|
Assert.Equal(-60m, service.NetPriceTotalFC);
|
|
Assert.Equal(0.4m, service.Quantity);
|
|
Assert.Equal(1m, service.BasisQuantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void MultipleBalanceGroups_AreMergedIndependently()
|
|
{
|
|
var service1 = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
|
|
var compensation1 = Compensation(BalanceA, netTotal: -150m);
|
|
var service2 = Service(BalanceB, netTotal: 200m, netUnit: 100m, quantity: 2m);
|
|
var compensation2 = Compensation(BalanceB, netTotal: -100m);
|
|
var export = ItemWith(service1, compensation1, service2, compensation2);
|
|
|
|
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
|
|
|
|
Assert.Equal(2, export.Positions.Count);
|
|
Assert.Equal(0m, service1.NetPriceTotalFC);
|
|
Assert.Equal(0m, service1.Quantity);
|
|
Assert.Equal(100m, service2.NetPriceTotalFC);
|
|
Assert.Equal(1m, service2.Quantity);
|
|
Assert.DoesNotContain(compensation1, export.Positions);
|
|
Assert.DoesNotContain(compensation2, export.Positions);
|
|
}
|
|
|
|
[Fact]
|
|
public void PositionsWithoutBalanceId_AreLeftUntouched()
|
|
{
|
|
var standalone = new ZugferdExportPositionItem
|
|
{
|
|
BalanceID = null,
|
|
NetPriceTotalFC = 99m,
|
|
NetPrice = 99m,
|
|
Quantity = 1m,
|
|
ArticleCode = "OTHER",
|
|
};
|
|
var export = ItemWith(standalone);
|
|
|
|
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
|
|
|
|
Assert.Single(export.Positions);
|
|
Assert.Same(standalone, export.Positions[0]);
|
|
Assert.Equal(99m, standalone.NetPriceTotalFC);
|
|
Assert.Equal(1m, standalone.Quantity);
|
|
Assert.Null(standalone.BasisQuantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveServicePeriod_ItemPeriodWinsBeforeReceiptHeadAndFallback()
|
|
{
|
|
var receipt = new ReceiptInvoice
|
|
{
|
|
ServicePeriodFrom = new System.DateTime(2024, 1, 1),
|
|
ServicePeriodTo = new System.DateTime(2024, 1, 31),
|
|
};
|
|
var item = new ReceiptInvoiceItem
|
|
{
|
|
I3D = 10,
|
|
ServicePeriodFrom = new System.DateTime(2024, 2, 2, 14, 0, 0),
|
|
ServicePeriodTo = new System.DateTime(2024, 2, 4, 16, 0, 0),
|
|
};
|
|
var fallbackCalled = false;
|
|
|
|
var result = InvoiceZugferdBL.ResolveServicePeriod(
|
|
receipt,
|
|
item,
|
|
BookKeepingReceiptKind.Invoice,
|
|
(_, _) =>
|
|
{
|
|
fallbackCalled = true;
|
|
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
|
|
});
|
|
|
|
Assert.Equal(new System.DateTime(2024, 2, 2), result.From);
|
|
Assert.Equal(new System.DateTime(2024, 2, 4), result.To);
|
|
Assert.False(fallbackCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveServicePeriod_ReceiptHeadWinsBeforeFallback()
|
|
{
|
|
var receipt = new ReceiptInvoice
|
|
{
|
|
ServicePeriodFrom = new System.DateTime(2024, 1, 1, 9, 0, 0),
|
|
ServicePeriodTo = new System.DateTime(2024, 1, 31, 18, 0, 0),
|
|
};
|
|
var item = new ReceiptInvoiceItem { I3D = 11 };
|
|
var fallbackCalled = false;
|
|
|
|
var result = InvoiceZugferdBL.ResolveServicePeriod(
|
|
receipt,
|
|
item,
|
|
BookKeepingReceiptKind.Invoice,
|
|
(_, _) =>
|
|
{
|
|
fallbackCalled = true;
|
|
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
|
|
});
|
|
|
|
Assert.Equal(new System.DateTime(2024, 1, 1), result.From);
|
|
Assert.Equal(new System.DateTime(2024, 1, 31), result.To);
|
|
Assert.False(fallbackCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveServicePeriod_UsesContractFallbackWhenItemAndHeadAreEmpty()
|
|
{
|
|
var receipt = new ReceiptInvoice();
|
|
var item = new ReceiptInvoiceItem { I3D = 12 };
|
|
|
|
var result = InvoiceZugferdBL.ResolveServicePeriod(
|
|
receipt,
|
|
item,
|
|
BookKeepingReceiptKind.Invoice,
|
|
(itemI3D, receiptKind) =>
|
|
{
|
|
Assert.Equal(12, itemI3D);
|
|
Assert.Equal(BookKeepingReceiptKind.Invoice, receiptKind);
|
|
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
|
|
});
|
|
|
|
Assert.Equal(new System.DateTime(2024, 3, 1), result.From);
|
|
Assert.Equal(new System.DateTime(2024, 3, 31), result.To);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyHeaderBillingPeriodFromPositions_UsesMinMaxAndKeepsSameDayPeriod()
|
|
{
|
|
var export = ItemWith(
|
|
new ZugferdExportPositionItem
|
|
{
|
|
BillingPeriodFrom = new System.DateTime(2024, 2, 15),
|
|
BillingPeriodTo = new System.DateTime(2024, 2, 15),
|
|
},
|
|
new ZugferdExportPositionItem
|
|
{
|
|
BillingPeriodFrom = new System.DateTime(2024, 1, 1),
|
|
BillingPeriodTo = new System.DateTime(2024, 3, 31),
|
|
});
|
|
|
|
InvoiceZugferdBL.ApplyHeaderBillingPeriodFromPositions(export);
|
|
|
|
Assert.Equal(new System.DateTime(2024, 1, 1), export.BillingPeriodFrom);
|
|
Assert.Equal(new System.DateTime(2024, 3, 31), export.BillingPeriodTo);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsSingleServicePeriodDate_ReturnsTrue_WhenDatesAreEqualIgnoringTime()
|
|
{
|
|
var isSingleDate = InvoiceZugferdBL.IsSingleServicePeriodDate(
|
|
new System.DateTime(2026, 2, 6, 10, 0, 0),
|
|
new System.DateTime(2026, 2, 6, 13, 30, 0));
|
|
|
|
Assert.True(isSingleDate);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetHeaderDeliveryDate_UsesServicePeriodTo_WhenHeaderPeriodIsSingleDate()
|
|
{
|
|
var exportItem = ItemWith();
|
|
exportItem.ReceiptDate = new System.DateTime(2026, 2, 1);
|
|
exportItem.DeliveryDate = new System.DateTime(2026, 2, 4);
|
|
exportItem.BillingPeriodFrom = new System.DateTime(2026, 2, 6, 10, 0, 0);
|
|
exportItem.BillingPeriodTo = new System.DateTime(2026, 2, 6, 13, 30, 0);
|
|
|
|
var deliveryDate = InvoiceZugferdBL.GetHeaderDeliveryDate(exportItem);
|
|
|
|
Assert.Equal(new System.DateTime(2026, 2, 6), deliveryDate);
|
|
}
|
|
|
|
[Fact]
|
|
public void DoCreateActualDeliverySupplyChainEvent_UsesOnlyDeliveryDateWhenPeriodDatesAreEqual()
|
|
{
|
|
var document = new System.Xml.XmlDocument();
|
|
|
|
var node = InvoiceZugferdBL.DoCreateActualDeliverySupplyChainEvent(
|
|
new System.DateTime(2026, 2, 6, 13, 30, 0),
|
|
document);
|
|
|
|
var occurrenceDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "OccurrenceDateTime");
|
|
var dateTimeStringNode = Assert.Single(occurrenceDateTimeNode.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "DateTimeString");
|
|
Assert.Equal("102", dateTimeStringNode.Attributes?["format"]?.Value);
|
|
Assert.Equal("20260206", dateTimeStringNode.InnerText);
|
|
}
|
|
|
|
[Fact]
|
|
public void DoCreateBillingSpecifiedPeriod_UsesStartAndEndDateWhenDatesDiffer()
|
|
{
|
|
var document = new System.Xml.XmlDocument();
|
|
|
|
var node = InvoiceZugferdBL.DoCreateBillingSpecifiedPeriod(
|
|
new System.DateTime(2026, 2, 6, 10, 0, 0),
|
|
new System.DateTime(2026, 2, 7, 13, 30, 0),
|
|
document);
|
|
|
|
var startDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "StartDateTime");
|
|
var endDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "EndDateTime");
|
|
|
|
Assert.Equal("20260206", startDateTimeNode.InnerText);
|
|
Assert.Equal("20260207", endDateTimeNode.InnerText);
|
|
}
|
|
}
|