diff --git a/src/Apps/DE/EDocumentDE/app/src/EDocItemChargesDE.PageExt.al b/src/Apps/DE/EDocumentDE/app/src/EDocItemChargesDE.PageExt.al
new file mode 100644
index 00000000000..f6bfa858f59
--- /dev/null
+++ b/src/Apps/DE/EDocumentDE/app/src/EDocItemChargesDE.PageExt.al
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Formats;
+
+using Microsoft.Inventory.Item;
+
+pageextension 11036 "E-Doc Item Charges DE" extends "Item Charges"
+{
+ layout
+ {
+ modify("E-Invoice Mapping")
+ {
+ Visible = true;
+ }
+ modify("E-Invoice Reason Text")
+ {
+ Visible = true;
+ }
+ modify("E-Invoice Reason Code")
+ {
+ Visible = true;
+ }
+ modify("E-Invoice Unit Code")
+ {
+ Visible = true;
+ }
+ }
+}
diff --git a/src/Apps/DE/EDocumentDE/app/src/EDocumentDEHelper.Codeunit.al b/src/Apps/DE/EDocumentDE/app/src/EDocumentDEHelper.Codeunit.al
index c21fd933ccd..ab7e7eee4e9 100644
--- a/src/Apps/DE/EDocumentDE/app/src/EDocumentDEHelper.Codeunit.al
+++ b/src/Apps/DE/EDocumentDE/app/src/EDocumentDEHelper.Codeunit.al
@@ -288,6 +288,27 @@ codeunit 11038 "E-Document DE Helper"
end;
end;
+ ///
+ /// Tells whether a signed amount is reported as a charge or as an allowance.
+ ///
+ /// The signed amount of the allowance/charge.
+ /// True for a charge, false for an allowance.
+ internal procedure IsCharge(SignedAmount: Decimal): Boolean
+ begin
+ exit(SignedAmount >= 0);
+ end;
+
+ ///
+ /// Gets the amount to report for a signed allowance/charge amount. The e-document formats report allowances with a
+ /// positive amount and distinguish them from charges through the charge indicator.
+ ///
+ /// The signed amount of the allowance/charge.
+ /// The absolute amount.
+ internal procedure GetReportedAmount(SignedAmount: Decimal): Decimal
+ begin
+ exit(Abs(SignedAmount));
+ end;
+
[TryFunction]
local procedure TryValidateRoutingNo(RoutingNo: Text[50])
begin
diff --git a/src/Apps/DE/EDocumentDE/app/src/EDocumentServiceDE.PageExt.al b/src/Apps/DE/EDocumentDE/app/src/EDocumentServiceDE.PageExt.al
index dbff16c6562..d7797beb491 100644
--- a/src/Apps/DE/EDocumentDE/app/src/EDocumentServiceDE.PageExt.al
+++ b/src/Apps/DE/EDocumentDE/app/src/EDocumentServiceDE.PageExt.al
@@ -42,6 +42,11 @@ pageextension 13915 "E-Document Service DE" extends "E-Document Service"
}
#pragma warning restore AS0125
}
+ modify("Item Charge E-Invoice Mapping")
+ {
+ Visible = true;
+ }
+
modify("Export Format")
{
trigger OnBeforeValidate()
diff --git a/src/Apps/DE/EDocumentDE/app/src/XRechnung/ExportXRechnungDocument.Codeunit.al b/src/Apps/DE/EDocumentDE/app/src/XRechnung/ExportXRechnungDocument.Codeunit.al
index af77d02b7da..9911d23757f 100644
--- a/src/Apps/DE/EDocumentDE/app/src/XRechnung/ExportXRechnungDocument.Codeunit.al
+++ b/src/Apps/DE/EDocumentDE/app/src/XRechnung/ExportXRechnungDocument.Codeunit.al
@@ -43,6 +43,10 @@ codeunit 13916 "Export XRechnung Document"
PeppolVATHelper: Codeunit "PEPPOL VAT Helper";
TypeHelper: Codeunit "Type Helper";
EDocumentDEHelper: Codeunit "E-Document DE Helper";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ ItemChargeStructures: Dictionary of [Integer, Integer];
+ LineLevelItemChargeAmounts: Dictionary of [Integer, Decimal];
+ LineLevelItemChargeLineNos: Dictionary of [Integer, List of [Integer]];
FeatureNameTok: Label 'E-document XRechnung Format', Locked = true;
StartEventNameTok: Label 'E-document XRechnung export started', Locked = true;
EndEventNameTok: Label 'E-document XRechnung export completed', Locked = true;
@@ -172,6 +176,7 @@ codeunit 13916 "Export XRechnung Document"
InsertAttachment(RootXMLNode, Database::"Sales Invoice Header", SalesInvoiceHeader."No.");
CalculateLineAmounts(SalesInvoiceHeader, SalesInvLine, Currency, LineAmounts);
DetectNotSubjectToVATLines(SalesInvLine);
+ ClassifyItemCharges(SalesInvoiceHeader, SalesInvLine);
InsertAccountingSupplierParty(SalesInvoiceHeader."Responsibility Center", SalesInvoiceHeader."Salesperson Code", RootXMLNode);
InsertAccountingCustomerParty(RootXMLNode, SalesInvoiceHeader);
InsertDelivery(RootXMLNode, SalesInvoiceHeader);
@@ -179,6 +184,7 @@ codeunit 13916 "Export XRechnung Document"
InsertPaymentTerms(RootXMLNode, SalesInvoiceHeader."Payment Terms Code");
InsertVATAmounts(SalesInvLine, LineVATAmount, LineAmount, LineDiscAmount, SalesInvoiceHeader."Prices Including VAT", Currency);
InsertInvDiscountAllowanceCharge(LineAmounts, SalesInvLine, CurrencyCode, RootXMLNode, LineDiscAmount, LineAmount, Currency."Amount Rounding Precision");
+ InsertItemChargeAllowanceCharges(RootXMLNode, SalesInvLine, CurrencyCode);
InsertTaxTotal(RootXMLNode, SalesInvLine, CurrencyCode, LineAmount, LineVATAmount);
InsertLegalMonetaryTotal(RootXMLNode, SalesInvLine, LineAmounts, CurrencyCode);
InsertInvoiceLines(RootXMLNode, SalesInvLine, Currency, CurrencyCode, SalesInvoiceHeader."Prices Including VAT");
@@ -220,6 +226,7 @@ codeunit 13916 "Export XRechnung Document"
InsertAttachment(RootXMLNode, Database::"Sales Cr.Memo Header", SalesCrMemoHeader."No.");
CalculateLineAmounts(SalesCrMemoHeader, SalesCrMemoLine, Currency, LineAmounts);
DetectNotSubjectToVATLines(SalesCrMemoLine);
+ ClassifyItemCharges(SalesCrMemoHeader, SalesCrMemoLine);
InsertAccountingSupplierParty(SalesCrMemoHeader."Responsibility Center", SalesCrMemoHeader."Salesperson Code", RootXMLNode);
InsertAccountingCustomerParty(RootXMLNode, SalesCrMemoHeader);
InsertDelivery(RootXMLNode, SalesCrMemoHeader);
@@ -227,6 +234,7 @@ codeunit 13916 "Export XRechnung Document"
InsertPaymentTerms(RootXMLNode, SalesCrMemoHeader."Payment Terms Code");
InsertVATAmounts(SalesCrMemoLine, LineVATAmount, LineAmount, LineDiscAmount, SalesCrMemoHeader."Prices Including VAT", Currency);
InsertInvDiscountAllowanceCharge(LineAmounts, SalesCrMemoLine, CurrencyCode, RootXMLNode, LineDiscAmount, LineAmount, Currency."Amount Rounding Precision");
+ InsertItemChargeAllowanceCharges(RootXMLNode, SalesCrMemoLine, CurrencyCode);
InsertTaxTotal(RootXMLNode, SalesCrMemoLine, CurrencyCode, LineAmount, LineVATAmount);
InsertLegalMonetaryTotal(RootXMLNode, SalesCrMemoLine, LineAmounts, CurrencyCode);
InsertCrMemoLines(RootXMLNode, SalesCrMemoLine, Currency, CurrencyCode, SalesCrMemoHeader."Prices Including VAT");
@@ -253,6 +261,7 @@ codeunit 13916 "Export XRechnung Document"
begin
Clear(ItemGTINCache);
GetSetups();
+ ClearItemChargeClassification();
TransferToSalesInvoiceHeader(ServiceInvoiceHeader, SalesInvoiceHeader);
SalesInvoiceHeader."Company Bank Account Code" := ServiceInvoiceHeader."Company Bank Account Code";
ServiceInvoiceLine.SetRange("Document No.", ServiceInvoiceHeader."No.");
@@ -312,6 +321,7 @@ codeunit 13916 "Export XRechnung Document"
begin
Clear(ItemGTINCache);
GetSetups();
+ ClearItemChargeClassification();
TransferToSalesCrMemoHeader(ServiceCrMemoHeader, SalesCrMemoHeader);
SalesCrMemoHeader."Company Bank Account Code" := ServiceCrMemoHeader."Company Bank Account Code";
ServiceCrMemoLine.SetRange("Document No.", ServiceCrMemoHeader."No.");
@@ -472,6 +482,456 @@ codeunit 13916 "Export XRechnung Document"
until SalesCrMemoLine.Next() = 0;
end;
+ #region ItemCharge
+ local procedure ClassifyItemCharges(SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvLine: Record "Sales Invoice Line")
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ TargetSalesInvLine: Record "Sales Invoice Line";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ExportedLineExists: Boolean;
+ begin
+ ClearItemChargeClassification();
+
+ ChargeSalesInvLine.CopyFilters(SalesInvLine);
+ ChargeSalesInvLine.SetRange(Type, ChargeSalesInvLine.Type::"Charge (Item)");
+ if not ChargeSalesInvLine.FindSet() then
+ exit;
+
+ ExportedLineExists := ExportedNonChargeLineExists(SalesInvLine);
+
+ repeat
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvLine, TargetSalesInvLine);
+ // The export skips lines that the classification cannot see, such as lines without a number, without a
+ // quantity, or removed by a subscriber. Turning the item charges into allowances/charges would then leave
+ // the document without any invoice line, which BR-16 does not allow.
+ if not ExportedLineExists then
+ Structure := Structure::"Line with Unit Code";
+ // A line level allowance/charge needs an invoice line to live in. Without one it degrades to document level, so that the charge is never lost.
+ if (Structure = Structure::"Line Allowance/Charge") and not IsExportedLine(SalesInvLine, TargetSalesInvLine."Line No.") then
+ Structure := Structure::"Document Allowance/Charge";
+ ItemChargeStructures.Add(ChargeSalesInvLine."Line No.", Structure.AsInteger());
+ if Structure = Structure::"Line Allowance/Charge" then
+ AddLineLevelItemCharge(TargetSalesInvLine."Line No.", ChargeSalesInvLine."Line No.", ChargeSalesInvLine.Amount);
+ until ChargeSalesInvLine.Next() = 0;
+ end;
+
+ local procedure ClassifyItemCharges(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TargetSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ExportedLineExists: Boolean;
+ begin
+ ClearItemChargeClassification();
+
+ ChargeSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ if not ChargeSalesCrMemoLine.FindSet() then
+ exit;
+
+ ExportedLineExists := ExportedNonChargeLineExists(SalesCrMemoLine);
+
+ repeat
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesCrMemoHeader, ChargeSalesCrMemoLine, TargetSalesCrMemoLine);
+ // The export skips lines that the classification cannot see, such as lines without a number, without a
+ // quantity, or removed by a subscriber. Turning the item charges into allowances/charges would then leave
+ // the document without any credit memo line, which BR-16 does not allow.
+ if not ExportedLineExists then
+ Structure := Structure::"Line with Unit Code";
+ // A line level allowance/charge needs a credit memo line to live in. Without one it degrades to document level, so that the charge is never lost.
+ if (Structure = Structure::"Line Allowance/Charge") and not IsExportedLine(SalesCrMemoLine, TargetSalesCrMemoLine."Line No.") then
+ Structure := Structure::"Document Allowance/Charge";
+ ItemChargeStructures.Add(ChargeSalesCrMemoLine."Line No.", Structure.AsInteger());
+ if Structure = Structure::"Line Allowance/Charge" then
+ AddLineLevelItemCharge(TargetSalesCrMemoLine."Line No.", ChargeSalesCrMemoLine."Line No.", ChargeSalesCrMemoLine.Amount);
+ until ChargeSalesCrMemoLine.Next() = 0;
+ end;
+
+ local procedure ClearItemChargeClassification()
+ begin
+ Clear(ItemChargeStructures);
+ Clear(LineLevelItemChargeAmounts);
+ Clear(LineLevelItemChargeLineNos);
+ end;
+
+ local procedure AddLineLevelItemCharge(TargetLineNo: Integer; ChargeLineNo: Integer; ChargeAmount: Decimal)
+ var
+ ChargeLineNos: List of [Integer];
+ begin
+ if LineLevelItemChargeAmounts.ContainsKey(TargetLineNo) then
+ LineLevelItemChargeAmounts.Set(TargetLineNo, LineLevelItemChargeAmounts.Get(TargetLineNo) + ChargeAmount)
+ else
+ LineLevelItemChargeAmounts.Add(TargetLineNo, ChargeAmount);
+
+ if not LineLevelItemChargeLineNos.Get(TargetLineNo, ChargeLineNos) then
+ LineLevelItemChargeLineNos.Add(TargetLineNo, ChargeLineNos);
+ ChargeLineNos.Add(ChargeLineNo);
+ end;
+
+ local procedure ExportedNonChargeLineExists(var SalesInvLine: Record "Sales Invoice Line"): Boolean
+ var
+ ExportedSalesInvLine: Record "Sales Invoice Line";
+ begin
+ ExportedSalesInvLine.CopyFilters(SalesInvLine);
+ ExportedSalesInvLine.SetFilter(Type, '<>%1', ExportedSalesInvLine.Type::"Charge (Item)");
+ exit(not ExportedSalesInvLine.IsEmpty());
+ end;
+
+ local procedure ExportedNonChargeLineExists(var SalesCrMemoLine: Record "Sales Cr.Memo Line"): Boolean
+ var
+ ExportedSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ ExportedSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ExportedSalesCrMemoLine.SetFilter(Type, '<>%1', ExportedSalesCrMemoLine.Type::"Charge (Item)");
+ exit(not ExportedSalesCrMemoLine.IsEmpty());
+ end;
+
+ local procedure IsExportedLine(var SalesInvLine: Record "Sales Invoice Line"; LineNo: Integer): Boolean
+ var
+ ExportedSalesInvLine: Record "Sales Invoice Line";
+ begin
+ if LineNo = 0 then
+ exit(false);
+ ExportedSalesInvLine.CopyFilters(SalesInvLine);
+ ExportedSalesInvLine.SetRange("Line No.", LineNo);
+ exit(not ExportedSalesInvLine.IsEmpty());
+ end;
+
+ local procedure IsExportedLine(var SalesCrMemoLine: Record "Sales Cr.Memo Line"; LineNo: Integer): Boolean
+ var
+ ExportedSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ if LineNo = 0 then
+ exit(false);
+ ExportedSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ExportedSalesCrMemoLine.SetRange("Line No.", LineNo);
+ exit(not ExportedSalesCrMemoLine.IsEmpty());
+ end;
+
+ local procedure InsertItemChargeAllowanceCharges(var RootXMLNode: XmlElement; var SalesInvLine: Record "Sales Invoice Line"; CurrencyCode: Code[10])
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ VATEXCode: Text;
+ VATClauseDescription: Text;
+ begin
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesInvLine.CopyFilters(SalesInvLine);
+ ChargeSalesInvLine.SetRange(Type, ChargeSalesInvLine.Type::"Charge (Item)");
+ if not ChargeSalesInvLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesInvLine."Line No.") then begin
+ PeppolVATHelper.GetVATClauseInfo(
+ ChargeSalesInvLine."VAT Bus. Posting Group", ChargeSalesInvLine."VAT Prod. Posting Group", DocumentLanguageCode, VATEXCode, VATClauseDescription);
+ InsertItemChargeAllowanceCharge(RootXMLNode, ChargeSalesInvLine, CurrencyCode, true, VATEXCode, VATClauseDescription);
+ end;
+ until ChargeSalesInvLine.Next() = 0;
+ end;
+
+ local procedure InsertLineLevelItemChargeAllowanceCharges(var InvoiceLineElement: XmlElement; var SalesInvLine: Record "Sales Invoice Line"; CurrencyCode: Code[10])
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ ChargeLineNos: List of [Integer];
+ ChargeLineNo: Integer;
+ begin
+ if not LineLevelItemChargeLineNos.Get(SalesInvLine."Line No.", ChargeLineNos) then
+ exit;
+
+ foreach ChargeLineNo in ChargeLineNos do begin
+ ChargeSalesInvLine.Get(SalesInvLine."Document No.", ChargeLineNo);
+ InsertItemChargeAllowanceCharge(InvoiceLineElement, ChargeSalesInvLine, CurrencyCode, false, '', '');
+ end;
+ end;
+
+ local procedure InsertItemChargeAllowanceCharges(var RootXMLNode: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; CurrencyCode: Code[10])
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ VATEXCode: Text;
+ VATClauseDescription: Text;
+ begin
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ if not ChargeSalesCrMemoLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesCrMemoLine."Line No.") then begin
+ PeppolVATHelper.GetVATClauseInfo(
+ ChargeSalesCrMemoLine."VAT Bus. Posting Group", ChargeSalesCrMemoLine."VAT Prod. Posting Group", DocumentLanguageCode, VATEXCode, VATClauseDescription);
+ InsertItemChargeAllowanceCharge(RootXMLNode, ChargeSalesCrMemoLine, CurrencyCode, true, VATEXCode, VATClauseDescription);
+ end;
+ until ChargeSalesCrMemoLine.Next() = 0;
+ end;
+
+ local procedure InsertLineLevelItemChargeAllowanceCharges(var CrMemoLineElement: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; CurrencyCode: Code[10])
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ChargeLineNos: List of [Integer];
+ ChargeLineNo: Integer;
+ begin
+ if not LineLevelItemChargeLineNos.Get(SalesCrMemoLine."Line No.", ChargeLineNos) then
+ exit;
+
+ foreach ChargeLineNo in ChargeLineNos do begin
+ ChargeSalesCrMemoLine.Get(SalesCrMemoLine."Document No.", ChargeLineNo);
+ InsertItemChargeAllowanceCharge(CrMemoLineElement, ChargeSalesCrMemoLine, CurrencyCode, false, '', '');
+ end;
+ end;
+
+ local procedure InsertItemChargeAllowanceCharge(var ParentXMLNode: XmlElement; ChargeSalesInvLine: Record "Sales Invoice Line"; CurrencyCode: Code[10]; InsertTaxCat: Boolean; VATEXCode: Text; VATClauseDescription: Text)
+ var
+ AllowanceChargeElement: XmlElement;
+ ReasonCode: Code[10];
+ ReasonText: Text[100];
+ begin
+ GetItemChargeReason(ChargeSalesInvLine, ReasonCode, ReasonText);
+ AllowanceChargeElement :=
+ CreateAllowanceCharge(
+ EDocumentDEHelper.IsCharge(ChargeSalesInvLine.Amount),
+ ReasonCode,
+ ReasonText,
+ FormatDecimal(EDocumentDEHelper.GetReportedAmount(ChargeSalesInvLine.Amount), AlwaysIncludeTwoDecimalPlacesForAmountFields),
+ CurrencyCode);
+ if InsertTaxCat then
+ InsertTaxCategory(
+ AllowanceChargeElement,
+ GetTaxCategoryID(ChargeSalesInvLine."Tax Category", ChargeSalesInvLine."VAT Bus. Posting Group", ChargeSalesInvLine."VAT Prod. Posting Group"),
+ ChargeSalesInvLine."VAT %", VATEXCode, VATClauseDescription);
+ ParentXMLNode.Add(AllowanceChargeElement);
+ end;
+
+ local procedure InsertItemChargeAllowanceCharge(var ParentXMLNode: XmlElement; ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line"; CurrencyCode: Code[10]; InsertTaxCat: Boolean; VATEXCode: Text; VATClauseDescription: Text)
+ var
+ AllowanceChargeElement: XmlElement;
+ ReasonCode: Code[10];
+ ReasonText: Text[100];
+ begin
+ GetItemChargeReason(ChargeSalesCrMemoLine, ReasonCode, ReasonText);
+ AllowanceChargeElement :=
+ CreateAllowanceCharge(
+ EDocumentDEHelper.IsCharge(ChargeSalesCrMemoLine.Amount),
+ ReasonCode,
+ ReasonText,
+ FormatDecimal(EDocumentDEHelper.GetReportedAmount(ChargeSalesCrMemoLine.Amount), AlwaysIncludeTwoDecimalPlacesForAmountFields),
+ CurrencyCode);
+ if InsertTaxCat then
+ InsertTaxCategory(
+ AllowanceChargeElement,
+ GetTaxCategoryID(ChargeSalesCrMemoLine."Tax Category", ChargeSalesCrMemoLine."VAT Bus. Posting Group", ChargeSalesCrMemoLine."VAT Prod. Posting Group"),
+ ChargeSalesCrMemoLine."VAT %", VATEXCode, VATClauseDescription);
+ ParentXMLNode.Add(AllowanceChargeElement);
+ end;
+
+ ///
+ /// Creates a UBL cac:AllowanceCharge element with its child elements in the order the UBL schema requires.
+ /// The caller adds the returned element to its parent, and may append a cac:TaxCategory to it beforehand,
+ /// which the UBL schema expects after the elements written here.
+ ///
+ /// True writes a charge, false writes an allowance.
+ /// The allowance/charge reason code. Not written when empty.
+ /// The allowance/charge reason. Not written when empty.
+ /// The already formatted, non negative amount of the allowance/charge.
+ /// The currency of the amount.
+ /// The cac:AllowanceCharge element.
+ local procedure CreateAllowanceCharge(ChargeIndicator: Boolean; ReasonCode: Code[10]; ReasonText: Text; FormattedAmount: Text; CurrencyCode: Code[10]) AllowanceChargeElement: XmlElement
+ begin
+ AllowanceChargeElement := XmlElement.Create('AllowanceCharge', XmlNamespaceCAC);
+ AllowanceChargeElement.Add(XmlElement.Create('ChargeIndicator', XmlNamespaceCBC, GetChargeIndicator(ChargeIndicator)));
+ if ReasonCode <> '' then
+ AllowanceChargeElement.Add(XmlElement.Create('AllowanceChargeReasonCode', XmlNamespaceCBC, ReasonCode));
+ if ReasonText <> '' then
+ AllowanceChargeElement.Add(XmlElement.Create('AllowanceChargeReason', XmlNamespaceCBC, ReasonText));
+ AllowanceChargeElement.Add(
+ XmlElement.Create('Amount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormattedAmount));
+ end;
+
+ local procedure GetChargeIndicator(IsChargeIndicator: Boolean): Text
+ begin
+ if IsChargeIndicator then
+ exit('true');
+ exit('false');
+ end;
+
+ local procedure GetItemChargeReason(ChargeSalesInvLine: Record "Sales Invoice Line"; var ReasonCode: Code[10]; var ReasonText: Text[100])
+ begin
+ EDocItemChargeMapping.GetItemChargeReason(ChargeSalesInvLine."No.", ReasonCode, ReasonText);
+ if ReasonText = '' then
+ ReasonText := ChargeSalesInvLine.Description;
+ if (ReasonText = '') and (ReasonCode = '') then
+ ReasonText := ChargeSalesInvLine."No.";
+ end;
+
+ local procedure GetItemChargeReason(ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line"; var ReasonCode: Code[10]; var ReasonText: Text[100])
+ begin
+ EDocItemChargeMapping.GetItemChargeReason(ChargeSalesCrMemoLine."No.", ReasonCode, ReasonText);
+ if ReasonText = '' then
+ ReasonText := ChargeSalesCrMemoLine.Description;
+ if (ReasonText = '') and (ReasonCode = '') then
+ ReasonText := ChargeSalesCrMemoLine."No.";
+ end;
+
+ local procedure GetDocumentLevelItemChargeTotals(var SalesInvLine: Record "Sales Invoice Line"; var TotalChargeAmount: Decimal; var TotalAllowanceAmount: Decimal)
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ begin
+ TotalChargeAmount := 0;
+ TotalAllowanceAmount := 0;
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesInvLine.CopyFilters(SalesInvLine);
+ ChargeSalesInvLine.SetRange(Type, ChargeSalesInvLine.Type::"Charge (Item)");
+ if not ChargeSalesInvLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesInvLine."Line No.") then
+ if EDocumentDEHelper.IsCharge(ChargeSalesInvLine.Amount) then
+ TotalChargeAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesInvLine.Amount)
+ else
+ TotalAllowanceAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesInvLine.Amount);
+ until ChargeSalesInvLine.Next() = 0;
+ end;
+
+ local procedure GetDocumentLevelItemChargeTotals(var SalesCrMemoLine: Record "Sales Cr.Memo Line"; var TotalChargeAmount: Decimal; var TotalAllowanceAmount: Decimal)
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ TotalChargeAmount := 0;
+ TotalAllowanceAmount := 0;
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ if not ChargeSalesCrMemoLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesCrMemoLine."Line No.") then
+ if EDocumentDEHelper.IsCharge(ChargeSalesCrMemoLine.Amount) then
+ TotalChargeAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesCrMemoLine.Amount)
+ else
+ TotalAllowanceAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesCrMemoLine.Amount);
+ until ChargeSalesCrMemoLine.Next() = 0;
+ end;
+
+ local procedure GetLineLevelItemChargeAmount(var SalesInvLine: Record "Sales Invoice Line") TotalChargeAmount: Decimal
+ begin
+ if not LineLevelItemChargeAmounts.Get(SalesInvLine."Line No.", TotalChargeAmount) then
+ exit(0);
+ end;
+
+ local procedure GetLineLevelItemChargeAmount(var SalesCrMemoLine: Record "Sales Cr.Memo Line") TotalChargeAmount: Decimal
+ begin
+ if not LineLevelItemChargeAmounts.Get(SalesCrMemoLine."Line No.", TotalChargeAmount) then
+ exit(0);
+ end;
+
+ local procedure InsertInvoicedQuantity(var InvoiceLineElement: XmlElement; SalesInvLine: Record "Sales Invoice Line")
+ begin
+ if IsItemChargeInvoiceLine(SalesInvLine."Line No.") then begin
+ InvoiceLineElement.Add(
+ XmlElement.Create(
+ 'InvoicedQuantity', XmlNamespaceCBC,
+ XmlAttribute.Create('unitCode', EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(SalesInvLine."No.")),
+ FormatDecimalUnlimited(EDocItemChargeMapping.GetFallbackQuantity(GetLineNetAmount(SalesInvLine)))));
+ exit;
+ end;
+
+ InvoiceLineElement.Add(
+ XmlElement.Create(
+ 'InvoicedQuantity', XmlNamespaceCBC,
+ XmlAttribute.Create('unitCode', GetUoMCode(SalesInvLine."Unit of Measure Code")),
+ FormatDecimalUnlimited(SalesInvLine.Quantity)));
+ end;
+
+ local procedure InsertCreditedQuantity(var CrMemoLineElement: XmlElement; SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ if IsItemChargeInvoiceLine(SalesCrMemoLine."Line No.") then begin
+ CrMemoLineElement.Add(
+ XmlElement.Create(
+ 'CreditedQuantity', XmlNamespaceCBC,
+ XmlAttribute.Create('unitCode', EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(SalesCrMemoLine."No.")),
+ FormatDecimalUnlimited(EDocItemChargeMapping.GetFallbackQuantity(GetLineNetAmount(SalesCrMemoLine)))));
+ exit;
+ end;
+
+ CrMemoLineElement.Add(
+ XmlElement.Create(
+ 'CreditedQuantity', XmlNamespaceCBC,
+ XmlAttribute.Create('unitCode', GetUoMCode(SalesCrMemoLine."Unit of Measure Code")),
+ FormatDecimalUnlimited(SalesCrMemoLine.Quantity)));
+ end;
+
+ local procedure GetInvoiceLineUnitPrice(SalesInvLine: Record "Sales Invoice Line"): Decimal
+ begin
+ if not IsItemChargeInvoiceLine(SalesInvLine."Line No.") then
+ exit(SalesInvLine."Unit Price");
+
+ exit(EDocItemChargeMapping.GetFallbackUnitPrice(GetLineNetAmount(SalesInvLine)));
+ end;
+
+ local procedure GetInvoiceLineUnitPrice(SalesCrMemoLine: Record "Sales Cr.Memo Line"): Decimal
+ begin
+ if not IsItemChargeInvoiceLine(SalesCrMemoLine."Line No.") then
+ exit(SalesCrMemoLine."Unit Price");
+
+ exit(EDocItemChargeMapping.GetFallbackUnitPrice(GetLineNetAmount(SalesCrMemoLine)));
+ end;
+
+ local procedure GetLineNetAmount(SalesInvLine: Record "Sales Invoice Line"): Decimal
+ begin
+ exit(SalesInvLine.Amount + SalesInvLine."Inv. Discount Amount");
+ end;
+
+ local procedure GetLineNetAmount(SalesCrMemoLine: Record "Sales Cr.Memo Line"): Decimal
+ begin
+ exit(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount");
+ end;
+
+ local procedure IsItemChargeAllowanceCharge(LineNo: Integer): Boolean
+ var
+ Structure: Enum "Item Charge E-Doc. Structure";
+ begin
+ if not TryGetItemChargeStructure(LineNo, Structure) then
+ exit(false);
+ exit(Structure <> Structure::"Line with Unit Code");
+ end;
+
+ local procedure IsItemChargeInvoiceLine(LineNo: Integer): Boolean
+ var
+ Structure: Enum "Item Charge E-Doc. Structure";
+ begin
+ if not TryGetItemChargeStructure(LineNo, Structure) then
+ exit(false);
+ exit(Structure = Structure::"Line with Unit Code");
+ end;
+
+ local procedure IsDocumentLevelItemCharge(LineNo: Integer): Boolean
+ var
+ Structure: Enum "Item Charge E-Doc. Structure";
+ begin
+ if not TryGetItemChargeStructure(LineNo, Structure) then
+ exit(false);
+ exit(Structure = Structure::"Document Allowance/Charge");
+ end;
+
+ local procedure TryGetItemChargeStructure(LineNo: Integer; var Structure: Enum "Item Charge E-Doc. Structure"): Boolean
+ begin
+ if not ItemChargeStructures.ContainsKey(LineNo) then
+ exit(false);
+ Structure := Enum::"Item Charge E-Doc. Structure".FromInteger(ItemChargeStructures.Get(LineNo));
+ exit(true);
+ end;
+ #endregion
+
local procedure InsertAccountingSupplierParty(RespCenterCode: Code[10]; SalespersonCode: Code[20]; var RootXMLNode: XmlElement)
var
AccountingSupplierPartyElement: XmlElement;
@@ -1124,13 +1584,25 @@ codeunit 13916 "Export XRechnung Document"
local procedure InsertLegalMonetaryTotal(var RootXMLNode: XmlElement; var SalesInvLine: Record "Sales Invoice Line"; LineAmounts: Dictionary of [Text, Decimal]; CurrencyCode: Code[10])
var
LegalMonetaryTotalElement: XmlElement;
+ ItemChargeTotalAmount: Decimal;
+ ItemChargeAllowanceTotalAmount: Decimal;
+ AllowanceTotalAmount: Decimal;
+ LineExtensionAmount: Decimal;
begin
+ GetDocumentLevelItemChargeTotals(SalesInvLine, ItemChargeTotalAmount, ItemChargeAllowanceTotalAmount);
+ AllowanceTotalAmount := LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")) + ItemChargeAllowanceTotalAmount;
+ LineExtensionAmount :=
+ LineAmounts.Get(SalesInvLine.FieldName(Amount)) + LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount"))
+ - ItemChargeTotalAmount + ItemChargeAllowanceTotalAmount;
+
LegalMonetaryTotalElement := XmlElement.Create('LegalMonetaryTotal', XmlNamespaceCAC);
- LegalMonetaryTotalElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName(Amount)) + LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ LegalMonetaryTotalElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineExtensionAmount, AlwaysIncludeTwoDecimalPlacesForAmountFields)));
LegalMonetaryTotalElement.Add(XmlElement.Create('TaxExclusiveAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName(Amount)), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
LegalMonetaryTotalElement.Add(XmlElement.Create('TaxInclusiveAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Amount Including VAT")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
- if LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")) > 0 then
- LegalMonetaryTotalElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ if AllowanceTotalAmount > 0 then
+ LegalMonetaryTotalElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(AllowanceTotalAmount, AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ if ItemChargeTotalAmount > 0 then
+ LegalMonetaryTotalElement.Add(XmlElement.Create('ChargeTotalAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(ItemChargeTotalAmount, AlwaysIncludeTwoDecimalPlacesForAmountFields)));
LegalMonetaryTotalElement.Add(XmlElement.Create('PayableAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Amount Including VAT")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
RootXMLNode.Add(LegalMonetaryTotalElement);
end;
@@ -1138,13 +1610,25 @@ codeunit 13916 "Export XRechnung Document"
local procedure InsertLegalMonetaryTotal(var RootXMLNode: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; LineAmounts: Dictionary of [Text, Decimal]; CurrencyCode: Code[10])
var
LegalMonetaryTotalElement: XmlElement;
+ ItemChargeTotalAmount: Decimal;
+ ItemChargeAllowanceTotalAmount: Decimal;
+ AllowanceTotalAmount: Decimal;
+ LineExtensionAmount: Decimal;
begin
+ GetDocumentLevelItemChargeTotals(SalesCrMemoLine, ItemChargeTotalAmount, ItemChargeAllowanceTotalAmount);
+ AllowanceTotalAmount := LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")) + ItemChargeAllowanceTotalAmount;
+ LineExtensionAmount :=
+ LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)) + LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount"))
+ - ItemChargeTotalAmount + ItemChargeAllowanceTotalAmount;
+
LegalMonetaryTotalElement := XmlElement.Create('LegalMonetaryTotal', XmlNamespaceCAC);
- LegalMonetaryTotalElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)) + LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ LegalMonetaryTotalElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineExtensionAmount, AlwaysIncludeTwoDecimalPlacesForAmountFields)));
LegalMonetaryTotalElement.Add(XmlElement.Create('TaxExclusiveAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
LegalMonetaryTotalElement.Add(XmlElement.Create('TaxInclusiveAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Amount Including VAT")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
- if LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")) > 0 then
- LegalMonetaryTotalElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ if AllowanceTotalAmount > 0 then
+ LegalMonetaryTotalElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(AllowanceTotalAmount, AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ if ItemChargeTotalAmount > 0 then
+ LegalMonetaryTotalElement.Add(XmlElement.Create('ChargeTotalAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(ItemChargeTotalAmount, AlwaysIncludeTwoDecimalPlacesForAmountFields)));
LegalMonetaryTotalElement.Add(XmlElement.Create('PayableAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Amount Including VAT")), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
RootXMLNode.Add(LegalMonetaryTotalElement);
end;
@@ -1194,7 +1678,8 @@ codeunit 13916 "Export XRechnung Document"
begin
SalesInvLine.FindSet();
repeat
- InsertInvoiceLine(InvoiceElement, SalesInvLine, Currency, CurrencyCode, PricesIncVAT);
+ if not IsItemChargeAllowanceCharge(SalesInvLine."Line No.") then
+ InsertInvoiceLine(InvoiceElement, SalesInvLine, Currency, CurrencyCode, PricesIncVAT);
until SalesInvLine.Next() = 0;
end;
@@ -1211,8 +1696,8 @@ codeunit 13916 "Export XRechnung Document"
if PricesIncVAT then
ExcludeVAT(SalesInvLine, Currency."Amount Rounding Precision");
InvoiceLineElement.Add(XmlElement.Create('ID', XmlNamespaceCBC, Format(SalesInvLine."Line No.")));
- InvoiceLineElement.Add(XmlElement.Create('InvoicedQuantity', XmlNamespaceCBC, XmlAttribute.Create('unitCode', GetUoMCode(SalesInvLine."Unit of Measure Code")), FormatDecimalUnlimited(SalesInvLine.Quantity)));
- InvoiceLineElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(SalesInvLine.Amount + SalesInvLine."Inv. Discount Amount", AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ InsertInvoicedQuantity(InvoiceLineElement, SalesInvLine);
+ InvoiceLineElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(SalesInvLine.Amount + SalesInvLine."Inv. Discount Amount" + GetLineLevelItemChargeAmount(SalesInvLine), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
if SalesInvLine."Shipment Date" <> 0D then
InsertInvoicePeriod(InvoiceLineElement, SalesInvLine."Shipment Date", SalesInvLine."Shipment Date");
InsertOrderLineReference(InvoiceLineElement, SalesInvLine."Line No.");
@@ -1221,9 +1706,10 @@ codeunit 13916 "Export XRechnung Document"
InvoiceLineElement, 'LineDiscount',
SalesInvLine."Line Discount Amount", SalesInvLine."Unit Price" * SalesInvLine.Quantity,
CurrencyCode, SalesInvLine."Line Discount %");
+ InsertLineLevelItemChargeAllowanceCharges(InvoiceLineElement, SalesInvLine, CurrencyCode);
InsertItem(InvoiceLineElement, SalesInvLine);
- InsertPrice(InvoiceLineElement, SalesInvLine."Unit Price", CurrencyCode);
+ InsertPrice(InvoiceLineElement, GetInvoiceLineUnitPrice(SalesInvLine), CurrencyCode);
OnBeforeAddInvoiceLineElement(InvoiceLineElement, SalesInvLine, Currency, CurrencyCode, PricesIncVAT);
InvoiceElement.Add(InvoiceLineElement);
end;
@@ -1233,7 +1719,8 @@ codeunit 13916 "Export XRechnung Document"
begin
SalesCrMemoLine.FindSet();
repeat
- InsertCrMemoLine(CrMemoElement, SalesCrMemoLine, Currency, CurrencyCode, PricesIncVAT);
+ if not IsItemChargeAllowanceCharge(SalesCrMemoLine."Line No.") then
+ InsertCrMemoLine(CrMemoElement, SalesCrMemoLine, Currency, CurrencyCode, PricesIncVAT);
until SalesCrMemoLine.Next() = 0;
end;
@@ -1250,8 +1737,8 @@ codeunit 13916 "Export XRechnung Document"
if PricesIncVAT then
ExcludeVAT(SalesCrMemoLine, Currency."Amount Rounding Precision");
CrMemoLineElement.Add(XmlElement.Create('ID', XmlNamespaceCBC, Format(SalesCrMemoLine."Line No.")));
- CrMemoLineElement.Add(XmlElement.Create('CreditedQuantity', XmlNamespaceCBC, XmlAttribute.Create('unitCode', GetUoMCode(SalesCrMemoLine."Unit of Measure Code")), FormatDecimalUnlimited(SalesCrMemoLine.Quantity)));
- CrMemoLineElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount", AlwaysIncludeTwoDecimalPlacesForAmountFields)));
+ InsertCreditedQuantity(CrMemoLineElement, SalesCrMemoLine);
+ CrMemoLineElement.Add(XmlElement.Create('LineExtensionAmount', XmlNamespaceCBC, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount" + GetLineLevelItemChargeAmount(SalesCrMemoLine), AlwaysIncludeTwoDecimalPlacesForAmountFields)));
if SalesCrMemoLine."Shipment Date" <> 0D then
InsertInvoicePeriod(CrMemoLineElement, SalesCrMemoLine."Shipment Date", SalesCrMemoLine."Shipment Date");
InsertOrderLineReference(CrMemoLineElement, SalesCrMemoLine."Line No.");
@@ -1260,9 +1747,10 @@ codeunit 13916 "Export XRechnung Document"
CrMemoLineElement, 'LineDiscount',
SalesCrMemoLine."Line Discount Amount", SalesCrMemoLine."Unit Price" * SalesCrMemoLine.Quantity,
CurrencyCode, SalesCrMemoLine."Line Discount %");
+ InsertLineLevelItemChargeAllowanceCharges(CrMemoLineElement, SalesCrMemoLine, CurrencyCode);
InsertItem(CrMemoLineElement, SalesCrMemoLine);
- InsertPrice(CrMemoLineElement, SalesCrMemoLine."Unit Price", CurrencyCode);
+ InsertPrice(CrMemoLineElement, GetInvoiceLineUnitPrice(SalesCrMemoLine), CurrencyCode);
OnBeforeAddCrMemoLineElement(CrMemoLineElement, SalesCrMemoLine, Currency, CurrencyCode, PricesIncVAT);
CrMemoElement.Add(CrMemoLineElement);
end;
diff --git a/src/Apps/DE/EDocumentDE/app/src/ZUGFeRD/ExportZUGFeRDDocument.Codeunit.al b/src/Apps/DE/EDocumentDE/app/src/ZUGFeRD/ExportZUGFeRDDocument.Codeunit.al
index f742be17a9e..2373cf5abcb 100644
--- a/src/Apps/DE/EDocumentDE/app/src/ZUGFeRD/ExportZUGFeRDDocument.Codeunit.al
+++ b/src/Apps/DE/EDocumentDE/app/src/ZUGFeRD/ExportZUGFeRDDocument.Codeunit.al
@@ -38,6 +38,11 @@ codeunit 13917 "Export ZUGFeRD Document"
EDocumentService: Record "E-Document Service";
FeatureTelemetry: Codeunit "Feature Telemetry";
PeppolVATHelper: Codeunit "PEPPOL VAT Helper";
+ EDocumentDEHelper: Codeunit "E-Document DE Helper";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ ItemChargeStructures: Dictionary of [Integer, Integer];
+ LineLevelItemChargeAmounts: Dictionary of [Integer, Decimal];
+ LineLevelItemChargeLineNos: Dictionary of [Integer, List of [Integer]];
FeatureNameTok: Label 'E-document ZUGFeRD Format', Locked = true;
StartEventNameTok: Label 'E-document ZUGFeRD export started', Locked = true;
EndEventNameTok: Label 'E-document ZUGFeRD export completed', Locked = true;
@@ -294,6 +299,7 @@ codeunit 13917 "Export ZUGFeRD Document"
XmlDoc.GetRoot(RootXMLNode);
InitializeNamespaces();
+ ClassifyItemCharges(SalesInvoiceHeader, SalesInvLine);
CurrencyCode := GetCurrencyCode(SalesInvoiceHeader."Currency Code", Currency);
CalculateLineAmounts(SalesInvoiceHeader, SalesInvLine, Currency, LineAmounts);
InsertVATAmounts(SalesInvLine, LineVATAmount, LineAmount, LineDiscAmount, SalesInvoiceHeader."Prices Including VAT", Currency);
@@ -329,6 +335,7 @@ codeunit 13917 "Export ZUGFeRD Document"
XmlDoc.GetRoot(RootXMLNode);
InitializeNamespaces();
+ ClassifyItemCharges(SalesCrMemoHeader, SalesCrMemoLine);
CurrencyCode := GetCurrencyCode(SalesCrMemoHeader."Currency Code", Currency);
CalculateLineAmounts(SalesCrMemoHeader, SalesCrMemoLine, Currency, LineAmounts);
InsertVATAmounts(SalesCrMemoLine, LineVATAmount, LineAmount, LineDiscAmount, SalesCrMemoHeader."Prices Including VAT", Currency);
@@ -358,6 +365,7 @@ codeunit 13917 "Export ZUGFeRD Document"
Clear(ItemGTINCache);
GetSetups();
FindEDocumentService();
+ ClearItemChargeClassification();
TransferToSalesInvoiceHeader(ServiceInvoiceHeader, SalesInvoiceHeader);
SalesInvoiceHeader."Company Bank Account Code" := ServiceInvoiceHeader."Company Bank Account Code";
ServiceInvoiceLine.SetRange("Document No.", ServiceInvoiceHeader."No.");
@@ -403,6 +411,7 @@ codeunit 13917 "Export ZUGFeRD Document"
Clear(ItemGTINCache);
GetSetups();
FindEDocumentService();
+ ClearItemChargeClassification();
TransferToSalesCrMemoHeader(ServiceCrMemoHeader, SalesCrMemoHeader);
SalesCrMemoHeader."Company Bank Account Code" := ServiceCrMemoHeader."Company Bank Account Code";
ServiceCrMemoLine.SetRange("Document No.", ServiceCrMemoHeader."No.");
@@ -795,6 +804,8 @@ codeunit 13917 "Export ZUGFeRD Document"
local procedure InsertApplicableHeaderTradeSettlement(var RootXMLNode: XmlElement; var SalesInvHeader: Record "Sales Invoice Header"; var SalesInvLine: Record "Sales Invoice Line"; CurrencyCode: Code[10]; var LineAmount: Dictionary of [Decimal, Decimal]; var LineVATAmount: Dictionary of [Decimal, Decimal]; var LineAmounts: Dictionary of [Text, Decimal]; var LineDiscAmount: Dictionary of [Decimal, Decimal])
var
SettlementElement, MonetarySummationElement : XmlElement;
+ ItemChargeTotalAmount: Decimal;
+ ItemChargeAllowanceTotalAmount: Decimal;
begin
SettlementElement := XmlElement.Create('ApplicableHeaderTradeSettlement', XmlNamespaceRAM);
@@ -802,11 +813,15 @@ codeunit 13917 "Export ZUGFeRD Document"
InsertPaymentMethod(SettlementElement, SalesInvHeader."Company Bank Account Code");
InsertTradeTax(SettlementElement, SalesInvLine, LineAmount, LineVATAmount);
InsertInvDiscountAllowanceCharge(SettlementElement, SalesInvLine, LineDiscAmount, LineAmounts);
+ InsertItemChargeAllowanceCharges(SettlementElement, SalesInvLine);
InsertPaymentTerms(SettlementElement, SalesInvHeader."Payment Terms Code", SalesInvHeader."Due Date");
+ GetDocumentLevelItemChargeTotals(SalesInvLine, ItemChargeTotalAmount, ItemChargeAllowanceTotalAmount);
MonetarySummationElement := XmlElement.Create('SpecifiedTradeSettlementHeaderMonetarySummation', XmlNamespaceRAM);
- MonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName(Amount)) + LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")))));
- MonetarySummationElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")))));
+ MonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName(Amount)) + LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")) - ItemChargeTotalAmount + ItemChargeAllowanceTotalAmount)));
+ if ItemChargeTotalAmount > 0 then
+ MonetarySummationElement.Add(XmlElement.Create('ChargeTotalAmount', XmlNamespaceRAM, FormatDecimal(ItemChargeTotalAmount)));
+ MonetarySummationElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Inv. Discount Amount")) + ItemChargeAllowanceTotalAmount)));
MonetarySummationElement.Add(XmlElement.Create('TaxBasisTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName(Amount)))));
MonetarySummationElement.Add(XmlElement.Create('TaxTotalAmount', XmlNamespaceRAM, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Amount Including VAT")) - LineAmounts.Get(SalesInvLine.FieldName(Amount)))));
MonetarySummationElement.Add(XmlElement.Create('GrandTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesInvLine.FieldName("Amount Including VAT")))));
@@ -820,6 +835,8 @@ codeunit 13917 "Export ZUGFeRD Document"
local procedure InsertApplicableHeaderTradeSettlement(var RootXMLNode: XmlElement; var SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line"; CurrencyCode: Code[10]; var LineAmount: Dictionary of [Decimal, Decimal]; var LineVATAmount: Dictionary of [Decimal, Decimal]; var LineAmounts: Dictionary of [Text, Decimal]; var LineDiscAmount: Dictionary of [Decimal, Decimal])
var
SettlementElement, MonetarySummationElement : XmlElement;
+ ItemChargeTotalAmount: Decimal;
+ ItemChargeAllowanceTotalAmount: Decimal;
begin
SettlementElement := XmlElement.Create('ApplicableHeaderTradeSettlement', XmlNamespaceRAM);
@@ -827,11 +844,15 @@ codeunit 13917 "Export ZUGFeRD Document"
InsertPaymentMethod(SettlementElement, SalesCrMemoHeader."Company Bank Account Code");
InsertTradeTax(SettlementElement, SalesCrMemoLine, LineAmount, LineVATAmount);
InsertInvDiscountAllowanceCharge(SettlementElement, SalesCrMemoLine, LineDiscAmount, LineAmounts);
+ InsertItemChargeAllowanceCharges(SettlementElement, SalesCrMemoLine);
InsertPaymentTerms(SettlementElement, SalesCrMemoHeader."Payment Terms Code", SalesCrMemoHeader."Due Date");
+ GetDocumentLevelItemChargeTotals(SalesCrMemoLine, ItemChargeTotalAmount, ItemChargeAllowanceTotalAmount);
MonetarySummationElement := XmlElement.Create('SpecifiedTradeSettlementHeaderMonetarySummation', XmlNamespaceRAM);
- MonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)) + LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")))));
- MonetarySummationElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")))));
+ MonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)) + LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")) - ItemChargeTotalAmount + ItemChargeAllowanceTotalAmount)));
+ if ItemChargeTotalAmount > 0 then
+ MonetarySummationElement.Add(XmlElement.Create('ChargeTotalAmount', XmlNamespaceRAM, FormatDecimal(ItemChargeTotalAmount)));
+ MonetarySummationElement.Add(XmlElement.Create('AllowanceTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Inv. Discount Amount")) + ItemChargeAllowanceTotalAmount)));
MonetarySummationElement.Add(XmlElement.Create('TaxBasisTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)))));
MonetarySummationElement.Add(XmlElement.Create('TaxTotalAmount', XmlNamespaceRAM, XmlAttribute.Create('currencyID', CurrencyCode), FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Amount Including VAT")) - LineAmounts.Get(SalesCrMemoLine.FieldName(Amount)))));
MonetarySummationElement.Add(XmlElement.Create('GrandTotalAmount', XmlNamespaceRAM, FormatDecimal(LineAmounts.Get(SalesCrMemoLine.FieldName("Amount Including VAT")))));
@@ -943,7 +964,8 @@ codeunit 13917 "Export ZUGFeRD Document"
SupplyChainTradeTransactionElement := XmlElement.Create('SupplyChainTradeTransaction', XmlNamespaceRSM);
if SalesInvoiceLine.FindSet() then
repeat
- InsertInvoiceLine(SupplyChainTradeTransactionElement, SalesInvoiceLine, Currency, CurrencyCode, SalesInvoiceHeader."Prices Including VAT");
+ if not IsItemChargeAllowanceCharge(SalesInvoiceLine."Line No.") then
+ InsertInvoiceLine(SupplyChainTradeTransactionElement, SalesInvoiceLine, Currency, CurrencyCode, SalesInvoiceHeader."Prices Including VAT");
until SalesInvoiceLine.Next() = 0;
InsertApplicableHeaderTradeAgreement(SupplyChainTradeTransactionElement, SalesInvoiceHeader);
InsertApplicableHeaderTradeDelivery(SupplyChainTradeTransactionElement, SalesInvoiceHeader);
@@ -1029,14 +1051,13 @@ codeunit 13917 "Export ZUGFeRD Document"
SpecifiedLineTradeAgreementElement := XmlElement.Create('SpecifiedLineTradeAgreement', XmlNamespaceRAM);
NetPriceProductTradePriceElement := XmlElement.Create('NetPriceProductTradePrice', XmlNamespaceRAM);
- ChargeAmountElement := XmlElement.Create('ChargeAmount', XmlNamespaceRAM, FormatDecimalUnlimited(SalesInvoiceLine."Unit Price"));
+ ChargeAmountElement := XmlElement.Create('ChargeAmount', XmlNamespaceRAM, FormatDecimalUnlimited(GetInvoiceLineUnitPrice(SalesInvoiceLine)));
NetPriceProductTradePriceElement.Add(ChargeAmountElement);
SpecifiedLineTradeAgreementElement.Add(NetPriceProductTradePriceElement);
InvoiceLineElement.Add(SpecifiedLineTradeAgreementElement);
SpecifiedLineTradeDeliveryElement := XmlElement.Create('SpecifiedLineTradeDelivery', XmlNamespaceRAM);
- BilledQuantityElement := XmlElement.Create('BilledQuantity', XmlNamespaceRAM, FormatDecimalUnlimited(SalesInvoiceLine.Quantity));
- BilledQuantityElement.SetAttribute('unitCode', GetUoMCode(SalesInvoiceLine."Unit of Measure Code"));
+ BilledQuantityElement := CreateBilledQuantityElement(SalesInvoiceLine);
SpecifiedLineTradeDeliveryElement.Add(BilledQuantityElement);
InvoiceLineElement.Add(SpecifiedLineTradeDeliveryElement);
@@ -1055,9 +1076,10 @@ codeunit 13917 "Export ZUGFeRD Document"
if SalesInvoiceLine."Line Discount Amount" <> 0 then
InsertAllowanceCharge(SpecifiedLineTradeSettlementElement, 'Line Discount', GetTaxCategoryID(SalesInvoiceLine."Tax Category", SalesInvoiceLine."VAT Bus. Posting Group",
SalesInvoiceLine."VAT Prod. Posting Group"), SalesInvoiceLine."Line Discount Amount", SalesInvoiceLine."VAT %", false);
+ InsertLineLevelItemChargeAllowanceCharges(SpecifiedLineTradeSettlementElement, SalesInvoiceLine);
SpecifiedTradeSettlementLineMonetarySummationElement := XmlElement.Create('SpecifiedTradeSettlementLineMonetarySummation', XmlNamespaceRAM);
- SpecifiedTradeSettlementLineMonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(SalesInvoiceLine.Amount + SalesInvoiceLine."Inv. Discount Amount")));
+ SpecifiedTradeSettlementLineMonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(SalesInvoiceLine.Amount + SalesInvoiceLine."Inv. Discount Amount" + GetLineLevelItemChargeAmount(SalesInvoiceLine))));
SpecifiedLineTradeSettlementElement.Add(SpecifiedTradeSettlementLineMonetarySummationElement);
InvoiceLineElement.Add(SpecifiedLineTradeSettlementElement);
@@ -1073,7 +1095,8 @@ codeunit 13917 "Export ZUGFeRD Document"
SupplyChainTradeTransactionElement := XmlElement.Create('SupplyChainTradeTransaction', XmlNamespaceRSM);
if SalesCrMemoLine.FindSet() then
repeat
- InsertCrMemoLine(SupplyChainTradeTransactionElement, SalesCrMemoLine, Currency, CurrencyCode, SalesCrMemoHeader."Prices Including VAT");
+ if not IsItemChargeAllowanceCharge(SalesCrMemoLine."Line No.") then
+ InsertCrMemoLine(SupplyChainTradeTransactionElement, SalesCrMemoLine, Currency, CurrencyCode, SalesCrMemoHeader."Prices Including VAT");
until SalesCrMemoLine.Next() = 0;
InsertApplicableHeaderTradeAgreement(SupplyChainTradeTransactionElement, SalesCrMemoHeader);
InsertApplicableHeaderTradeDelivery(SupplyChainTradeTransactionElement, SalesCrMemoHeader);
@@ -1119,14 +1142,13 @@ codeunit 13917 "Export ZUGFeRD Document"
SpecifiedLineTradeAgreementElement := XmlElement.Create('SpecifiedLineTradeAgreement', XmlNamespaceRAM);
NetPriceProductTradePriceElement := XmlElement.Create('NetPriceProductTradePrice', XmlNamespaceRAM);
- ChargeAmountElement := XmlElement.Create('ChargeAmount', XmlNamespaceRAM, FormatDecimalUnlimited(SalesCrMemoLine."Unit Price"));
+ ChargeAmountElement := XmlElement.Create('ChargeAmount', XmlNamespaceRAM, FormatDecimalUnlimited(GetInvoiceLineUnitPrice(SalesCrMemoLine)));
NetPriceProductTradePriceElement.Add(ChargeAmountElement);
SpecifiedLineTradeAgreementElement.Add(NetPriceProductTradePriceElement);
CrMemoLineElement.Add(SpecifiedLineTradeAgreementElement);
SpecifiedLineTradeDeliveryElement := XmlElement.Create('SpecifiedLineTradeDelivery', XmlNamespaceRAM);
- BilledQuantityElement := XmlElement.Create('BilledQuantity', XmlNamespaceRAM, FormatDecimalUnlimited(SalesCrMemoLine.Quantity));
- BilledQuantityElement.SetAttribute('unitCode', GetUoMCode(SalesCrMemoLine."Unit of Measure Code"));
+ BilledQuantityElement := CreateBilledQuantityElement(SalesCrMemoLine);
SpecifiedLineTradeDeliveryElement.Add(BilledQuantityElement);
CrMemoLineElement.Add(SpecifiedLineTradeDeliveryElement);
@@ -1145,8 +1167,10 @@ codeunit 13917 "Export ZUGFeRD Document"
if SalesCrMemoLine."Line Discount Amount" <> 0 then
InsertAllowanceCharge(SpecifiedLineTradeSettlementElement, 'Line Discount', GetTaxCategoryID(SalesCrMemoLine."Tax Category", SalesCrMemoLine."VAT Bus. Posting Group",
SalesCrMemoLine."VAT Prod. Posting Group"), SalesCrMemoLine."Line Discount Amount", SalesCrMemoLine."VAT %", false);
+ InsertLineLevelItemChargeAllowanceCharges(SpecifiedLineTradeSettlementElement, SalesCrMemoLine);
+
SpecifiedTradeSettlementLineMonetarySummationElement := XmlElement.Create('SpecifiedTradeSettlementLineMonetarySummation', XmlNamespaceRAM);
- SpecifiedTradeSettlementLineMonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount")));
+ SpecifiedTradeSettlementLineMonetarySummationElement.Add(XmlElement.Create('LineTotalAmount', XmlNamespaceRAM, FormatDecimal(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount" + GetLineLevelItemChargeAmount(SalesCrMemoLine))));
SpecifiedLineTradeSettlementElement.Add(SpecifiedTradeSettlementLineMonetarySummationElement);
CrMemoLineElement.Add(SpecifiedLineTradeSettlementElement);
@@ -1342,7 +1366,6 @@ codeunit 13917 "Export ZUGFeRD Document"
local procedure GetBuyerReference(RecordVariant: Variant): Text
var
SalesInvoiceHeader: Record "Sales Invoice Header";
- EDocDEHelper: Codeunit "E-Document DE Helper";
DataTypeManagement: Codeunit "Data Type Management";
HeaderRecordRef: RecordRef;
BuyerReferenceFieldRef: FieldRef;
@@ -1367,7 +1390,7 @@ codeunit 13917 "Export ZUGFeRD Document"
BuyerReference := BuyerReferenceFieldRef.Value();
BillToCustomerNo := CustomerNoFieldRef.Value();
YourReference := YourReferenceFieldRef.Value();
- exit(EDocDEHelper.GetBuyerReferenceValue(BuyerReference, BillToCustomerNo, YourReference));
+ exit(EDocumentDEHelper.GetBuyerReferenceValue(BuyerReference, BillToCustomerNo, YourReference));
end;
local procedure GetIBAN(IBAN: Text[50]) IBANFormatted: Text[50]
@@ -1434,6 +1457,406 @@ codeunit 13917 "Export ZUGFeRD Document"
EmailAddress := CompanyInformation."E-Mail";
end;
+ #region ItemCharge
+ local procedure ClassifyItemCharges(SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvLine: Record "Sales Invoice Line")
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ TargetSalesInvLine: Record "Sales Invoice Line";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ExportedLineExists: Boolean;
+ begin
+ ClearItemChargeClassification();
+
+ ChargeSalesInvLine.CopyFilters(SalesInvLine);
+ ChargeSalesInvLine.SetRange(Type, ChargeSalesInvLine.Type::"Charge (Item)");
+ if not ChargeSalesInvLine.FindSet() then
+ exit;
+
+ ExportedLineExists := ExportedNonChargeLineExists(SalesInvLine);
+
+ repeat
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvLine, TargetSalesInvLine);
+ // The export skips lines that the classification cannot see, such as lines without a number, without a
+ // quantity, or removed by a subscriber. Turning the item charges into allowances/charges would then leave
+ // the document without any invoice line, which BR-16 does not allow.
+ if not ExportedLineExists then
+ Structure := Structure::"Line with Unit Code";
+ // A line level allowance/charge needs an invoice line to live in. Without one it degrades to document level, so that the charge is never lost.
+ if (Structure = Structure::"Line Allowance/Charge") and not IsExportedLine(SalesInvLine, TargetSalesInvLine."Line No.") then
+ Structure := Structure::"Document Allowance/Charge";
+ ItemChargeStructures.Add(ChargeSalesInvLine."Line No.", Structure.AsInteger());
+ if Structure = Structure::"Line Allowance/Charge" then
+ AddLineLevelItemCharge(TargetSalesInvLine."Line No.", ChargeSalesInvLine."Line No.", ChargeSalesInvLine.Amount);
+ until ChargeSalesInvLine.Next() = 0;
+ end;
+
+ local procedure ClassifyItemCharges(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TargetSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ExportedLineExists: Boolean;
+ begin
+ ClearItemChargeClassification();
+
+ ChargeSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ if not ChargeSalesCrMemoLine.FindSet() then
+ exit;
+
+ ExportedLineExists := ExportedNonChargeLineExists(SalesCrMemoLine);
+
+ repeat
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesCrMemoHeader, ChargeSalesCrMemoLine, TargetSalesCrMemoLine);
+ // The export skips lines that the classification cannot see, such as lines without a number, without a
+ // quantity, or removed by a subscriber. Turning the item charges into allowances/charges would then leave
+ // the document without any credit memo line, which BR-16 does not allow.
+ if not ExportedLineExists then
+ Structure := Structure::"Line with Unit Code";
+ // A line level allowance/charge needs a credit memo line to live in. Without one it degrades to document level, so that the charge is never lost.
+ if (Structure = Structure::"Line Allowance/Charge") and not IsExportedLine(SalesCrMemoLine, TargetSalesCrMemoLine."Line No.") then
+ Structure := Structure::"Document Allowance/Charge";
+ ItemChargeStructures.Add(ChargeSalesCrMemoLine."Line No.", Structure.AsInteger());
+ if Structure = Structure::"Line Allowance/Charge" then
+ AddLineLevelItemCharge(TargetSalesCrMemoLine."Line No.", ChargeSalesCrMemoLine."Line No.", ChargeSalesCrMemoLine.Amount);
+ until ChargeSalesCrMemoLine.Next() = 0;
+ end;
+
+ local procedure ClearItemChargeClassification()
+ begin
+ Clear(ItemChargeStructures);
+ Clear(LineLevelItemChargeAmounts);
+ Clear(LineLevelItemChargeLineNos);
+ end;
+
+ local procedure AddLineLevelItemCharge(TargetLineNo: Integer; ChargeLineNo: Integer; ChargeAmount: Decimal)
+ var
+ ChargeLineNos: List of [Integer];
+ begin
+ if LineLevelItemChargeAmounts.ContainsKey(TargetLineNo) then
+ LineLevelItemChargeAmounts.Set(TargetLineNo, LineLevelItemChargeAmounts.Get(TargetLineNo) + ChargeAmount)
+ else
+ LineLevelItemChargeAmounts.Add(TargetLineNo, ChargeAmount);
+
+ if not LineLevelItemChargeLineNos.Get(TargetLineNo, ChargeLineNos) then
+ LineLevelItemChargeLineNos.Add(TargetLineNo, ChargeLineNos);
+ ChargeLineNos.Add(ChargeLineNo);
+ end;
+
+ local procedure ExportedNonChargeLineExists(var SalesInvLine: Record "Sales Invoice Line"): Boolean
+ var
+ ExportedSalesInvLine: Record "Sales Invoice Line";
+ begin
+ ExportedSalesInvLine.CopyFilters(SalesInvLine);
+ ExportedSalesInvLine.SetFilter(Type, '<>%1', ExportedSalesInvLine.Type::"Charge (Item)");
+ exit(not ExportedSalesInvLine.IsEmpty());
+ end;
+
+ local procedure ExportedNonChargeLineExists(var SalesCrMemoLine: Record "Sales Cr.Memo Line"): Boolean
+ var
+ ExportedSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ ExportedSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ExportedSalesCrMemoLine.SetFilter(Type, '<>%1', ExportedSalesCrMemoLine.Type::"Charge (Item)");
+ exit(not ExportedSalesCrMemoLine.IsEmpty());
+ end;
+
+ local procedure IsExportedLine(var SalesInvLine: Record "Sales Invoice Line"; LineNo: Integer): Boolean
+ var
+ ExportedSalesInvLine: Record "Sales Invoice Line";
+ begin
+ if LineNo = 0 then
+ exit(false);
+ ExportedSalesInvLine.CopyFilters(SalesInvLine);
+ ExportedSalesInvLine.SetRange("Line No.", LineNo);
+ exit(not ExportedSalesInvLine.IsEmpty());
+ end;
+
+ local procedure IsExportedLine(var SalesCrMemoLine: Record "Sales Cr.Memo Line"; LineNo: Integer): Boolean
+ var
+ ExportedSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ if LineNo = 0 then
+ exit(false);
+ ExportedSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ExportedSalesCrMemoLine.SetRange("Line No.", LineNo);
+ exit(not ExportedSalesCrMemoLine.IsEmpty());
+ end;
+
+ local procedure InsertItemChargeAllowanceCharges(var SettlementElement: XmlElement; var SalesInvLine: Record "Sales Invoice Line")
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ begin
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesInvLine.CopyFilters(SalesInvLine);
+ ChargeSalesInvLine.SetRange(Type, ChargeSalesInvLine.Type::"Charge (Item)");
+ if not ChargeSalesInvLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesInvLine."Line No.") then
+ InsertItemChargeAllowanceCharge(SettlementElement, ChargeSalesInvLine, true);
+ until ChargeSalesInvLine.Next() = 0;
+ end;
+
+ local procedure InsertItemChargeAllowanceCharges(var SettlementElement: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ if not ChargeSalesCrMemoLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesCrMemoLine."Line No.") then
+ InsertItemChargeAllowanceCharge(SettlementElement, ChargeSalesCrMemoLine, true);
+ until ChargeSalesCrMemoLine.Next() = 0;
+ end;
+
+ local procedure InsertLineLevelItemChargeAllowanceCharges(var LineTradeSettlementElement: XmlElement; var SalesInvLine: Record "Sales Invoice Line")
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ ChargeLineNos: List of [Integer];
+ ChargeLineNo: Integer;
+ begin
+ if not LineLevelItemChargeLineNos.Get(SalesInvLine."Line No.", ChargeLineNos) then
+ exit;
+
+ foreach ChargeLineNo in ChargeLineNos do begin
+ ChargeSalesInvLine.Get(SalesInvLine."Document No.", ChargeLineNo);
+ InsertItemChargeAllowanceCharge(LineTradeSettlementElement, ChargeSalesInvLine, false);
+ end;
+ end;
+
+ local procedure InsertLineLevelItemChargeAllowanceCharges(var LineTradeSettlementElement: XmlElement; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ChargeLineNos: List of [Integer];
+ ChargeLineNo: Integer;
+ begin
+ if not LineLevelItemChargeLineNos.Get(SalesCrMemoLine."Line No.", ChargeLineNos) then
+ exit;
+
+ foreach ChargeLineNo in ChargeLineNos do begin
+ ChargeSalesCrMemoLine.Get(SalesCrMemoLine."Document No.", ChargeLineNo);
+ InsertItemChargeAllowanceCharge(LineTradeSettlementElement, ChargeSalesCrMemoLine, false);
+ end;
+ end;
+
+ local procedure InsertItemChargeAllowanceCharge(var ParentXMLNode: XmlElement; ChargeSalesInvLine: Record "Sales Invoice Line"; InsertCategoryTax: Boolean)
+ var
+ AllowanceChargeElement: XmlElement;
+ ReasonCode: Code[10];
+ ReasonText: Text[100];
+ begin
+ GetItemChargeReason(ChargeSalesInvLine, ReasonCode, ReasonText);
+ AllowanceChargeElement := XmlElement.Create('SpecifiedTradeAllowanceCharge', XmlNamespaceRAM);
+ AllowanceChargeElement.Add(
+ XmlElement.Create('ChargeIndicator', XmlNamespaceRAM,
+ XmlElement.Create('Indicator', XmlNamespaceUDT, EDocumentDEHelper.IsCharge(ChargeSalesInvLine.Amount))));
+ AllowanceChargeElement.Add(XmlElement.Create('ActualAmount', XmlNamespaceRAM, FormatDecimal(EDocumentDEHelper.GetReportedAmount(ChargeSalesInvLine.Amount))));
+ if ReasonCode <> '' then
+ AllowanceChargeElement.Add(XmlElement.Create('ReasonCode', XmlNamespaceRAM, ReasonCode));
+ AllowanceChargeElement.Add(XmlElement.Create('Reason', XmlNamespaceRAM, ReasonText));
+ if InsertCategoryTax then
+ InsertCategoryTradeTax(
+ AllowanceChargeElement,
+ GetTaxCategoryID(ChargeSalesInvLine."Tax Category", ChargeSalesInvLine."VAT Bus. Posting Group", ChargeSalesInvLine."VAT Prod. Posting Group"),
+ FormatFiveDecimal(ChargeSalesInvLine."VAT %"));
+ ParentXMLNode.Add(AllowanceChargeElement);
+ end;
+
+ local procedure InsertItemChargeAllowanceCharge(var ParentXMLNode: XmlElement; ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line"; InsertCategoryTax: Boolean)
+ var
+ AllowanceChargeElement: XmlElement;
+ ReasonCode: Code[10];
+ ReasonText: Text[100];
+ begin
+ GetItemChargeReason(ChargeSalesCrMemoLine, ReasonCode, ReasonText);
+ AllowanceChargeElement := XmlElement.Create('SpecifiedTradeAllowanceCharge', XmlNamespaceRAM);
+ AllowanceChargeElement.Add(
+ XmlElement.Create('ChargeIndicator', XmlNamespaceRAM,
+ XmlElement.Create('Indicator', XmlNamespaceUDT, EDocumentDEHelper.IsCharge(ChargeSalesCrMemoLine.Amount))));
+ AllowanceChargeElement.Add(XmlElement.Create('ActualAmount', XmlNamespaceRAM, FormatDecimal(EDocumentDEHelper.GetReportedAmount(ChargeSalesCrMemoLine.Amount))));
+ if ReasonCode <> '' then
+ AllowanceChargeElement.Add(XmlElement.Create('ReasonCode', XmlNamespaceRAM, ReasonCode));
+ AllowanceChargeElement.Add(XmlElement.Create('Reason', XmlNamespaceRAM, ReasonText));
+ if InsertCategoryTax then
+ InsertCategoryTradeTax(
+ AllowanceChargeElement,
+ GetTaxCategoryID(ChargeSalesCrMemoLine."Tax Category", ChargeSalesCrMemoLine."VAT Bus. Posting Group", ChargeSalesCrMemoLine."VAT Prod. Posting Group"),
+ FormatFiveDecimal(ChargeSalesCrMemoLine."VAT %"));
+ ParentXMLNode.Add(AllowanceChargeElement);
+ end;
+
+ local procedure GetItemChargeReason(ChargeSalesInvLine: Record "Sales Invoice Line"; var ReasonCode: Code[10]; var ReasonText: Text[100])
+ begin
+ EDocItemChargeMapping.GetItemChargeReason(ChargeSalesInvLine."No.", ReasonCode, ReasonText);
+ if ReasonText = '' then
+ ReasonText := ChargeSalesInvLine.Description;
+ if (ReasonText = '') and (ReasonCode = '') then
+ ReasonText := ChargeSalesInvLine."No.";
+ end;
+
+ local procedure GetItemChargeReason(ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line"; var ReasonCode: Code[10]; var ReasonText: Text[100])
+ begin
+ EDocItemChargeMapping.GetItemChargeReason(ChargeSalesCrMemoLine."No.", ReasonCode, ReasonText);
+ if ReasonText = '' then
+ ReasonText := ChargeSalesCrMemoLine.Description;
+ if (ReasonText = '') and (ReasonCode = '') then
+ ReasonText := ChargeSalesCrMemoLine."No.";
+ end;
+
+ local procedure GetDocumentLevelItemChargeTotals(var SalesInvLine: Record "Sales Invoice Line"; var TotalChargeAmount: Decimal; var TotalAllowanceAmount: Decimal)
+ var
+ ChargeSalesInvLine: Record "Sales Invoice Line";
+ begin
+ TotalChargeAmount := 0;
+ TotalAllowanceAmount := 0;
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesInvLine.CopyFilters(SalesInvLine);
+ ChargeSalesInvLine.SetRange(Type, ChargeSalesInvLine.Type::"Charge (Item)");
+ if not ChargeSalesInvLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesInvLine."Line No.") then
+ if EDocumentDEHelper.IsCharge(ChargeSalesInvLine.Amount) then
+ TotalChargeAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesInvLine.Amount)
+ else
+ TotalAllowanceAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesInvLine.Amount);
+ until ChargeSalesInvLine.Next() = 0;
+ end;
+
+ local procedure GetDocumentLevelItemChargeTotals(var SalesCrMemoLine: Record "Sales Cr.Memo Line"; var TotalChargeAmount: Decimal; var TotalAllowanceAmount: Decimal)
+ var
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ TotalChargeAmount := 0;
+ TotalAllowanceAmount := 0;
+ if ItemChargeStructures.Count() = 0 then
+ exit;
+
+ ChargeSalesCrMemoLine.CopyFilters(SalesCrMemoLine);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ if not ChargeSalesCrMemoLine.FindSet() then
+ exit;
+
+ repeat
+ if IsDocumentLevelItemCharge(ChargeSalesCrMemoLine."Line No.") then
+ if EDocumentDEHelper.IsCharge(ChargeSalesCrMemoLine.Amount) then
+ TotalChargeAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesCrMemoLine.Amount)
+ else
+ TotalAllowanceAmount += EDocumentDEHelper.GetReportedAmount(ChargeSalesCrMemoLine.Amount);
+ until ChargeSalesCrMemoLine.Next() = 0;
+ end;
+
+ local procedure GetLineLevelItemChargeAmount(var SalesInvLine: Record "Sales Invoice Line") TotalChargeAmount: Decimal
+ begin
+ if not LineLevelItemChargeAmounts.Get(SalesInvLine."Line No.", TotalChargeAmount) then
+ exit(0);
+ end;
+
+ local procedure GetLineLevelItemChargeAmount(var SalesCrMemoLine: Record "Sales Cr.Memo Line") TotalChargeAmount: Decimal
+ begin
+ if not LineLevelItemChargeAmounts.Get(SalesCrMemoLine."Line No.", TotalChargeAmount) then
+ exit(0);
+ end;
+
+ local procedure CreateBilledQuantityElement(SalesInvoiceLine: Record "Sales Invoice Line") BilledQuantityElement: XmlElement
+ begin
+ if IsItemChargeInvoiceLine(SalesInvoiceLine."Line No.") then begin
+ BilledQuantityElement := XmlElement.Create('BilledQuantity', XmlNamespaceRAM, FormatDecimalUnlimited(EDocItemChargeMapping.GetFallbackQuantity(GetLineNetAmount(SalesInvoiceLine))));
+ BilledQuantityElement.SetAttribute('unitCode', EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(SalesInvoiceLine."No."));
+ exit;
+ end;
+
+ BilledQuantityElement := XmlElement.Create('BilledQuantity', XmlNamespaceRAM, FormatDecimalUnlimited(SalesInvoiceLine.Quantity));
+ BilledQuantityElement.SetAttribute('unitCode', GetUoMCode(SalesInvoiceLine."Unit of Measure Code"));
+ end;
+
+ local procedure CreateBilledQuantityElement(SalesCrMemoLine: Record "Sales Cr.Memo Line") BilledQuantityElement: XmlElement
+ begin
+ if IsItemChargeInvoiceLine(SalesCrMemoLine."Line No.") then begin
+ BilledQuantityElement := XmlElement.Create('BilledQuantity', XmlNamespaceRAM, FormatDecimalUnlimited(EDocItemChargeMapping.GetFallbackQuantity(GetLineNetAmount(SalesCrMemoLine))));
+ BilledQuantityElement.SetAttribute('unitCode', EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(SalesCrMemoLine."No."));
+ exit;
+ end;
+
+ BilledQuantityElement := XmlElement.Create('BilledQuantity', XmlNamespaceRAM, FormatDecimalUnlimited(SalesCrMemoLine.Quantity));
+ BilledQuantityElement.SetAttribute('unitCode', GetUoMCode(SalesCrMemoLine."Unit of Measure Code"));
+ end;
+
+ local procedure GetInvoiceLineUnitPrice(SalesInvoiceLine: Record "Sales Invoice Line"): Decimal
+ begin
+ if not IsItemChargeInvoiceLine(SalesInvoiceLine."Line No.") then
+ exit(SalesInvoiceLine."Unit Price");
+
+ exit(EDocItemChargeMapping.GetFallbackUnitPrice(GetLineNetAmount(SalesInvoiceLine)));
+ end;
+
+ local procedure GetInvoiceLineUnitPrice(SalesCrMemoLine: Record "Sales Cr.Memo Line"): Decimal
+ begin
+ if not IsItemChargeInvoiceLine(SalesCrMemoLine."Line No.") then
+ exit(SalesCrMemoLine."Unit Price");
+
+ exit(EDocItemChargeMapping.GetFallbackUnitPrice(GetLineNetAmount(SalesCrMemoLine)));
+ end;
+
+ local procedure GetLineNetAmount(SalesInvoiceLine: Record "Sales Invoice Line"): Decimal
+ begin
+ exit(SalesInvoiceLine.Amount + SalesInvoiceLine."Inv. Discount Amount");
+ end;
+
+ local procedure GetLineNetAmount(SalesCrMemoLine: Record "Sales Cr.Memo Line"): Decimal
+ begin
+ exit(SalesCrMemoLine.Amount + SalesCrMemoLine."Inv. Discount Amount");
+ end;
+
+ local procedure IsItemChargeAllowanceCharge(LineNo: Integer): Boolean
+ var
+ Structure: Enum "Item Charge E-Doc. Structure";
+ begin
+ if not TryGetItemChargeStructure(LineNo, Structure) then
+ exit(false);
+ exit(Structure <> Structure::"Line with Unit Code");
+ end;
+
+ local procedure IsItemChargeInvoiceLine(LineNo: Integer): Boolean
+ var
+ Structure: Enum "Item Charge E-Doc. Structure";
+ begin
+ if not TryGetItemChargeStructure(LineNo, Structure) then
+ exit(false);
+ exit(Structure = Structure::"Line with Unit Code");
+ end;
+
+ local procedure IsDocumentLevelItemCharge(LineNo: Integer): Boolean
+ var
+ Structure: Enum "Item Charge E-Doc. Structure";
+ begin
+ if not TryGetItemChargeStructure(LineNo, Structure) then
+ exit(false);
+ exit(Structure = Structure::"Document Allowance/Charge");
+ end;
+
+ local procedure TryGetItemChargeStructure(LineNo: Integer; var Structure: Enum "Item Charge E-Doc. Structure"): Boolean
+ begin
+ if not ItemChargeStructures.ContainsKey(LineNo) then
+ exit(false);
+ Structure := Enum::"Item Charge E-Doc. Structure".FromInteger(ItemChargeStructures.Get(LineNo));
+ exit(true);
+ end;
+ #endregion
+
#region CommonFunctions
local procedure GetSetups()
begin
diff --git a/src/Apps/DE/EDocumentDE/demo data/app.json b/src/Apps/DE/EDocumentDE/demo data/app.json
index f7d666c0d83..2cb31c02015 100644
--- a/src/Apps/DE/EDocumentDE/demo data/app.json
+++ b/src/Apps/DE/EDocumentDE/demo data/app.json
@@ -1,4 +1,4 @@
-{
+{
"id": "f3e4e6f8-2ba7-4202-834d-141ed9b89194",
"name": "E-Document Core Demo Data (DE)",
"publisher": "Microsoft",
@@ -54,8 +54,5 @@
"includeSourceInSymbolFile": true
},
"application": "29.0.0.0",
- "target": "OnPrem",
- "features": [
- "TranslationFile"
- ]
+ "target": "OnPrem"
}
\ No newline at end of file
diff --git a/src/Apps/DE/EDocumentDE/test/src/ItemChargeUIDETests.Codeunit.al b/src/Apps/DE/EDocumentDE/test/src/ItemChargeUIDETests.Codeunit.al
new file mode 100644
index 00000000000..22062a06324
--- /dev/null
+++ b/src/Apps/DE/EDocumentDE/test/src/ItemChargeUIDETests.Codeunit.al
@@ -0,0 +1,162 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Formats;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Integration;
+using Microsoft.Inventory.Item;
+
+codeunit 148502 "Item Charge UI DE Tests"
+{
+ Subtype = Test;
+ TestType = Uncategorized;
+
+ trigger OnRun();
+ begin
+ // [FEATURE] [E-Document] [Item Charge] [UI]
+ end;
+
+ var
+ LibraryInventory: Codeunit "Library - Inventory";
+ LibraryEdocument: Codeunit "Library - E-Document";
+ Assert: Codeunit Assert;
+ IncorrectValueErr: Label 'Incorrect value for %1', Locked = true;
+
+ #region EDocumentService
+ [Test]
+ procedure ItemChargeMappingIsVisibleAndEditableOnServicePageForXRechnung()
+ var
+ EDocumentService: Record "E-Document Service";
+ EDocumentServicePage: TestPage "E-Document Service";
+ begin
+ // [SCENARIO] The Item Charge E-Invoice Mapping setting can be changed on the E-Document Service page for an XRechnung service
+ // [GIVEN] An E-Document service with XRechnung format
+ EDocumentService.Get(LibraryEdocument.CreateService("E-Document Format"::XRechnung, "Service Integration"::"No Integration"));
+
+ // [WHEN] The E-Document Service page is opened for the service
+ EDocumentServicePage.Trap();
+ Page.Run(Page::"E-Document Service", EDocumentService);
+
+ // [THEN] The Item Charge E-Invoice Mapping field is visible and editable
+ Assert.IsTrue(EDocumentServicePage."Item Charge E-Invoice Mapping".Visible(), 'Item Charge E-Invoice Mapping must be visible for XRechnung');
+ Assert.IsTrue(EDocumentServicePage."Item Charge E-Invoice Mapping".Editable(), 'Item Charge E-Invoice Mapping must be editable for XRechnung');
+
+ // [WHEN] A mapping is selected on the page
+ EDocumentServicePage."Item Charge E-Invoice Mapping".SetValue(Format(Enum::"Item Charge E-Invoice Mapping"::"Line with Unit Code"));
+ EDocumentServicePage.Close();
+
+ // [THEN] The selected mapping is stored on the service record
+ EDocumentService.Find();
+ Assert.AreEqual(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code", EDocumentService."Item Charge E-Invoice Mapping", StrSubstNo(IncorrectValueErr, EDocumentService.FieldCaption("Item Charge E-Invoice Mapping")));
+ end;
+
+ [Test]
+ procedure ItemChargeMappingIsVisibleOnServicePageForZUGFeRD()
+ var
+ EDocumentService: Record "E-Document Service";
+ EDocumentServicePage: TestPage "E-Document Service";
+ begin
+ // [SCENARIO] The Item Charge E-Invoice Mapping setting is offered for a ZUGFeRD service
+ // [GIVEN] An E-Document service with ZUGFeRD format
+ EDocumentService.Get(LibraryEdocument.CreateService("E-Document Format"::ZUGFeRD, "Service Integration"::"No Integration"));
+
+ // [WHEN] The E-Document Service page is opened for the service
+ EDocumentServicePage.Trap();
+ Page.Run(Page::"E-Document Service", EDocumentService);
+
+ // [THEN] The Item Charge E-Invoice Mapping field is visible
+ Assert.IsTrue(EDocumentServicePage."Item Charge E-Invoice Mapping".Visible(), 'Item Charge E-Invoice Mapping must be visible for ZUGFeRD');
+ EDocumentServicePage.Close();
+ end;
+
+ [Test]
+ procedure ItemChargeMappingIsVisibleOnServicePageForPeppolBisDE()
+ var
+ EDocumentService: Record "E-Document Service";
+ EDocumentServicePage: TestPage "E-Document Service";
+ begin
+ // [SCENARIO] The Item Charge E-Invoice Mapping setting is offered for every German e-document format, including PEPPOL BIS 3.0 DE
+ // [GIVEN] An E-Document service with PEPPOL BIS 3.0 DE format
+ EDocumentService.Get(LibraryEdocument.CreateService("E-Document Format"::"PEPPOL BIS 3.0 DE", "Service Integration"::"No Integration"));
+
+ // [WHEN] The E-Document Service page is opened for the service
+ EDocumentServicePage.Trap();
+ Page.Run(Page::"E-Document Service", EDocumentService);
+
+ // [THEN] The Item Charge E-Invoice Mapping field is visible
+ Assert.IsTrue(EDocumentServicePage."Item Charge E-Invoice Mapping".Visible(), 'Item Charge E-Invoice Mapping must be visible for PEPPOL BIS 3.0 DE');
+ EDocumentServicePage.Close();
+ end;
+ #endregion
+
+ #region ItemCharges
+ [Test]
+ procedure ItemChargeOverrideFieldsAreEditableOnItemChargesPage()
+ var
+ ItemCharge: Record "Item Charge";
+ ItemChargesPage: TestPage "Item Charges";
+ begin
+ // [SCENARIO] The per-item-charge e-invoice override fields can be changed on the Item Charges page
+ // [GIVEN] An item charge
+ LibraryInventory.CreateItemCharge(ItemCharge);
+
+ // [WHEN] The Item Charges page is opened for the item charge
+ ItemChargesPage.OpenEdit();
+ ItemChargesPage.GoToRecord(ItemCharge);
+
+ // [THEN] The four override fields are visible and editable
+ Assert.IsTrue(ItemChargesPage."E-Invoice Mapping".Visible(), 'E-Invoice Mapping must be visible');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Mapping".Editable(), 'E-Invoice Mapping must be editable');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Reason Text".Visible(), 'E-Invoice Reason Text must be visible');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Reason Text".Editable(), 'E-Invoice Reason Text must be editable');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Reason Code".Visible(), 'E-Invoice Reason Code must be visible');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Reason Code".Editable(), 'E-Invoice Reason Code must be editable');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Unit Code".Visible(), 'E-Invoice Unit Code must be visible');
+ Assert.IsTrue(ItemChargesPage."E-Invoice Unit Code".Editable(), 'E-Invoice Unit Code must be editable');
+
+ // [WHEN] Values are entered in the override fields
+ ItemChargesPage."E-Invoice Mapping".SetValue(Format(Enum::"Item Charge Mapping Override"::"Document Allowance/Charge"));
+ ItemChargesPage."E-Invoice Reason Text".SetValue('Freight surcharge');
+ ItemChargesPage."E-Invoice Reason Code".SetValue('FC');
+ ItemChargesPage."E-Invoice Unit Code".SetValue('HUR');
+ ItemChargesPage.Close();
+
+ // [THEN] The values are stored on the item charge record
+ ItemCharge.Find();
+ Assert.AreEqual(ItemCharge."E-Invoice Mapping"::"Document Allowance/Charge", ItemCharge."E-Invoice Mapping", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Mapping")));
+ Assert.AreEqual('Freight surcharge', ItemCharge."E-Invoice Reason Text", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Reason Text")));
+ Assert.AreEqual('FC', ItemCharge."E-Invoice Reason Code", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Reason Code")));
+ Assert.AreEqual('HUR', ItemCharge."E-Invoice Unit Code", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Unit Code")));
+ end;
+
+ [Test]
+ procedure ItemChargeBlankMappingIsDistinctFromAutomaticOnItemChargesPage()
+ var
+ ItemCharge: Record "Item Charge";
+ ItemChargesPage: TestPage "Item Charges";
+ begin
+ // [SCENARIO] A blank mapping override (use the service setting) and the Automatic override are two distinct states in the UI
+ // [GIVEN] An item charge without a mapping override
+ LibraryInventory.CreateItemCharge(ItemCharge);
+
+ // [WHEN] The Item Charges page is opened for the item charge
+ ItemChargesPage.OpenEdit();
+ ItemChargesPage.GoToRecord(ItemCharge);
+
+ // [THEN] The mapping override is shown as blank, not as Automatic
+ Assert.AreNotEqual(Format(Enum::"Item Charge Mapping Override"::Automatic), ItemChargesPage."E-Invoice Mapping".Value(), 'A blank override must not be rendered as Automatic');
+ Assert.AreEqual('', DelChr(ItemChargesPage."E-Invoice Mapping".Value(), '<>', ' '), 'An item charge without override must show a blank mapping');
+
+ // [WHEN] Automatic is selected on the page
+ ItemChargesPage."E-Invoice Mapping".SetValue(Format(Enum::"Item Charge Mapping Override"::Automatic));
+ ItemChargesPage.Close();
+
+ // [THEN] The Automatic override is stored, distinct from the blank value
+ ItemCharge.Find();
+ Assert.AreEqual(ItemCharge."E-Invoice Mapping"::Automatic, ItemCharge."E-Invoice Mapping", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Mapping")));
+ Assert.AreNotEqual(ItemCharge."E-Invoice Mapping"::" ", ItemCharge."E-Invoice Mapping", 'Automatic must be stored as an override, not as the blank value');
+ end;
+ #endregion
+}
diff --git a/src/Apps/DE/EDocumentDE/test/src/XRechnungXMLDocumentTests.Codeunit.al b/src/Apps/DE/EDocumentDE/test/src/XRechnungXMLDocumentTests.Codeunit.al
index b998cefe55e..cd5a65c11de 100644
--- a/src/Apps/DE/EDocumentDE/test/src/XRechnungXMLDocumentTests.Codeunit.al
+++ b/src/Apps/DE/EDocumentDE/test/src/XRechnungXMLDocumentTests.Codeunit.al
@@ -18,6 +18,7 @@ using Microsoft.Foundation.PaymentTerms;
using Microsoft.Foundation.UOM;
using Microsoft.Inventory.Item;
using Microsoft.Inventory.Location;
+using Microsoft.Inventory.Setup;
using Microsoft.Purchases.Document;
using Microsoft.Purchases.Vendor;
using Microsoft.Sales.Customer;
@@ -60,6 +61,21 @@ codeunit 13918 "XRechnung XML Document Tests"
IncorrectValueErr: Label 'Incorrect value for %1', Locked = true;
AttributeNotFoundErr: Label 'Attribute %1 not found for node: %2', Locked = true, Comment = '%1 = XML attribute name, %2 = XML element XPath';
UnexpectedNodeErr: Label 'Node %1 must not exist.', Locked = true;
+ DocumentAllowanceChargeTok: Label '/ubl:Invoice/cac:AllowanceCharge', Locked = true;
+ InvoiceLineTok: Label '/ubl:Invoice/cac:InvoiceLine', Locked = true;
+ InvoiceLineAllowanceChargeTok: Label '/ubl:Invoice/cac:InvoiceLine/cac:AllowanceCharge', Locked = true;
+ LegalMonetaryTotalTok: Label '/ubl:Invoice/cac:LegalMonetaryTotal', Locked = true;
+ TaxTotalPathTok: Label '/ubl:Invoice/cac:TaxTotal', Locked = true;
+ CrMemoDocumentAllowanceChargeTok: Label '/ns0:CreditNote/cac:AllowanceCharge', Locked = true;
+ CrMemoLineTok: Label '/ns0:CreditNote/cac:CreditNoteLine', Locked = true;
+ CrMemoLineAllowanceChargeTok: Label '/ns0:CreditNote/cac:CreditNoteLine/cac:AllowanceCharge', Locked = true;
+ CrMemoLegalMonetaryTotalTok: Label '/ns0:CreditNote/cac:LegalMonetaryTotal', Locked = true;
+ CrMemoTaxTotalPathTok: Label '/ns0:CreditNote/cac:TaxTotal', Locked = true;
+ TaxCategoryStandardTok: Label 'S', Locked = true;
+ ItemChargeReasonTextTok: Label 'Freight surcharge', Locked = true;
+ ItemChargeReasonCodeTok: Label 'FC', Locked = true;
+ UnitCodeOneTok: Label 'C62', Locked = true;
+ UnitCodeHourTok: Label 'HUR', Locked = true;
SupplierTaxSchemeTok: Label '/ubl:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme', Locked = true;
SupplierPartyIdTok: Label '/ubl:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyIdentification/cbc:ID', Locked = true;
SupplierLegalEntityIdTok: Label '/ubl:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID', Locked = true;
@@ -1923,6 +1939,1369 @@ codeunit 13918 "XRechnung XML Document Tests"
end;
#endregion
+ #region ItemCharge
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyDocumentLevelItemChargeAllowanceCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge classified as a document level allowance/charge is exported as cac:AllowanceCharge under the invoice instead of as an invoice line
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and one item charge assigned to both of them
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] A document level charge is exported with the amount and the VAT category of the item charge
+ Path := DocumentAllowanceChargeTok + '/cbc:ChargeIndicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/cac:TaxCategory/cbc:ID';
+ Assert.AreEqual(TaxCategoryStandardTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/cac:TaxCategory/cbc:Percent';
+ Assert.AreEqual(ExportXRechnungDocument.FormatFiveDecimal(ChargeSalesInvoiceLine."VAT %"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The item charge is no longer exported as an invoice line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'Only the item lines must be exported as invoice lines.');
+ Assert.IsFalse(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/cac:Item/cac:SellersItemIdentification/cbc:ID', ItemChargeNo), 'The item charge must not be exported as an invoice line.');
+
+ // [THEN] The charge is not repeated as a line level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'A document level charge must not be exported inside an invoice line.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyDocumentLevelItemChargeReason()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] The reason text and reason code of the item charge are exported on the document level allowance/charge
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with an item charge that is a document level charge
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+
+ // [GIVEN] The item charge carries a reason text and a reason code
+ SetItemChargeReason(ItemChargeNo, ItemChargeReasonTextTok, ItemChargeReasonCodeTok);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The reason code and the reason text of the item charge are exported
+ Path := DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReasonCode';
+ Assert.AreEqual(ItemChargeReasonCodeTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason';
+ Assert.AreEqual(ItemChargeReasonTextTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyDocumentLevelItemChargeReasonFallsBackToDescription()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Without a reason text on the item charge the description of the item charge line is exported, so that the mandatory allowance/charge reason is never empty
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with an item charge that is a document level charge and has no reason text
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The description of the item charge line is exported as the reason
+ Path := DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason';
+ Assert.AreEqual(ChargeSalesInvoiceLine.Description, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] No empty reason code is exported
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReasonCode'), 'An item charge without a reason code must not export an empty reason code.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyDocumentLevelItemChargeReasonFallsBackToItemChargeNo()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Without a reason text, a reason code and a line description the item charge code is exported as the reason, so that the allowance/charge always carries one of the two reason elements EN 16931 requires
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with a document level item charge that has neither a reason text, nor a reason code, nor a line description
+ SalesInvoiceHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::Invoice, 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ Assert.AreEqual('', ChargeSalesInvoiceLine.Description, 'The scenario requires an item charge line without a description.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The code of the item charge is exported as the reason
+ Path := DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason';
+ Assert.AreEqual(ItemChargeNo, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyDocumentLevelItemChargeWithReasonCodeOnlyKeepsTheReasonCode()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A reason code alone already satisfies the reason requirement of EN 16931, so the item charge code is not substituted as the reason text
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with a document level item charge without a line description
+ SalesInvoiceHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::Invoice, 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+
+ // [GIVEN] The item charge carries a reason code but no reason text
+ SetItemChargeReason(ItemChargeNo, '', ItemChargeReasonCodeTok);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The reason code of the item charge is exported
+ Path := DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReasonCode';
+ Assert.AreEqual(ItemChargeReasonCodeTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The code of the item charge is not exported as the reason
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason'), 'An item charge with a reason code must not fall back to the item charge code as the reason.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyLineLevelItemChargeAllowanceCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge classified as a line level allowance/charge is exported inside the invoice line it is assigned to
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge with the same VAT assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the invoice line of the assigned line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate invoice line.');
+ Path := InvoiceLineTok + '/cbc:ID';
+ Assert.AreEqual(Format(ItemSalesInvoiceLine."Line No."), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := InvoiceLineAllowanceChargeTok + '/cbc:ChargeIndicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := InvoiceLineAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The line level allowance/charge carries no VAT category, because the VAT category of the invoice line applies
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok + '/cac:TaxCategory/cbc:ID'), 'A line level allowance/charge must not carry its own VAT category.');
+
+ // [THEN] The charge is not repeated as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'A line level charge must not be exported as a document level allowance/charge.');
+
+ // [THEN] The net amount of the invoice line includes the charge
+ Path := InvoiceLineTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ItemSalesInvoiceLine.Amount + ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyLineLevelItemChargeOnlyAffectsTheAssignedLine()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ AssignedLineAmount: Decimal;
+ UnassignedLineAmount: Decimal;
+ Path: Text;
+ begin
+ // [SCENARIO] A line level allowance/charge is exported only in the invoice line it is assigned to, and leaves the other invoice lines untouched
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and an item charge assigned to the first line only
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+ AssignedLineAmount := ItemSalesInvoiceLine.Amount;
+ ItemSalesInvoiceLine.Next();
+ UnassignedLineAmount := ItemSalesInvoiceLine.Amount;
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] Exactly one invoice line carries the allowance/charge
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate invoice line.');
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'The charge must be exported in the assigned invoice line only.');
+
+ // [THEN] Only the assigned invoice line reports the charge in its net amount
+ Path := InvoiceLineTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(AssignedLineAmount + ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(UnassignedLineAmount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyItemChargeInvoiceLineUsesFallbackQuantityAndUnitCode()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge exported as a regular invoice line carries quantity 1 and the unit code C62, never an empty unit code
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge of quantity 2 assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+ Assert.AreEqual(2, ChargeSalesInvoiceLine.Quantity, 'The scenario requires an item charge quantity that differs from the fallback quantity.');
+ Assert.AreEqual('', ChargeSalesInvoiceLine."Unit of Measure Code", 'The scenario requires an item charge line without a unit of measure.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as an invoice line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line.');
+ Path := InvoiceLineTok + '/cbc:ID';
+ Assert.AreEqual(Format(ChargeSalesInvoiceLine."Line No."), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The invoice line of the item charge carries quantity 1 and the unit code C62
+ Path := InvoiceLineTok + '/cbc:InvoicedQuantity';
+ Assert.AreEqual('1', GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The unit price of the invoice line matches the net amount, so that quantity times price stays the net amount of the line
+ Path := InvoiceLineTok + '/cac:Price/cbc:PriceAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimalUnlimited(ChargeSalesInvoiceLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The item line keeps its own quantity and unit code
+ Path := InvoiceLineTok + '/cbc:InvoicedQuantity';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimalUnlimited(ItemSalesInvoiceLine.Quantity), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(ExportXRechnungDocument.GetUoMCode(ItemSalesInvoiceLine."Unit of Measure Code"), GetAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyItemChargeInvoiceLineUsesUnitCodeOfItemCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A unit code configured on the item charge replaces C62 on the invoice line of the item charge
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales invoice with an item charge that carries the unit code HUR
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ SetItemChargeUnitCode(ItemChargeNo, UnitCodeHourTok);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The invoice line of the item charge carries the unit code of the item charge
+ Path := InvoiceLineTok + '/cbc:InvoicedQuantity';
+ Assert.AreEqual(UnitCodeHourTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyNegativeItemChargeInvoiceLineUsesNegativeQuantity()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge exported as a regular invoice line reports a negative quantity and a positive unit price, so that the exported document satisfies BR-27
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales invoice with one item line and a negative item charge assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ Assert.IsTrue(ChargeSalesInvoiceLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as an invoice line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line.');
+ Path := InvoiceLineTok + '/cbc:ID';
+ Assert.AreEqual(Format(ChargeSalesInvoiceLine."Line No."), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The invoice line of the item charge reports the negative fallback quantity with the fallback unit code
+ Path := InvoiceLineTok + '/cbc:InvoicedQuantity';
+ Assert.AreEqual('-1', GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The unit price of the invoice line is not negative, because the item net price must never be negative
+ Path := InvoiceLineTok + '/cac:Price/cbc:PriceAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimalUnlimited(-ChargeSalesInvoiceLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The net amount of the invoice line stays negative
+ Path := InvoiceLineTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The quantity of the invoice line times its unit price stays the net amount of the line
+ VerifyLastLineAmountMatchesQuantityTimesPrice(
+ TempXMLBuffer, InvoiceLineTok + '/cbc:InvoicedQuantity', InvoiceLineTok + '/cac:Price/cbc:PriceAmount', InvoiceLineTok + '/cbc:LineExtensionAmount');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyNegativeItemChargeIsExportedAsAllowance()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge is exported as an allowance with a positive amount
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and a negative item charge assigned to both of them
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ Assert.IsTrue(ChargeSalesInvoiceLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported as an allowance with a positive amount
+ Path := DocumentAllowanceChargeTok + '/cbc:ChargeIndicator';
+ Assert.AreEqual('false', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(-ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The allowance is reported in the allowance total and not in the charge total
+ SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT");
+ Path := LegalMonetaryTotalTok + '/cbc:AllowanceTotalAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(-ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, LegalMonetaryTotalTok + '/cbc:ChargeTotalAmount'), 'A negative item charge must not be reported as a charge total.');
+
+ // [THEN] The totals stay consistent
+ Path := LegalMonetaryTotalTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount - ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := LegalMonetaryTotalTok + '/cbc:TaxExclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyForcedLineLevelItemChargeWithoutTargetLineIsDocumentLevel()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A forced line level allowance/charge that cannot be resolved to a single invoice line degrades to a document level allowance/charge
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line Allowance/Charge");
+
+ // [GIVEN] A posted sales invoice with an item charge assigned to two item lines, so that no single target line can be resolved
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported at document level instead of inside an invoice line
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'An unresolved line level charge must not be exported inside an invoice line.');
+ Path := DocumentAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The charge is not exported as an invoice line either
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'Only the item lines must be exported as invoice lines.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyChargeOnlyInvoiceKeepsInvoiceLine()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ begin
+ // [SCENARIO] A posted sales invoice whose only line is an item charge keeps that line as an invoice line even when the service forces a document level allowance/charge, so that the exported document satisfies BR-16
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document level allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [GIVEN] A posted sales invoice that only contains an item charge assigned to an earlier shipment
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithShipmentChargeOnly(ItemChargeNo));
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported as the only invoice line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line, so that the document keeps at least one invoice line.');
+ Assert.IsTrue(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/cac:Item/cac:SellersItemIdentification/cbc:ID', ItemChargeNo), 'The exported invoice line must be the item charge.');
+
+ // [THEN] The charge is not exported as an allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'The item charge must not be exported as a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyTotalsWithDocumentLevelItemCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Moving an item charge out of the invoice lines keeps the document totals and the tax subtotals consistent
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and one item charge assigned to both of them
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The sum of the invoice lines no longer contains the charge and the charge is reported as the charge total
+ Path := LegalMonetaryTotalTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount - ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := LegalMonetaryTotalTok + '/cbc:ChargeTotalAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, LegalMonetaryTotalTok + '/cbc:AllowanceTotalAmount'), 'A positive item charge must not be reported as an allowance total.');
+
+ // [THEN] The exported invoice lines add up to the reported line extension amount
+ Assert.AreEqual(
+ SalesInvoiceHeader.Amount - ChargeSalesInvoiceLine.Amount, SumNodeValuesByPath(TempXMLBuffer, InvoiceLineTok + '/cbc:LineExtensionAmount'),
+ 'The exported invoice lines must add up to the reported line extension amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := LegalMonetaryTotalTok + '/cbc:TaxExclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := LegalMonetaryTotalTok + '/cbc:TaxInclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := LegalMonetaryTotalTok + '/cbc:PayableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := TaxTotalPathTok + '/cbc:TaxAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT" - SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := TaxTotalPathTok + '/cac:TaxSubtotal/cbc:TaxableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyTotalsWithLineLevelItemCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A line level allowance/charge stays inside the sum of the invoice lines and leaves the document totals untouched
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge with the same VAT assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the invoice line it is assigned to
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate invoice line.');
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'The item charge must be exported as a line level allowance/charge.');
+
+ // [THEN] The line extension amount still contains the charge and no charge total is reported
+ Path := LegalMonetaryTotalTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, LegalMonetaryTotalTok + '/cbc:ChargeTotalAmount'), 'A line level charge must not be reported as a charge total.');
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, LegalMonetaryTotalTok + '/cbc:AllowanceTotalAmount'), 'A line level charge must not be reported as an allowance total.');
+
+ // [THEN] The exported invoice lines add up to the reported line extension amount
+ Assert.AreEqual(
+ SalesInvoiceHeader.Amount, SumNodeValuesByPath(TempXMLBuffer, InvoiceLineTok + '/cbc:LineExtensionAmount'),
+ 'The exported invoice lines must add up to the reported line extension amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := LegalMonetaryTotalTok + '/cbc:TaxExclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := LegalMonetaryTotalTok + '/cbc:PayableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := TaxTotalPathTok + '/cac:TaxSubtotal/cbc:TaxableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyDocumentLevelItemChargeAllowanceCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo classified as a document level allowance/charge is exported as cac:AllowanceCharge under the credit note instead of as a credit note line
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with two item lines and one item charge assigned to both of them
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+
+ // [THEN] The item charge line of the credit memo carries a positive amount, so that a charge on a credit note keeps the charge indicator of an invoice
+ Assert.IsTrue(ChargeSalesCrMemoLine.Amount > 0, 'The scenario requires a positive item charge amount on the credit memo.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] A document level charge is exported with the amount and the VAT category of the item charge
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:ChargeIndicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoDocumentAllowanceChargeTok + '/cac:TaxCategory/cbc:ID';
+ Assert.AreEqual(TaxCategoryStandardTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoDocumentAllowanceChargeTok + '/cac:TaxCategory/cbc:Percent';
+ Assert.AreEqual(ExportXRechnungDocument.FormatFiveDecimal(ChargeSalesCrMemoLine."VAT %"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The reason text and the reason code of the item charge line are exported
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason';
+ Assert.AreEqual(ChargeSalesCrMemoLine.Description, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The item charge is no longer exported as a credit note line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, CrMemoLineTok), 'Only the item lines must be exported as credit note lines.');
+ Assert.IsFalse(NodeValueExists(TempXMLBuffer, CrMemoLineTok + '/cac:Item/cac:SellersItemIdentification/cbc:ID', ItemChargeNo), 'The item charge must not be exported as a credit note line.');
+
+ // [THEN] The charge is not repeated as a line level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLineAllowanceChargeTok), 'A document level charge must not be exported inside a credit note line.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyDocumentLevelItemChargeReasonFallsBackToItemChargeNo()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Without a reason text, a reason code and a line description the item charge code is exported as the reason, so that the allowance/charge always carries one of the two reason elements EN 16931 requires
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with a document level item charge that has neither a reason text, nor a reason code, nor a line description
+ SalesCrMemoHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::"Credit Memo", 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.AreEqual('', ChargeSalesCrMemoLine.Description, 'The scenario requires an item charge line without a description.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The code of the item charge is exported as the reason
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason';
+ Assert.AreEqual(ItemChargeNo, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyDocumentLevelItemChargeWithReasonCodeOnlyKeepsTheReasonCode()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A reason code alone already satisfies the reason requirement of EN 16931, so the item charge code is not substituted as the reason text
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with a document level item charge without a line description
+ SalesCrMemoHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::"Credit Memo", 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+
+ // [GIVEN] The item charge carries a reason code but no reason text
+ SetItemChargeReason(ItemChargeNo, '', ItemChargeReasonCodeTok);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The reason code of the item charge is exported
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:AllowanceChargeReasonCode';
+ Assert.AreEqual(ItemChargeReasonCodeTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The code of the item charge is not exported as the reason
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoDocumentAllowanceChargeTok + '/cbc:AllowanceChargeReason'), 'An item charge with a reason code must not fall back to the item charge code as the reason.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyLineLevelItemChargeAllowanceCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo classified as a line level allowance/charge is exported inside the credit note line it is assigned to
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge with the same VAT assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ GetItemCrMemoLine(SalesCrMemoHeader, ItemSalesCrMemoLine);
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the credit note line of the assigned line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, CrMemoLineTok), 'The item charge must not be exported as a separate credit note line.');
+ Path := CrMemoLineTok + '/cbc:ID';
+ Assert.AreEqual(Format(ItemSalesCrMemoLine."Line No."), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLineAllowanceChargeTok + '/cbc:ChargeIndicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLineAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The line level allowance/charge carries no VAT category, because the VAT category of the credit note line applies
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLineAllowanceChargeTok + '/cac:TaxCategory/cbc:ID'), 'A line level allowance/charge must not carry its own VAT category.');
+
+ // [THEN] The charge is not repeated as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoDocumentAllowanceChargeTok), 'A line level charge must not be exported as a document level allowance/charge.');
+
+ // [THEN] The net amount of the credit note line includes the charge
+ Path := CrMemoLineTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ItemSalesCrMemoLine.Amount + ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyItemChargeCrMemoLineUsesFallbackQuantityAndUnitCode()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo exported as a regular credit note line carries quantity 1 and the unit code C62, never an empty unit code
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge of quantity 2 assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.AreEqual(2, ChargeSalesCrMemoLine.Quantity, 'The scenario requires an item charge quantity that differs from the fallback quantity.');
+ Assert.AreEqual('', ChargeSalesCrMemoLine."Unit of Measure Code", 'The scenario requires an item charge line without a unit of measure.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as a credit note line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, CrMemoLineTok), 'The item charge must be exported as a credit note line.');
+ Path := CrMemoLineTok + '/cbc:ID';
+ Assert.AreEqual(Format(ChargeSalesCrMemoLine."Line No."), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The credit note line of the item charge carries quantity 1 and the unit code C62
+ Path := CrMemoLineTok + '/cbc:CreditedQuantity';
+ Assert.AreEqual('1', GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The unit price of the credit note line matches the net amount, so that quantity times price stays the net amount of the line
+ Path := CrMemoLineTok + '/cac:Price/cbc:PriceAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimalUnlimited(ChargeSalesCrMemoLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] No allowance/charge is exported for the item charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoDocumentAllowanceChargeTok), 'An item charge exported as a credit note line must not be exported as an allowance/charge.');
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLineAllowanceChargeTok), 'An item charge exported as a credit note line must not be exported as an allowance/charge.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyNegativeItemChargeCrMemoLineUsesNegativeQuantity()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge of a posted sales credit memo exported as a regular credit note line reports a negative quantity and a positive unit price, so that the exported document satisfies BR-27
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales credit memo with one item line and a negative item charge assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.IsTrue(ChargeSalesCrMemoLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The credit note line of the item charge reports the negative fallback quantity with the fallback unit code
+ Path := CrMemoLineTok + '/cbc:CreditedQuantity';
+ Assert.AreEqual('-1', GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The unit price of the credit note line is not negative, because the item net price must never be negative
+ Path := CrMemoLineTok + '/cac:Price/cbc:PriceAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimalUnlimited(-ChargeSalesCrMemoLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The quantity of the credit note line times its unit price stays the net amount of the line
+ VerifyLastLineAmountMatchesQuantityTimesPrice(
+ TempXMLBuffer, CrMemoLineTok + '/cbc:CreditedQuantity', CrMemoLineTok + '/cac:Price/cbc:PriceAmount', CrMemoLineTok + '/cbc:LineExtensionAmount');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyNegativeItemChargeIsExportedAsAllowance()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge of a posted sales credit memo is exported as an allowance with a positive amount
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with two item lines and a negative item charge assigned to both of them
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(2, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.IsTrue(ChargeSalesCrMemoLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported as an allowance with a positive amount
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:ChargeIndicator';
+ Assert.AreEqual('false', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoDocumentAllowanceChargeTok + '/cbc:Amount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(-ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The allowance is reported in the allowance total and not in the charge total
+ SalesCrMemoHeader.CalcFields(Amount, "Amount Including VAT");
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:AllowanceTotalAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(-ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLegalMonetaryTotalTok + '/cbc:ChargeTotalAmount'), 'A negative item charge must not be reported as a charge total.');
+
+ // [THEN] The totals stay consistent
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount - ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:TaxExclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyTotalsWithDocumentLevelItemCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Moving an item charge out of the credit note lines keeps the document totals and the tax subtotals consistent
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with two item lines and one item charge assigned to both of them
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ SalesCrMemoHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The sum of the credit note lines no longer contains the charge and the charge is reported as the charge total
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount - ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:ChargeTotalAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLegalMonetaryTotalTok + '/cbc:AllowanceTotalAmount'), 'A positive item charge must not be reported as an allowance total.');
+
+ // [THEN] The exported credit note lines add up to the reported line extension amount
+ Assert.AreEqual(
+ SalesCrMemoHeader.Amount - ChargeSalesCrMemoLine.Amount, SumNodeValuesByPath(TempXMLBuffer, CrMemoLineTok + '/cbc:LineExtensionAmount'),
+ 'The exported credit note lines must add up to the reported line extension amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:TaxExclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:TaxInclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:PayableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := CrMemoTaxTotalPathTok + '/cbc:TaxAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT" - SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoTaxTotalPathTok + '/cac:TaxSubtotal/cbc:TaxableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyTotalsWithLineLevelItemCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A line level allowance/charge on a posted sales credit memo stays inside the sum of the credit note lines and leaves the document totals untouched
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge with the same VAT assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ SalesCrMemoHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the credit note line it is assigned to
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, CrMemoLineTok), 'The item charge must not be exported as a separate credit note line.');
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, CrMemoLineAllowanceChargeTok), 'The item charge must be exported as a line level allowance/charge.');
+
+ // [THEN] The line extension amount still contains the charge and no charge total is reported
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:LineExtensionAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLegalMonetaryTotalTok + '/cbc:ChargeTotalAmount'), 'A line level charge must not be reported as a charge total.');
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoLegalMonetaryTotalTok + '/cbc:AllowanceTotalAmount'), 'A line level charge must not be reported as an allowance total.');
+
+ // [THEN] The exported credit note lines add up to the reported line extension amount
+ Assert.AreEqual(
+ SalesCrMemoHeader.Amount, SumNodeValuesByPath(TempXMLBuffer, CrMemoLineTok + '/cbc:LineExtensionAmount'),
+ 'The exported credit note lines must add up to the reported line extension amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:TaxExclusiveAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := CrMemoLegalMonetaryTotalTok + '/cbc:PayableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := CrMemoTaxTotalPathTok + '/cac:TaxSubtotal/cbc:TaxableAmount';
+ Assert.AreEqual(ExportXRechnungDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInXRechnungFormatVerifyChargeKeepsInvoiceLineWhenTheOnlyItemLineIsNotExported()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A posted sales invoice whose only item line is skipped by the export keeps the item charge as an invoice line even when the service forces a document level allowance/charge, so that the exported document satisfies BR-16
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document level allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [GIVEN] A posted sales invoice with an item charge assigned to an earlier shipment and one item line without a quantity, which the export skips
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithChargeAndZeroQuantityLine(ItemChargeNo));
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+ Assert.AreEqual(0, ItemSalesInvoiceLine.Quantity, 'The scenario requires an item line without a quantity.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as the only invoice line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line, so that the document keeps at least one invoice line.');
+ Assert.IsTrue(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/cac:Item/cac:SellersItemIdentification/cbc:ID', ItemChargeNo), 'The exported invoice line must be the item charge.');
+
+ // [THEN] The invoice line of the item charge carries the fallback quantity and the unit code C62
+ Path := InvoiceLineTok + '/cbc:InvoicedQuantity';
+ Assert.AreEqual('1', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The charge is not exported as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'The item charge must not be exported as a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInXRechnungFormatVerifyChargeKeepsCrMemoLineWhenTheOnlyItemLineIsNotExported()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A posted sales credit memo whose only item line is skipped by the export keeps the item charge as a credit note line even when the service forces a document level allowance/charge, so that the exported document satisfies BR-16
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document level allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [GIVEN] A posted sales credit memo with an item charge assigned to an earlier return receipt and one item line without a quantity, which the export skips
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithChargeAndZeroQuantityLine(ItemChargeNo));
+ GetItemCrMemoLine(SalesCrMemoHeader, ItemSalesCrMemoLine);
+ Assert.AreEqual(0, ItemSalesCrMemoLine.Quantity, 'The scenario requires an item line without a quantity.');
+
+ // [WHEN] Export XRechnung Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as the only credit note line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, CrMemoLineTok), 'The item charge must be exported as a credit note line, so that the document keeps at least one credit note line.');
+ Assert.IsTrue(NodeValueExists(TempXMLBuffer, CrMemoLineTok + '/cac:Item/cac:SellersItemIdentification/cbc:ID', ItemChargeNo), 'The exported credit note line must be the item charge.');
+
+ // [THEN] The credit note line of the item charge carries the fallback quantity and the unit code C62
+ Path := CrMemoLineTok + '/cbc:CreditedQuantity';
+ Assert.AreEqual('1', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The charge is not exported as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, CrMemoDocumentAllowanceChargeTok), 'The item charge must not be exported as a document level allowance/charge.');
+ end;
+
+ #endregion
+
+ local procedure CreateAndPostSalesInvoiceWithItemCharge(NoOfItemLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesInvoiceWithItemCharge(NoOfItemLines, NoOfItemLines, ChargeQuantity, ChargeUnitPrice, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesCrMemoWithItemCharge(NoOfItemLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::"Credit Memo", NoOfItemLines, NoOfItemLines, ChargeQuantity, ChargeUnitPrice, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesInvoiceWithItemCharge(NoOfItemLines: Integer; NoOfAssignedLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::Invoice, NoOfItemLines, NoOfAssignedLines, ChargeQuantity, ChargeUnitPrice, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesDocumentWithItemCharge(DocumentType: Enum "Sales Document Type"; NoOfItemLines: Integer; NoOfAssignedLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesDocumentWithItemCharge(DocumentType, NoOfItemLines, NoOfAssignedLines, ChargeQuantity, ChargeUnitPrice, false, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesDocumentWithItemCharge(DocumentType: Enum "Sales Document Type"; NoOfItemLines: Integer; NoOfAssignedLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; BlankChargeDescription: Boolean; var ItemChargeNo: Code[20]): Code[20]
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ ItemSalesLine: Record "Sales Line";
+ ItemLineNo: array[2] of Integer;
+ Index: Integer;
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CreateSalesHeader(SalesHeader, DocumentType);
+ for Index := 1 to NoOfItemLines do begin
+ CreateItemSalesLine(ItemSalesLine, SalesHeader, Item);
+ ItemLineNo[Index] := ItemSalesLine."Line No.";
+ end;
+
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, ChargeQuantity);
+ ChargeSalesLine.Validate("Unit Price", ChargeUnitPrice);
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ if BlankChargeDescription then
+ ChargeSalesLine.Description := '';
+ ChargeSalesLine.Modify(true);
+
+ for Index := 1 to NoOfAssignedLines do begin
+ LibraryInventory.CreateItemChargeAssignment(
+ ItemChargeAssignmentSales, ChargeSalesLine, SalesHeader."Document Type", SalesHeader."No.", ItemLineNo[Index], Item."No.");
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeQuantity / NoOfAssignedLines);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostSalesInvoiceWithShipmentChargeOnly(var ItemChargeNo: Code[20]): Code[20]
+ var
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ CustomerNo: Code[20];
+ ShipmentNo: Code[20];
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CustomerNo := CreateCustomer();
+ ShipmentNo := CreateAndPostShipmentOnly(CustomerNo, Item);
+
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo);
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ChargeSalesLine.Modify(true);
+ AssignItemChargeToShipment(ChargeSalesLine, ShipmentNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostSalesInvoiceWithChargeAndZeroQuantityLine(var ItemChargeNo: Code[20]): Code[20]
+ var
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ZeroQuantitySalesLine: Record "Sales Line";
+ ChargeSalesLine: Record "Sales Line";
+ CustomerNo: Code[20];
+ ShipmentNo: Code[20];
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CustomerNo := CreateCustomer();
+ ShipmentNo := CreateAndPostShipmentOnly(CustomerNo, Item);
+
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo);
+ LibrarySales.CreateSalesLine(ZeroQuantitySalesLine, SalesHeader, ZeroQuantitySalesLine.Type::Item, Item."No.", 0);
+ ZeroQuantitySalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ ZeroQuantitySalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ZeroQuantitySalesLine.Modify(true);
+
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ChargeSalesLine.Modify(true);
+ AssignItemChargeToShipment(ChargeSalesLine, ShipmentNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostSalesCrMemoWithChargeAndZeroQuantityLine(var ItemChargeNo: Code[20]): Code[20]
+ var
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ZeroQuantitySalesLine: Record "Sales Line";
+ ChargeSalesLine: Record "Sales Line";
+ CustomerNo: Code[20];
+ ReturnReceiptNo: Code[20];
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CustomerNo := CreateCustomer();
+ ReturnReceiptNo := CreateAndPostReturnReceiptOnly(CustomerNo, Item);
+
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::"Credit Memo", CustomerNo);
+ LibrarySales.CreateSalesLine(ZeroQuantitySalesLine, SalesHeader, ZeroQuantitySalesLine.Type::Item, Item."No.", 0);
+ ZeroQuantitySalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ ZeroQuantitySalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ZeroQuantitySalesLine.Modify(true);
+
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ChargeSalesLine.Modify(true);
+ AssignItemChargeToReturnReceipt(ChargeSalesLine, ReturnReceiptNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostReturnReceiptOnly(CustomerNo: Code[20]; Item: Record Item): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ ItemSalesLine: Record "Sales Line";
+ ReturnReceiptHeader: Record "Return Receipt Header";
+ begin
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::"Return Order", CustomerNo);
+ CreateItemSalesLine(ItemSalesLine, SalesHeader, Item);
+ LibrarySales.PostSalesDocument(SalesHeader, true, false);
+
+ ReturnReceiptHeader.SetRange("Return Order No.", SalesHeader."No.");
+ ReturnReceiptHeader.FindFirst();
+ exit(ReturnReceiptHeader."No.");
+ end;
+
+ local procedure AssignItemChargeToReturnReceipt(ChargeSalesLine: Record "Sales Line"; ReturnReceiptNo: Code[20])
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ ReturnReceiptLine: Record "Return Receipt Line";
+ ItemChargeAssgntSales: Codeunit "Item Charge Assgnt. (Sales)";
+ begin
+ ItemChargeAssignmentSales.Init();
+ ItemChargeAssignmentSales.Validate("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.Validate("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.Validate("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.Validate("Item Charge No.", ChargeSalesLine."No.");
+ ItemChargeAssignmentSales.Validate("Unit Cost", ChargeSalesLine."Unit Price");
+ ReturnReceiptLine.SetRange("Document No.", ReturnReceiptNo);
+ ReturnReceiptLine.FindFirst();
+ ItemChargeAssgntSales.CreateRcptChargeAssgnt(ReturnReceiptLine, ItemChargeAssignmentSales);
+
+ ItemChargeAssignmentSales.SetRange("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.SetRange("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.SetRange("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.FindFirst();
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeSalesLine.Quantity);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ local procedure CreateAndPostShipmentOnly(CustomerNo: Code[20]; Item: Record Item): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ ItemSalesLine: Record "Sales Line";
+ SalesShipmentHeader: Record "Sales Shipment Header";
+ begin
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::Order, CustomerNo);
+ CreateItemSalesLine(ItemSalesLine, SalesHeader, Item);
+ LibrarySales.PostSalesDocument(SalesHeader, true, false);
+
+ SalesShipmentHeader.SetRange("Order No.", SalesHeader."No.");
+ SalesShipmentHeader.FindFirst();
+ exit(SalesShipmentHeader."No.");
+ end;
+
+ local procedure AssignItemChargeToShipment(ChargeSalesLine: Record "Sales Line"; ShipmentNo: Code[20])
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ SalesShipmentLine: Record "Sales Shipment Line";
+ ItemChargeAssgntSales: Codeunit "Item Charge Assgnt. (Sales)";
+ begin
+ ItemChargeAssignmentSales.Init();
+ ItemChargeAssignmentSales.Validate("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.Validate("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.Validate("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.Validate("Item Charge No.", ChargeSalesLine."No.");
+ ItemChargeAssignmentSales.Validate("Unit Cost", ChargeSalesLine."Unit Price");
+ SalesShipmentLine.SetRange("Document No.", ShipmentNo);
+ SalesShipmentLine.FindFirst();
+ ItemChargeAssgntSales.CreateShptChargeAssgnt(SalesShipmentLine, ItemChargeAssignmentSales);
+
+ ItemChargeAssignmentSales.SetRange("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.SetRange("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.SetRange("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.FindFirst();
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeSalesLine.Quantity);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ local procedure PrepareItemChargePosting()
+ var
+ InventorySetup: Record "Inventory Setup";
+ begin
+ LibrarySales.SetStockoutWarning(false);
+ LibrarySales.SetCreditWarningsToNoWarnings();
+ LibrarySales.SetCalcInvDiscount(false);
+ InventorySetup.Get();
+ InventorySetup.Validate("Prevent Negative Inventory", false);
+ InventorySetup.Modify(true);
+ end;
+
+ local procedure CreateItemSalesLine(var SalesLine: Record "Sales Line"; SalesHeader: Record "Sales Header"; Item: Record Item)
+ var
+ UnitOfMeasure: Record "Unit of Measure";
+ begin
+ LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure);
+ UnitOfMeasure."International Standard Code" := LibraryUtility.GenerateGUID();
+ UnitOfMeasure.Modify(true);
+ LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
+ SalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ SalesLine.Validate("Unit of Measure", UnitOfMeasure.Code);
+ SalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ SalesLine.Modify(true);
+ end;
+
+ local procedure CreateItemChargeForItem(Item: Record Item): Code[20]
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge.Validate("Gen. Prod. Posting Group", Item."Gen. Prod. Posting Group");
+ ItemCharge.Validate("VAT Prod. Posting Group", Item."VAT Prod. Posting Group");
+ ItemCharge.Modify(true);
+ exit(ItemCharge."No.");
+ end;
+
+ local procedure SetServiceItemChargeMapping(ItemChargeMapping: Enum "Item Charge E-Invoice Mapping")
+ begin
+ EDocumentService."Item Charge E-Invoice Mapping" := ItemChargeMapping;
+ EDocumentService.Modify();
+ end;
+
+ local procedure SetItemChargeReason(ItemChargeNo: Code[20]; ReasonText: Text[100]; ReasonCode: Code[10])
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(ItemChargeNo);
+ ItemCharge."E-Invoice Reason Text" := ReasonText;
+ ItemCharge."E-Invoice Reason Code" := ReasonCode;
+ ItemCharge.Modify(false);
+ end;
+
+ local procedure SetItemChargeUnitCode(ItemChargeNo: Code[20]; UnitCode: Code[10])
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(ItemChargeNo);
+ ItemCharge."E-Invoice Unit Code" := UnitCode;
+ ItemCharge.Modify(false);
+ end;
+
+ local procedure GetChargeInvoiceLine(SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No.");
+ SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::"Charge (Item)");
+ SalesInvoiceLine.FindFirst();
+ end;
+
+ local procedure GetChargeCrMemoLine(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ SalesCrMemoLine.SetRange("Document No.", SalesCrMemoHeader."No.");
+ SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::"Charge (Item)");
+ SalesCrMemoLine.FindFirst();
+ end;
+
+ local procedure GetItemCrMemoLine(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ SalesCrMemoLine.SetRange("Document No.", SalesCrMemoHeader."No.");
+ SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::Item);
+ SalesCrMemoLine.FindFirst();
+ end;
+
+ local procedure GetItemInvoiceLine(SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No.");
+ SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::Item);
+ SalesInvoiceLine.FindFirst();
+ end;
+
+ local procedure GetNodeCountByPath(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text): Integer
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, XPath);
+ exit(TempXMLBuffer.Count());
+ end;
+
+ local procedure NodeValueExists(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text; NodeValue: Text): Boolean
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, XPath);
+ TempXMLBuffer.SetRange(Value, NodeValue);
+ exit(not TempXMLBuffer.IsEmpty());
+ end;
+
+ local procedure SumNodeValuesByPath(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text) Total: Decimal
+ var
+ NodeValue: Decimal;
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, XPath);
+ if TempXMLBuffer.FindSet() then
+ repeat
+ Evaluate(NodeValue, TempXMLBuffer.Value, 9);
+ Total += NodeValue;
+ until TempXMLBuffer.Next() = 0;
+ end;
+
+ local procedure GetLastAttributeByPathWithError(var TempXMLBuffer: Record "XML Buffer" temporary; ElementXPath: Text; AttributeName: Text): Text
+ var
+ TempXMLBufferAttribute: Record "XML Buffer" temporary;
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, ElementXPath);
+ if TempXMLBuffer.FindLast() then begin
+ TempXMLBufferAttribute.Copy(TempXMLBuffer, true);
+ TempXMLBufferAttribute.Reset();
+ TempXMLBufferAttribute.SetRange("Parent Entry No.", TempXMLBuffer."Entry No.");
+ TempXMLBufferAttribute.SetRange(Type, TempXMLBufferAttribute.Type::Attribute);
+ TempXMLBufferAttribute.SetRange(Name, AttributeName);
+ if TempXMLBufferAttribute.FindFirst() then
+ exit(TempXMLBufferAttribute.Value);
+ end;
+ Error(AttributeNotFoundErr, AttributeName, ElementXPath);
+ end;
+
local procedure CreateAndPostSalesDocument(DocumentType: Enum "Sales Document Type"; LineType: Enum "Sales Line Type"; InvoiceDiscount: Boolean): Code[20];
var
SalesHeader: Record "Sales Header";
@@ -3270,6 +4649,18 @@ codeunit 13918 "XRechnung XML Document Tests"
Error('Node not found: %1', XPath);
end;
+ local procedure VerifyLastLineAmountMatchesQuantityTimesPrice(var TempXMLBuffer: Record "XML Buffer" temporary; QuantityXPath: Text; PriceXPath: Text; LineAmountXPath: Text)
+ var
+ LineAmount: Decimal;
+ Price: Decimal;
+ Quantity: Decimal;
+ begin
+ Evaluate(Quantity, GetLastNodeByPathWithError(TempXMLBuffer, QuantityXPath), 9);
+ Evaluate(Price, GetLastNodeByPathWithError(TempXMLBuffer, PriceXPath), 9);
+ Evaluate(LineAmount, GetLastNodeByPathWithError(TempXMLBuffer, LineAmountXPath), 9);
+ Assert.AreEqual(LineAmount, Round(Quantity * Price, 0.01), 'The quantity times the unit price must stay the net amount of the line.');
+ end;
+
local procedure GetAttributeByPathWithError(var TempXMLBuffer: Record "XML Buffer" temporary; ElementXPath: Text; AttributeName: Text): Text
var
TempXMLBufferAttribute: Record "XML Buffer" temporary;
diff --git a/src/Apps/DE/EDocumentDE/test/src/ZUGFeRDXMLDocumentTests.Codeunit.al b/src/Apps/DE/EDocumentDE/test/src/ZUGFeRDXMLDocumentTests.Codeunit.al
index 5d124af8213..1d1b4f365e2 100644
--- a/src/Apps/DE/EDocumentDE/test/src/ZUGFeRDXMLDocumentTests.Codeunit.al
+++ b/src/Apps/DE/EDocumentDE/test/src/ZUGFeRDXMLDocumentTests.Codeunit.al
@@ -18,6 +18,7 @@ using Microsoft.Foundation.Reporting;
using Microsoft.Foundation.UOM;
using Microsoft.Inventory.Item;
using Microsoft.Inventory.Location;
+using Microsoft.Inventory.Setup;
using Microsoft.Purchases.Document;
using Microsoft.Purchases.Vendor;
using Microsoft.Sales.Customer;
@@ -59,6 +60,18 @@ codeunit 13922 "ZUGFeRD XML Document Tests"
IncorrectValueErr: Label 'Incorrect value for %1', Locked = true;
AttributeNotFoundErr: Label 'Attribute %1 not found for node: %2', Locked = true, Comment = '%1 = XML attribute name, %2 = XML element XPath';
UnexpectedNodeErr: Label 'Node %1 must not exist.', Locked = true;
+ DocumentAllowanceChargeTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeAllowanceCharge', Locked = true;
+ InvoiceLineTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem', Locked = true;
+ InvoiceLineAllowanceChargeTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeAllowanceCharge', Locked = true;
+ MonetarySummationTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation', Locked = true;
+ HeaderTradeTaxTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax', Locked = true;
+ LineMonetarySummationTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation', Locked = true;
+ BilledQuantityTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', Locked = true;
+ TaxCategoryStandardTok: Label 'S', Locked = true;
+ ItemChargeReasonTextTok: Label 'Freight surcharge', Locked = true;
+ ItemChargeReasonCodeTok: Label 'FC', Locked = true;
+ UnitCodeOneTok: Label 'C62', Locked = true;
+ UnitCodeHourTok: Label 'HUR', Locked = true;
DocumentLineTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem', Locked = true;
SellerTaxRegistrationTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID', Locked = true;
BuyerGlobalIdTok: Label '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:GlobalID', Locked = true;
@@ -1960,6 +1973,1344 @@ codeunit 13922 "ZUGFeRD XML Document Tests"
end;
#endregion
+ #region ItemCharge
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyDocumentLevelItemChargeAllowanceCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge classified as a document level allowance/charge is exported as ram:SpecifiedTradeAllowanceCharge in the header trade settlement instead of as an invoice line
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and one item charge assigned to both of them
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] A document level charge is exported with the amount and the VAT category of the item charge
+ Path := DocumentAllowanceChargeTok + '/ram:ChargeIndicator/udt:Indicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:CategoryTradeTax/ram:CategoryCode';
+ Assert.AreEqual(TaxCategoryStandardTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:CategoryTradeTax/ram:RateApplicablePercent';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatFiveDecimal(ChargeSalesInvoiceLine."VAT %"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The item charge is no longer exported as an invoice line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'Only the item lines must be exported as invoice lines.');
+ Assert.IsFalse(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/ram:SpecifiedTradeProduct/ram:SellerAssignedID', ItemChargeNo), 'The item charge must not be exported as an invoice line.');
+
+ // [THEN] The charge is not repeated as a line level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'A document level charge must not be exported inside an invoice line.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyDocumentLevelItemChargeReason()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] The reason text and reason code of the item charge are exported on the document level allowance/charge
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with an item charge that is a document level charge
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+
+ // [GIVEN] The item charge carries a reason text and a reason code
+ SetItemChargeReason(ItemChargeNo, ItemChargeReasonTextTok, ItemChargeReasonCodeTok);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The reason code and the reason text of the item charge are exported
+ Path := DocumentAllowanceChargeTok + '/ram:ReasonCode';
+ Assert.AreEqual(ItemChargeReasonCodeTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual(ItemChargeReasonTextTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyDocumentLevelItemChargeReasonFallsBackToDescription()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Without a reason text on the item charge the description of the item charge line is exported, so that the mandatory allowance/charge reason is never empty
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with an item charge that is a document level charge and has no reason text
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The description of the item charge line is exported as the reason
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual(ChargeSalesInvoiceLine.Description, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] No empty reason code is exported
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok + '/ram:ReasonCode'), 'An item charge without a reason code must not export an empty reason code.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyDocumentLevelItemChargeReasonFallsBackToItemChargeNo()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Without a reason text, a reason code and a line description the item charge code is exported as the reason, so that the allowance/charge always carries one of the two reason elements EN 16931 requires
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with a document level item charge that has neither a reason text, nor a reason code, nor a line description
+ SalesInvoiceHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::Invoice, 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ Assert.AreEqual('', ChargeSalesInvoiceLine.Description, 'The scenario requires an item charge line without a description.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The code of the item charge is exported as the reason
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual(ItemChargeNo, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyDocumentLevelItemChargeWithReasonCodeOnlyKeepsTheReasonCode()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A reason code alone already satisfies the reason requirement of EN 16931, so the item charge code is not substituted as the reason text
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with a document level item charge without a line description
+ SalesInvoiceHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::Invoice, 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+
+ // [GIVEN] The item charge carries a reason code but no reason text
+ SetItemChargeReason(ItemChargeNo, '', ItemChargeReasonCodeTok);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The reason code of the item charge is exported
+ Path := DocumentAllowanceChargeTok + '/ram:ReasonCode';
+ Assert.AreEqual(ItemChargeReasonCodeTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The code of the item charge is not exported as the reason
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual('', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyLineLevelItemChargeAllowanceCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge classified as a line level allowance/charge is exported inside the line trade settlement of the invoice line it is assigned to
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge with the same VAT assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the invoice line of the assigned line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate invoice line.');
+ Path := InvoiceLineTok + '/ram:AssociatedDocumentLineDocument/ram:LineID';
+ Assert.AreEqual(Format(ItemSalesInvoiceLine."Line No."), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := InvoiceLineAllowanceChargeTok + '/ram:ChargeIndicator/udt:Indicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := InvoiceLineAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The line level allowance/charge carries no VAT category, because the VAT category of the invoice line applies
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok + '/ram:CategoryTradeTax/ram:CategoryCode'), 'A line level allowance/charge must not carry its own VAT category.');
+
+ // [THEN] The charge is not repeated as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'A line level charge must not be exported as a document level allowance/charge.');
+
+ // [THEN] The net amount of the invoice line includes the charge
+ Path := LineMonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ItemSalesInvoiceLine.Amount + ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyLineLevelItemChargeOnlyAffectsTheAssignedLine()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ AssignedLineAmount: Decimal;
+ UnassignedLineAmount: Decimal;
+ Path: Text;
+ begin
+ // [SCENARIO] A line level allowance/charge is exported only in the invoice line it is assigned to, and leaves the other invoice lines untouched
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and an item charge assigned to the first line only
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+ AssignedLineAmount := ItemSalesInvoiceLine.Amount;
+ ItemSalesInvoiceLine.Next();
+ UnassignedLineAmount := ItemSalesInvoiceLine.Amount;
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] Exactly one invoice line carries the allowance/charge
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate invoice line.');
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'The charge must be exported in the assigned invoice line only.');
+
+ // [THEN] Only the assigned invoice line reports the charge in its net amount
+ Path := LineMonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(AssignedLineAmount + ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(UnassignedLineAmount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyItemChargeInvoiceLineUsesFallbackQuantityAndUnitCode()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge exported as a regular invoice line carries quantity 1 and the unit code C62, never an empty unit code
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge of quantity 2 assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+ Assert.AreEqual(2, ChargeSalesInvoiceLine.Quantity, 'The scenario requires an item charge quantity that differs from the fallback quantity.');
+ Assert.AreEqual('', ChargeSalesInvoiceLine."Unit of Measure Code", 'The scenario requires an item charge line without a unit of measure.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as an invoice line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line.');
+ Path := InvoiceLineTok + '/ram:AssociatedDocumentLineDocument/ram:LineID';
+ Assert.AreEqual(Format(ChargeSalesInvoiceLine."Line No."), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The invoice line of the item charge carries quantity 1 and the unit code C62
+ Path := BilledQuantityTok;
+ Assert.AreEqual('1', GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The unit price of the invoice line matches the net amount, so that quantity times price stays the net amount of the line
+ Path := InvoiceLineTok + '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimalUnlimited(ChargeSalesInvoiceLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The item line keeps its own quantity and unit code
+ Path := BilledQuantityTok;
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimalUnlimited(ItemSalesInvoiceLine.Quantity), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(ExportZUGFeRDDocument.GetUoMCode(ItemSalesInvoiceLine."Unit of Measure Code"), GetAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyItemChargeInvoiceLineUsesUnitCodeOfItemCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A unit code configured on the item charge replaces C62 on the invoice line of the item charge
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales invoice with an item charge that carries the unit code HUR
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ SetItemChargeUnitCode(ItemChargeNo, UnitCodeHourTok);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The invoice line of the item charge carries the unit code of the item charge
+ Path := BilledQuantityTok;
+ Assert.AreEqual(UnitCodeHourTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyNegativeItemChargeInvoiceLineUsesNegativeQuantity()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge exported as a regular invoice line reports a negative quantity and a positive net price, so that the exported document satisfies BR-27
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales invoice with one item line and a negative item charge assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ Assert.IsTrue(ChargeSalesInvoiceLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as an invoice line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line.');
+ Path := InvoiceLineTok + '/ram:AssociatedDocumentLineDocument/ram:LineID';
+ Assert.AreEqual(Format(ChargeSalesInvoiceLine."Line No."), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The invoice line of the item charge reports the negative fallback quantity with the fallback unit code
+ Path := BilledQuantityTok;
+ Assert.AreEqual('-1', GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The net price of the invoice line is not negative, because the item net price must never be negative
+ Path := InvoiceLineTok + '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimalUnlimited(-ChargeSalesInvoiceLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The net amount of the invoice line stays negative
+ Path := LineMonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The quantity of the invoice line times its net price stays the net amount of the line
+ VerifyLastLineAmountMatchesQuantityTimesPrice(
+ TempXMLBuffer, BilledQuantityTok,
+ InvoiceLineTok + '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount',
+ LineMonetarySummationTok + '/ram:LineTotalAmount');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyNegativeItemChargeIsExportedAsAllowance()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge is exported as an allowance with a positive amount
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and a negative item charge assigned to both of them
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ Assert.IsTrue(ChargeSalesInvoiceLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported as an allowance with a positive amount
+ Path := DocumentAllowanceChargeTok + '/ram:ChargeIndicator/udt:Indicator';
+ Assert.AreEqual('false', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(-ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The allowance is reported in the allowance total and not in a charge total
+ SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT");
+ Path := MonetarySummationTok + '/ram:AllowanceTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(-ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, MonetarySummationTok + '/ram:ChargeTotalAmount'), 'A negative item charge must not be reported as a charge total.');
+
+ // [THEN] The totals stay consistent
+ Path := MonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount - ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:TaxBasisTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyForcedLineLevelItemChargeWithoutTargetLineIsDocumentLevel()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A forced line level allowance/charge that cannot be resolved to a single invoice line degrades to a document level allowance/charge
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into an invoice line allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line Allowance/Charge");
+
+ // [GIVEN] A posted sales invoice with an item charge assigned to two item lines, so that no single target line can be resolved
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported at document level instead of inside an invoice line
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'An unresolved line level charge must not be exported inside an invoice line.');
+ Path := DocumentAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The charge is not exported as an invoice line either
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'Only the item lines must be exported as invoice lines.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyTotalsWithDocumentLevelItemCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Moving an item charge out of the invoice lines keeps the monetary summation and the tax subtotals consistent
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with two item lines and one item charge assigned to both of them
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeInvoiceLine(SalesInvoiceHeader, ChargeSalesInvoiceLine);
+ SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The sum of the invoice lines no longer contains the charge and the charge is reported as the charge total
+ Path := MonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount - ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:ChargeTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesInvoiceLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:AllowanceTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(0), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The exported invoice lines add up to the reported line total amount
+ Assert.AreEqual(
+ SalesInvoiceHeader.Amount - ChargeSalesInvoiceLine.Amount, SumNodeValuesByPath(TempXMLBuffer, LineMonetarySummationTok + '/ram:LineTotalAmount'),
+ 'The exported invoice lines must add up to the reported line total amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := MonetarySummationTok + '/ram:TaxBasisTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:GrandTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:DuePayableAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:TaxTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT" - SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := HeaderTradeTaxTok + '/ram:CalculatedAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT" - SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := HeaderTradeTaxTok + '/ram:BasisAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyTotalsWithLineLevelItemCharge()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A line level allowance/charge stays inside the sum of the invoice lines and leaves the document totals untouched
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge with the same VAT assigned to that line
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ SalesInvoiceHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the invoice line it is assigned to
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate invoice line.');
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'The item charge must be exported as a line level allowance/charge.');
+
+ // [THEN] The line total amount still contains the charge and no charge total is reported
+ Path := MonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, MonetarySummationTok + '/ram:ChargeTotalAmount'), 'A line level charge must not be reported as a charge total.');
+ Path := MonetarySummationTok + '/ram:AllowanceTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(0), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The exported invoice lines add up to the reported line total amount
+ Assert.AreEqual(
+ SalesInvoiceHeader.Amount, SumNodeValuesByPath(TempXMLBuffer, LineMonetarySummationTok + '/ram:LineTotalAmount'),
+ 'The exported invoice lines must add up to the reported line total amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := MonetarySummationTok + '/ram:TaxBasisTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:DuePayableAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := HeaderTradeTaxTok + '/ram:BasisAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesInvoiceHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyDocumentLevelItemChargeAllowanceCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo classified as a document level allowance/charge is exported as ram:SpecifiedTradeAllowanceCharge in the header trade settlement instead of as a credit memo line
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with two item lines and one item charge assigned to both of them
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+
+ // [THEN] The item charge line of the credit memo carries a positive amount, so that a charge on a credit note keeps the charge indicator of an invoice
+ Assert.IsTrue(ChargeSalesCrMemoLine.Amount > 0, 'The scenario requires a positive item charge amount on the credit memo.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] A document level charge is exported with the amount and the VAT category of the item charge
+ Path := DocumentAllowanceChargeTok + '/ram:ChargeIndicator/udt:Indicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:CategoryTradeTax/ram:CategoryCode';
+ Assert.AreEqual(TaxCategoryStandardTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:CategoryTradeTax/ram:RateApplicablePercent';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatFiveDecimal(ChargeSalesCrMemoLine."VAT %"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The description of the item charge line is exported as the reason
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual(ChargeSalesCrMemoLine.Description, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The item charge is no longer exported as a credit memo line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'Only the item lines must be exported as credit memo lines.');
+ Assert.IsFalse(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/ram:SpecifiedTradeProduct/ram:SellerAssignedID', ItemChargeNo), 'The item charge must not be exported as a credit memo line.');
+
+ // [THEN] The charge is not repeated as a line level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'A document level charge must not be exported inside a credit memo line.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyDocumentLevelItemChargeReasonFallsBackToItemChargeNo()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Without a reason text, a reason code and a line description the item charge code is exported as the reason, so that the allowance/charge always carries one of the two reason elements EN 16931 requires
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with a document level item charge that has neither a reason text, nor a reason code, nor a line description
+ SalesCrMemoHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::"Credit Memo", 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.AreEqual('', ChargeSalesCrMemoLine.Description, 'The scenario requires an item charge line without a description.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The code of the item charge is exported as the reason
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual(ItemChargeNo, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyDocumentLevelItemChargeWithReasonCodeOnlyKeepsTheReasonCode()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A reason code alone already satisfies the reason requirement of EN 16931, so the item charge code is not substituted as the reason text
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with a document level item charge without a line description
+ SalesCrMemoHeader.Get(
+ CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::"Credit Memo", 2, 2, 2, LibraryRandom.RandDecInRange(10, 50, 2), true, ItemChargeNo));
+
+ // [GIVEN] The item charge carries a reason code but no reason text
+ SetItemChargeReason(ItemChargeNo, '', ItemChargeReasonCodeTok);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The reason code of the item charge is exported
+ Path := DocumentAllowanceChargeTok + '/ram:ReasonCode';
+ Assert.AreEqual(ItemChargeReasonCodeTok, GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The code of the item charge is not exported as the reason
+ Path := DocumentAllowanceChargeTok + '/ram:Reason';
+ Assert.AreEqual('', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyLineLevelItemChargeAllowanceCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo classified as a line level allowance/charge is exported inside the line trade settlement of the credit memo line it is assigned to
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge with the same VAT assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ GetItemCrMemoLine(SalesCrMemoHeader, ItemSalesCrMemoLine);
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the credit memo line of the assigned line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate credit memo line.');
+ Path := InvoiceLineTok + '/ram:AssociatedDocumentLineDocument/ram:LineID';
+ Assert.AreEqual(Format(ItemSalesCrMemoLine."Line No."), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := InvoiceLineAllowanceChargeTok + '/ram:ChargeIndicator/udt:Indicator';
+ Assert.AreEqual('true', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := InvoiceLineAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The line level allowance/charge carries no VAT category, because the VAT category of the credit memo line applies
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok + '/ram:CategoryTradeTax/ram:CategoryCode'), 'A line level allowance/charge must not carry its own VAT category.');
+
+ // [THEN] The charge is not repeated as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'A line level charge must not be exported as a document level allowance/charge.');
+
+ // [THEN] The net amount of the credit memo line includes the charge
+ Path := LineMonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ItemSalesCrMemoLine.Amount + ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyItemChargeCrMemoLineUsesFallbackQuantityAndUnitCode()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo exported as a regular credit memo line carries quantity 1 and the unit code C62, never an empty unit code
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge of quantity 2 assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.AreEqual(2, ChargeSalesCrMemoLine.Quantity, 'The scenario requires an item charge quantity that differs from the fallback quantity.');
+ Assert.AreEqual('', ChargeSalesCrMemoLine."Unit of Measure Code", 'The scenario requires an item charge line without a unit of measure.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as a credit memo line
+ Assert.AreEqual(2, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as a credit memo line.');
+ Path := InvoiceLineTok + '/ram:AssociatedDocumentLineDocument/ram:LineID';
+ Assert.AreEqual(Format(ChargeSalesCrMemoLine."Line No."), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The credit memo line of the item charge carries quantity 1 and the unit code C62
+ Assert.AreEqual('1', GetLastNodeByPathWithError(TempXMLBuffer, BilledQuantityTok), StrSubstNo(IncorrectValueErr, BilledQuantityTok));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, BilledQuantityTok, 'unitCode'), StrSubstNo(IncorrectValueErr, BilledQuantityTok));
+
+ // [THEN] The unit price of the credit memo line matches the net amount, so that quantity times price stays the net amount of the line
+ Path := InvoiceLineTok + '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimalUnlimited(ChargeSalesCrMemoLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] No allowance/charge is exported for the item charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'An item charge exported as a credit memo line must not be exported as an allowance/charge.');
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'An item charge exported as a credit memo line must not be exported as an allowance/charge.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyNegativeItemChargeCrMemoLineUsesNegativeQuantity()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge of a posted sales credit memo exported as a regular credit memo line reports a negative quantity and a positive net price, so that the exported document satisfies BR-27
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document line with a unit code
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [GIVEN] A posted sales credit memo with one item line and a negative item charge assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.IsTrue(ChargeSalesCrMemoLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The credit memo line of the item charge reports the negative fallback quantity with the fallback unit code
+ Assert.AreEqual('-1', GetLastNodeByPathWithError(TempXMLBuffer, BilledQuantityTok), StrSubstNo(IncorrectValueErr, BilledQuantityTok));
+ Assert.AreEqual(UnitCodeOneTok, GetLastAttributeByPathWithError(TempXMLBuffer, BilledQuantityTok, 'unitCode'), StrSubstNo(IncorrectValueErr, BilledQuantityTok));
+
+ // [THEN] The net price of the credit memo line is not negative, because the item net price must never be negative
+ Path := InvoiceLineTok + '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimalUnlimited(-ChargeSalesCrMemoLine.Amount), GetLastNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The quantity of the credit memo line times its net price stays the net amount of the line
+ VerifyLastLineAmountMatchesQuantityTimesPrice(
+ TempXMLBuffer, BilledQuantityTok,
+ InvoiceLineTok + '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount',
+ LineMonetarySummationTok + '/ram:LineTotalAmount');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyNegativeItemChargeIsExportedAsAllowance()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A negative item charge of a posted sales credit memo is exported as an allowance with a positive amount
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with two item lines and a negative item charge assigned to both of them
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(2, 2, -LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ Assert.IsTrue(ChargeSalesCrMemoLine.Amount < 0, 'The scenario requires a negative item charge amount.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported as an allowance with a positive amount
+ Path := DocumentAllowanceChargeTok + '/ram:ChargeIndicator/udt:Indicator';
+ Assert.AreEqual('false', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := DocumentAllowanceChargeTok + '/ram:ActualAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(-ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The allowance is reported in the allowance total and not in a charge total
+ SalesCrMemoHeader.CalcFields(Amount, "Amount Including VAT");
+ Path := MonetarySummationTok + '/ram:AllowanceTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(-ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, MonetarySummationTok + '/ram:ChargeTotalAmount'), 'A negative item charge must not be reported as a charge total.');
+
+ // [THEN] The totals stay consistent
+ Path := MonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount - ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:TaxBasisTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyTotalsWithDocumentLevelItemCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] Moving an item charge out of the credit memo lines keeps the monetary summation and the tax subtotals consistent
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with two item lines and one item charge assigned to both of them
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(2, 2, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ GetChargeCrMemoLine(SalesCrMemoHeader, ChargeSalesCrMemoLine);
+ SalesCrMemoHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The sum of the credit memo lines no longer contains the charge and the charge is reported as the charge total
+ Path := MonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount - ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:ChargeTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(ChargeSalesCrMemoLine.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:AllowanceTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(0), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The exported credit memo lines add up to the reported line total amount
+ Assert.AreEqual(
+ SalesCrMemoHeader.Amount - ChargeSalesCrMemoLine.Amount, SumNodeValuesByPath(TempXMLBuffer, LineMonetarySummationTok + '/ram:LineTotalAmount'),
+ 'The exported credit memo lines must add up to the reported line total amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := MonetarySummationTok + '/ram:TaxBasisTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:GrandTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:DuePayableAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := HeaderTradeTaxTok + '/ram:CalculatedAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT" - SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := HeaderTradeTaxTok + '/ram:BasisAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyTotalsWithLineLevelItemCharge()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A line level allowance/charge on a posted sales credit memo stays inside the sum of the credit memo lines and leaves the document totals untouched
+ Initialize();
+
+ // [GIVEN] A service that maps item charges automatically
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge with the same VAT assigned to that line
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithItemCharge(1, 1, LibraryRandom.RandDecInRange(10, 50, 2), ItemChargeNo));
+ SalesCrMemoHeader.CalcFields(Amount, "Amount Including VAT");
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The charge is exported inside the credit memo line it is assigned to
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must not be exported as a separate credit memo line.');
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineAllowanceChargeTok), 'The item charge must be exported as a line level allowance/charge.');
+
+ // [THEN] The line total amount still contains the charge and no charge total is reported
+ Path := MonetarySummationTok + '/ram:LineTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, MonetarySummationTok + '/ram:ChargeTotalAmount'), 'A line level charge must not be reported as a charge total.');
+ Path := MonetarySummationTok + '/ram:AllowanceTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(0), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The exported credit memo lines add up to the reported line total amount
+ Assert.AreEqual(
+ SalesCrMemoHeader.Amount, SumNodeValuesByPath(TempXMLBuffer, LineMonetarySummationTok + '/ram:LineTotalAmount'),
+ 'The exported credit memo lines must add up to the reported line total amount.');
+
+ // [THEN] The remaining document totals are unchanged
+ Path := MonetarySummationTok + '/ram:TaxBasisTotalAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Path := MonetarySummationTok + '/ram:DuePayableAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader."Amount Including VAT"), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The tax subtotal still covers the charge
+ Path := HeaderTradeTaxTok + '/ram:BasisAmount';
+ Assert.AreEqual(ExportZUGFeRDDocument.FormatDecimal(SalesCrMemoHeader.Amount), GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ end;
+
+ [Test]
+ procedure ExportPostedSalesInvoiceInZUGFeRDFormatVerifyChargeKeepsInvoiceLineWhenTheOnlyItemLineIsNotExported()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ Path: Text;
+ begin
+ // [SCENARIO] A posted sales invoice whose only item line is skipped by the export keeps the item charge as an invoice line even when the service forces a document level allowance/charge, so that the exported document satisfies BR-16
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document level allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [GIVEN] A posted sales invoice with an item charge assigned to an earlier shipment and one item line without a quantity, which the export skips
+ SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithChargeAndZeroQuantityLine(ItemChargeNo));
+ GetItemInvoiceLine(SalesInvoiceHeader, ItemSalesInvoiceLine);
+ Assert.AreEqual(0, ItemSalesInvoiceLine.Quantity, 'The scenario requires an item line without a quantity.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportInvoice(SalesInvoiceHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as the only invoice line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as an invoice line, so that the document keeps at least one invoice line.');
+ Assert.IsTrue(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/ram:SpecifiedTradeProduct/ram:SellerAssignedID', ItemChargeNo), 'The exported invoice line must be the item charge.');
+
+ // [THEN] The invoice line of the item charge carries the fallback quantity and the unit code C62
+ Path := BilledQuantityTok;
+ Assert.AreEqual('1', GetNodeByPathWithError(TempXMLBuffer, Path), StrSubstNo(IncorrectValueErr, Path));
+ Assert.AreEqual(UnitCodeOneTok, GetAttributeByPathWithError(TempXMLBuffer, Path, 'unitCode'), StrSubstNo(IncorrectValueErr, Path));
+
+ // [THEN] The charge is not exported as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'The item charge must not be exported as a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ExportPostedSalesCrMemoInZUGFeRDFormatVerifyChargeKeepsCrMemoLineWhenTheOnlyItemLineIsNotExported()
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TempXMLBuffer: Record "XML Buffer" temporary;
+ ItemChargeNo: Code[20];
+ begin
+ // [SCENARIO] A posted sales credit memo whose only item line is skipped by the export keeps the item charge as a credit memo line even when the service forces a document level allowance/charge, so that the exported document satisfies BR-16
+ Initialize();
+
+ // [GIVEN] A service that forces item charges into a document level allowance/charge
+ SetServiceItemChargeMapping(EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [GIVEN] A posted sales credit memo with an item charge assigned to an earlier return receipt and one item line without a quantity, which the export skips
+ SalesCrMemoHeader.Get(CreateAndPostSalesCrMemoWithChargeAndZeroQuantityLine(ItemChargeNo));
+ GetItemCrMemoLine(SalesCrMemoHeader, ItemSalesCrMemoLine);
+ Assert.AreEqual(0, ItemSalesCrMemoLine.Quantity, 'The scenario requires an item line without a quantity.');
+
+ // [WHEN] Export ZUGFeRD Electronic Document.
+ ExportCreditMemo(SalesCrMemoHeader, TempXMLBuffer);
+
+ // [THEN] The item charge is exported as the only credit memo line
+ Assert.AreEqual(1, GetNodeCountByPath(TempXMLBuffer, InvoiceLineTok), 'The item charge must be exported as a credit memo line, so that the document keeps at least one credit memo line.');
+ Assert.IsTrue(NodeValueExists(TempXMLBuffer, InvoiceLineTok + '/ram:SpecifiedTradeProduct/ram:SellerAssignedID', ItemChargeNo), 'The exported credit memo line must be the item charge.');
+
+ // [THEN] The credit memo line of the item charge carries the fallback quantity and the unit code C62
+ Assert.AreEqual('1', GetNodeByPathWithError(TempXMLBuffer, BilledQuantityTok), StrSubstNo(IncorrectValueErr, BilledQuantityTok));
+ Assert.AreEqual(UnitCodeOneTok, GetAttributeByPathWithError(TempXMLBuffer, BilledQuantityTok, 'unitCode'), StrSubstNo(IncorrectValueErr, BilledQuantityTok));
+
+ // [THEN] The charge is not exported as a document level allowance/charge
+ Assert.AreEqual(0, GetNodeCountByPath(TempXMLBuffer, DocumentAllowanceChargeTok), 'The item charge must not be exported as a document level allowance/charge.');
+ end;
+ #endregion
+
+ local procedure CreateAndPostSalesInvoiceWithChargeAndZeroQuantityLine(var ItemChargeNo: Code[20]): Code[20]
+ var
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ZeroQuantitySalesLine: Record "Sales Line";
+ ChargeSalesLine: Record "Sales Line";
+ CustomerNo: Code[20];
+ ShipmentNo: Code[20];
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CustomerNo := CreateCustomer();
+ ShipmentNo := CreateAndPostShipmentOnly(CustomerNo, Item);
+
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo);
+ LibrarySales.CreateSalesLine(ZeroQuantitySalesLine, SalesHeader, ZeroQuantitySalesLine.Type::Item, Item."No.", 0);
+ ZeroQuantitySalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ ZeroQuantitySalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ZeroQuantitySalesLine.Modify(true);
+
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ChargeSalesLine.Modify(true);
+ AssignItemChargeToShipment(ChargeSalesLine, ShipmentNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostSalesCrMemoWithChargeAndZeroQuantityLine(var ItemChargeNo: Code[20]): Code[20]
+ var
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ZeroQuantitySalesLine: Record "Sales Line";
+ ChargeSalesLine: Record "Sales Line";
+ CustomerNo: Code[20];
+ ReturnReceiptNo: Code[20];
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CustomerNo := CreateCustomer();
+ ReturnReceiptNo := CreateAndPostReturnReceiptOnly(CustomerNo, Item);
+
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::"Credit Memo", CustomerNo);
+ LibrarySales.CreateSalesLine(ZeroQuantitySalesLine, SalesHeader, ZeroQuantitySalesLine.Type::Item, Item."No.", 0);
+ ZeroQuantitySalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ ZeroQuantitySalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ZeroQuantitySalesLine.Modify(true);
+
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ ChargeSalesLine.Modify(true);
+ AssignItemChargeToReturnReceipt(ChargeSalesLine, ReturnReceiptNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostReturnReceiptOnly(CustomerNo: Code[20]; Item: Record Item): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ ItemSalesLine: Record "Sales Line";
+ ReturnReceiptHeader: Record "Return Receipt Header";
+ begin
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::"Return Order", CustomerNo);
+ CreateItemSalesLine(ItemSalesLine, SalesHeader, Item);
+ LibrarySales.PostSalesDocument(SalesHeader, true, false);
+
+ ReturnReceiptHeader.SetRange("Return Order No.", SalesHeader."No.");
+ ReturnReceiptHeader.FindFirst();
+ exit(ReturnReceiptHeader."No.");
+ end;
+
+ local procedure AssignItemChargeToReturnReceipt(ChargeSalesLine: Record "Sales Line"; ReturnReceiptNo: Code[20])
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ ReturnReceiptLine: Record "Return Receipt Line";
+ ItemChargeAssgntSales: Codeunit "Item Charge Assgnt. (Sales)";
+ begin
+ ItemChargeAssignmentSales.Init();
+ ItemChargeAssignmentSales.Validate("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.Validate("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.Validate("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.Validate("Item Charge No.", ChargeSalesLine."No.");
+ ItemChargeAssignmentSales.Validate("Unit Cost", ChargeSalesLine."Unit Price");
+ ReturnReceiptLine.SetRange("Document No.", ReturnReceiptNo);
+ ReturnReceiptLine.FindFirst();
+ ItemChargeAssgntSales.CreateRcptChargeAssgnt(ReturnReceiptLine, ItemChargeAssignmentSales);
+
+ ItemChargeAssignmentSales.SetRange("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.SetRange("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.SetRange("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.FindFirst();
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeSalesLine.Quantity);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ local procedure CreateAndPostShipmentOnly(CustomerNo: Code[20]; Item: Record Item): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ ItemSalesLine: Record "Sales Line";
+ SalesShipmentHeader: Record "Sales Shipment Header";
+ begin
+ CreateSalesHeader(SalesHeader, "Sales Document Type"::Order, CustomerNo);
+ CreateItemSalesLine(ItemSalesLine, SalesHeader, Item);
+ LibrarySales.PostSalesDocument(SalesHeader, true, false);
+
+ SalesShipmentHeader.SetRange("Order No.", SalesHeader."No.");
+ SalesShipmentHeader.FindFirst();
+ exit(SalesShipmentHeader."No.");
+ end;
+
+ local procedure AssignItemChargeToShipment(ChargeSalesLine: Record "Sales Line"; ShipmentNo: Code[20])
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ SalesShipmentLine: Record "Sales Shipment Line";
+ ItemChargeAssgntSales: Codeunit "Item Charge Assgnt. (Sales)";
+ begin
+ ItemChargeAssignmentSales.Init();
+ ItemChargeAssignmentSales.Validate("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.Validate("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.Validate("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.Validate("Item Charge No.", ChargeSalesLine."No.");
+ ItemChargeAssignmentSales.Validate("Unit Cost", ChargeSalesLine."Unit Price");
+ SalesShipmentLine.SetRange("Document No.", ShipmentNo);
+ SalesShipmentLine.FindFirst();
+ ItemChargeAssgntSales.CreateShptChargeAssgnt(SalesShipmentLine, ItemChargeAssignmentSales);
+
+ ItemChargeAssignmentSales.SetRange("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.SetRange("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.SetRange("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.FindFirst();
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeSalesLine.Quantity);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ local procedure CreateAndPostSalesInvoiceWithItemCharge(NoOfItemLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesInvoiceWithItemCharge(NoOfItemLines, NoOfItemLines, ChargeQuantity, ChargeUnitPrice, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesCrMemoWithItemCharge(NoOfItemLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::"Credit Memo", NoOfItemLines, NoOfItemLines, ChargeQuantity, ChargeUnitPrice, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesInvoiceWithItemCharge(NoOfItemLines: Integer; NoOfAssignedLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesDocumentWithItemCharge("Sales Document Type"::Invoice, NoOfItemLines, NoOfAssignedLines, ChargeQuantity, ChargeUnitPrice, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesDocumentWithItemCharge(DocumentType: Enum "Sales Document Type"; NoOfItemLines: Integer; NoOfAssignedLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; var ItemChargeNo: Code[20]): Code[20]
+ begin
+ exit(CreateAndPostSalesDocumentWithItemCharge(DocumentType, NoOfItemLines, NoOfAssignedLines, ChargeQuantity, ChargeUnitPrice, false, ItemChargeNo));
+ end;
+
+ local procedure CreateAndPostSalesDocumentWithItemCharge(DocumentType: Enum "Sales Document Type"; NoOfItemLines: Integer; NoOfAssignedLines: Integer; ChargeQuantity: Decimal; ChargeUnitPrice: Decimal; BlankChargeDescription: Boolean; var ItemChargeNo: Code[20]): Code[20]
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ Item: Record Item;
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ ItemSalesLine: Record "Sales Line";
+ ItemLineNo: array[2] of Integer;
+ Index: Integer;
+ begin
+ PrepareItemChargePosting();
+ LibraryInventory.CreateItem(Item);
+ CreateSalesHeader(SalesHeader, DocumentType);
+ for Index := 1 to NoOfItemLines do begin
+ CreateItemSalesLine(ItemSalesLine, SalesHeader, Item);
+ ItemLineNo[Index] := ItemSalesLine."Line No.";
+ end;
+
+ ItemChargeNo := CreateItemChargeForItem(Item);
+ LibrarySales.CreateSalesLine(ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", ItemChargeNo, ChargeQuantity);
+ ChargeSalesLine.Validate("Unit Price", ChargeUnitPrice);
+ ChargeSalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ if BlankChargeDescription then
+ ChargeSalesLine.Description := '';
+ ChargeSalesLine.Modify(true);
+
+ for Index := 1 to NoOfAssignedLines do begin
+ LibraryInventory.CreateItemChargeAssignment(
+ ItemChargeAssignmentSales, ChargeSalesLine, SalesHeader."Document Type", SalesHeader."No.", ItemLineNo[Index], Item."No.");
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeQuantity / NoOfAssignedLines);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure PrepareItemChargePosting()
+ var
+ InventorySetup: Record "Inventory Setup";
+ begin
+ LibrarySales.SetStockoutWarning(false);
+ LibrarySales.SetCreditWarningsToNoWarnings();
+ LibrarySales.SetCalcInvDiscount(false);
+ InventorySetup.Get();
+ InventorySetup.Validate("Prevent Negative Inventory", false);
+ InventorySetup.Modify(true);
+ end;
+
+ local procedure CreateItemSalesLine(var SalesLine: Record "Sales Line"; SalesHeader: Record "Sales Header"; Item: Record Item)
+ var
+ UnitOfMeasure: Record "Unit of Measure";
+ begin
+ LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure);
+ UnitOfMeasure."International Standard Code" := LibraryUtility.GenerateGUID();
+ UnitOfMeasure.Modify(true);
+ LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
+ SalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ SalesLine.Validate("Unit of Measure", UnitOfMeasure.Code);
+ SalesLine.Validate("Tax Category", TaxCategoryStandardTok);
+ SalesLine.Modify(true);
+ end;
+
+ local procedure CreateItemChargeForItem(Item: Record Item): Code[20]
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge.Validate("Gen. Prod. Posting Group", Item."Gen. Prod. Posting Group");
+ ItemCharge.Validate("VAT Prod. Posting Group", Item."VAT Prod. Posting Group");
+ ItemCharge.Modify(true);
+ exit(ItemCharge."No.");
+ end;
+
+ local procedure SetServiceItemChargeMapping(ItemChargeMapping: Enum "Item Charge E-Invoice Mapping")
+ begin
+ EDocumentService."Item Charge E-Invoice Mapping" := ItemChargeMapping;
+ EDocumentService.Modify();
+ end;
+
+ local procedure SetItemChargeReason(ItemChargeNo: Code[20]; ReasonText: Text[100]; ReasonCode: Code[10])
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(ItemChargeNo);
+ ItemCharge."E-Invoice Reason Text" := ReasonText;
+ ItemCharge."E-Invoice Reason Code" := ReasonCode;
+ ItemCharge.Modify(false);
+ end;
+
+ local procedure SetItemChargeUnitCode(ItemChargeNo: Code[20]; UnitCode: Code[10])
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(ItemChargeNo);
+ ItemCharge."E-Invoice Unit Code" := UnitCode;
+ ItemCharge.Modify(false);
+ end;
+
+ local procedure GetChargeInvoiceLine(SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No.");
+ SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::"Charge (Item)");
+ SalesInvoiceLine.FindFirst();
+ end;
+
+ local procedure GetItemInvoiceLine(SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No.");
+ SalesInvoiceLine.SetRange(Type, SalesInvoiceLine.Type::Item);
+ SalesInvoiceLine.FindFirst();
+ end;
+
+ local procedure GetChargeCrMemoLine(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ SalesCrMemoLine.SetRange("Document No.", SalesCrMemoHeader."No.");
+ SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::"Charge (Item)");
+ SalesCrMemoLine.FindFirst();
+ end;
+
+ local procedure GetItemCrMemoLine(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var SalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ SalesCrMemoLine.SetRange("Document No.", SalesCrMemoHeader."No.");
+ SalesCrMemoLine.SetRange(Type, SalesCrMemoLine.Type::Item);
+ SalesCrMemoLine.FindFirst();
+ end;
+
+ local procedure GetNodeCountByPath(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text): Integer
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, XPath);
+ exit(TempXMLBuffer.Count());
+ end;
+
+ local procedure NodeValueExists(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text; NodeValue: Text): Boolean
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, XPath);
+ TempXMLBuffer.SetRange(Value, NodeValue);
+ exit(not TempXMLBuffer.IsEmpty());
+ end;
+
+ local procedure SumNodeValuesByPath(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text) Total: Decimal
+ var
+ NodeValue: Decimal;
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, XPath);
+ if TempXMLBuffer.FindSet() then
+ repeat
+ Evaluate(NodeValue, TempXMLBuffer.Value, 9);
+ Total += NodeValue;
+ until TempXMLBuffer.Next() = 0;
+ end;
+
+ local procedure GetAttributeByPathWithError(var TempXMLBuffer: Record "XML Buffer" temporary; ElementXPath: Text; AttributeName: Text): Text
+ var
+ TempXMLBufferAttribute: Record "XML Buffer" temporary;
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, ElementXPath);
+ if TempXMLBuffer.FindFirst() then begin
+ TempXMLBufferAttribute.Copy(TempXMLBuffer, true);
+ TempXMLBufferAttribute.Reset();
+ TempXMLBufferAttribute.SetRange("Parent Entry No.", TempXMLBuffer."Entry No.");
+ TempXMLBufferAttribute.SetRange(Type, TempXMLBufferAttribute.Type::Attribute);
+ TempXMLBufferAttribute.SetRange(Name, AttributeName);
+ if TempXMLBufferAttribute.FindFirst() then
+ exit(TempXMLBufferAttribute.Value);
+ end;
+ Error(AttributeNotFoundErr, AttributeName, ElementXPath);
+ end;
+
+ local procedure GetLastAttributeByPathWithError(var TempXMLBuffer: Record "XML Buffer" temporary; ElementXPath: Text; AttributeName: Text): Text
+ var
+ TempXMLBufferAttribute: Record "XML Buffer" temporary;
+ begin
+ TempXMLBuffer.Reset();
+ TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
+ TempXMLBuffer.SetRange(Path, ElementXPath);
+ if TempXMLBuffer.FindLast() then begin
+ TempXMLBufferAttribute.Copy(TempXMLBuffer, true);
+ TempXMLBufferAttribute.Reset();
+ TempXMLBufferAttribute.SetRange("Parent Entry No.", TempXMLBuffer."Entry No.");
+ TempXMLBufferAttribute.SetRange(Type, TempXMLBufferAttribute.Type::Attribute);
+ TempXMLBufferAttribute.SetRange(Name, AttributeName);
+ if TempXMLBufferAttribute.FindFirst() then
+ exit(TempXMLBufferAttribute.Value);
+ end;
+ Error(AttributeNotFoundErr, AttributeName, ElementXPath);
+ end;
+
local procedure CreateAndPostSalesDocument(DocumentType: Enum "Sales Document Type"; LineType: Enum "Sales Line Type"; InvoiceDiscount: Boolean): Code[20];
var
SalesHeader: Record "Sales Header";
@@ -3271,25 +4622,6 @@ codeunit 13922 "ZUGFeRD XML Document Tests"
Error('Node not found: %1', XPath);
end;
- local procedure GetAttributeByPathWithError(var TempXMLBuffer: Record "XML Buffer" temporary; ElementXPath: Text; AttributeName: Text): Text
- var
- TempXMLBufferAttribute: Record "XML Buffer" temporary;
- begin
- TempXMLBuffer.Reset();
- TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element);
- TempXMLBuffer.SetRange(Path, ElementXPath);
- if TempXMLBuffer.FindFirst() then begin
- TempXMLBufferAttribute.Copy(TempXMLBuffer, true);
- TempXMLBufferAttribute.Reset();
- TempXMLBufferAttribute.SetRange("Parent Entry No.", TempXMLBuffer."Entry No.");
- TempXMLBufferAttribute.SetRange(Type, TempXMLBufferAttribute.Type::Attribute);
- TempXMLBufferAttribute.SetRange(Name, AttributeName);
- if TempXMLBufferAttribute.FindFirst() then
- exit(TempXMLBufferAttribute.Value);
- end;
- Error(AttributeNotFoundErr, AttributeName, ElementXPath);
- end;
-
local procedure NodeExistsByPath(var TempXMLBuffer: Record "XML Buffer" temporary; XPath: Text): Boolean
begin
TempXMLBuffer.Reset();
@@ -3316,6 +4648,18 @@ codeunit 13922 "ZUGFeRD XML Document Tests"
Error('Node not found: %1', XPath);
end;
+ local procedure VerifyLastLineAmountMatchesQuantityTimesPrice(var TempXMLBuffer: Record "XML Buffer" temporary; QuantityXPath: Text; PriceXPath: Text; LineAmountXPath: Text)
+ var
+ LineAmount: Decimal;
+ Price: Decimal;
+ Quantity: Decimal;
+ begin
+ Evaluate(Quantity, GetLastNodeByPathWithError(TempXMLBuffer, QuantityXPath), 9);
+ Evaluate(Price, GetLastNodeByPathWithError(TempXMLBuffer, PriceXPath), 9);
+ Evaluate(LineAmount, GetLastNodeByPathWithError(TempXMLBuffer, LineAmountXPath), 9);
+ Assert.AreEqual(LineAmount, Round(Quantity * Price, 0.01), 'The quantity times the unit price must stay the net amount of the line.');
+ end;
+
local procedure GetVATRegistrationNo(VATRegistrationNo: Text[20]; CountryRegionCode: Code[10]): Text[30];
begin
if CopyStr(VATRegistrationNo, 1, 2) <> CountryRegionCode then
diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al
index 4e2fdcd725d..cf7dc70f292 100644
--- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al
+++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al
@@ -177,6 +177,7 @@ permissionset 6100 "E-Doc. Core - Objects"
codeunit "E-Doc. Sales Providers" = X,
codeunit "E-Doc. Create Sales Order" = X,
codeunit "E-Doc. Sales Doc. Helper" = X,
+ codeunit "E-Doc. Item Charge Mapping" = X,
codeunit "Receive Documents" = X,
codeunit ReceiveContext = X,
codeunit "Send Runner" = X,
diff --git a/src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharge.TableExt.al b/src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharge.TableExt.al
new file mode 100644
index 00000000000..19b6d6bc141
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharge.TableExt.al
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.Inventory.Item;
+
+using Microsoft.eServices.EDocument;
+
+tableextension 6536 "E-Doc. Item Charge" extends "Item Charge"
+{
+ fields
+ {
+ field(6530; "E-Invoice Mapping"; Enum "Item Charge Mapping Override")
+ {
+ Caption = 'E-Invoice Mapping';
+ ToolTip = 'Specifies how this item charge is represented in exported e-documents, overriding the Item Charge Mapping setting of the exporting e-document service. If empty, the setting of the service applies. Automatic is itself an override: the item charge is classified based on its assignment to invoice lines, even if the service enforces a fixed representation.';
+ DataClassification = CustomerContent;
+ }
+ field(6531; "E-Invoice Reason Text"; Text[100])
+ {
+ Caption = 'E-Invoice Reason Text';
+ ToolTip = 'Specifies the allowance or charge reason text that is exported for this item charge in e-documents.';
+ DataClassification = CustomerContent;
+ }
+ field(6532; "E-Invoice Reason Code"; Code[10])
+ {
+ Caption = 'E-Invoice Reason Code';
+ ToolTip = 'Specifies the allowance or charge reason code that is exported for this item charge in e-documents.';
+ DataClassification = CustomerContent;
+ }
+ field(6533; "E-Invoice Unit Code"; Code[10])
+ {
+ Caption = 'E-Invoice Unit Code';
+ ToolTip = 'Specifies the unit code that is exported when this item charge is represented as an invoice line in e-documents. If empty, the unit code C62 is exported.';
+ DataClassification = CustomerContent;
+ }
+ }
+}
diff --git a/src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharges.PageExt.al b/src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharges.PageExt.al
new file mode 100644
index 00000000000..3389deed83a
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharges.PageExt.al
@@ -0,0 +1,42 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Extensions;
+
+using Microsoft.Inventory.Item;
+
+///
+/// A page extension for the Item Charges page that offers the per item charge e-document overrides.
+/// The columns are hidden, because they only take effect for e-document formats whose export evaluates them.
+/// An extension for such a format makes the columns visible.
+///
+pageextension 6537 "E-Doc. Item Charges" extends "Item Charges"
+{
+ layout
+ {
+ addlast(Control1)
+ {
+ field("E-Invoice Mapping"; Rec."E-Invoice Mapping")
+ {
+ ApplicationArea = All;
+ Visible = false;
+ }
+ field("E-Invoice Reason Text"; Rec."E-Invoice Reason Text")
+ {
+ ApplicationArea = All;
+ Visible = false;
+ }
+ field("E-Invoice Reason Code"; Rec."E-Invoice Reason Code")
+ {
+ ApplicationArea = All;
+ Visible = false;
+ }
+ field("E-Invoice Unit Code"; Rec."E-Invoice Unit Code")
+ {
+ ApplicationArea = All;
+ Visible = false;
+ }
+ }
+ }
+}
diff --git a/src/Apps/W1/EDocument/App/src/Extensions/ItemChargeMappingOverride.Enum.al b/src/Apps/W1/EDocument/App/src/Extensions/ItemChargeMappingOverride.Enum.al
new file mode 100644
index 00000000000..935c66f51b3
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Extensions/ItemChargeMappingOverride.Enum.al
@@ -0,0 +1,45 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument;
+
+///
+/// Overrides per item charge how the charge is represented in an exported e-document.
+/// The blank value means that no override is set and the setting of the exporting service applies.
+/// Automatic is a real override: it forces the automatic classification even if the service forces a structure.
+///
+///
+/// The enum is extensible, but the built-in classification only resolves the members declared here.
+/// An extension that adds a member is responsible for turning it into a structure: subscribe to
+/// "E-Doc. Item Charge Mapping".OnAfterGetItemChargeStructure and OnAfterGetSalesCrMemoItemChargeStructure,
+/// read the value from the "Item Charge" the "No." of the charge line the event passes points at, and set
+/// the Structure parameter accordingly. Both events are raised last, so the structure a subscriber sets wins
+/// over the built-in classification.
+///
+enum 6535 "Item Charge Mapping Override"
+{
+ Extensible = true;
+ Caption = 'Item Charge Mapping Override';
+
+ value(0; " ")
+ {
+ Caption = ' ', Locked = true;
+ }
+ value(1; Automatic)
+ {
+ Caption = 'Automatic';
+ }
+ value(2; "Document Allowance/Charge")
+ {
+ Caption = 'Document Level Allowance/Charge';
+ }
+ value(3; "Line Allowance/Charge")
+ {
+ Caption = 'Invoice Line Allowance/Charge';
+ }
+ value(4; "Line with Unit Code")
+ {
+ Caption = 'Invoice Line with Unit Code';
+ }
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al
new file mode 100644
index 00000000000..87da816483a
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al
@@ -0,0 +1,378 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument;
+
+using Microsoft.Foundation.Enums;
+using Microsoft.Inventory.Item;
+using Microsoft.Inventory.Ledger;
+using Microsoft.Sales.History;
+
+codeunit 6532 "E-Doc. Item Charge Mapping"
+{
+ Permissions =
+ tabledata "Item Charge" = r,
+ tabledata "Sales Invoice Line" = r,
+ tabledata "Sales Cr.Memo Line" = r,
+ tabledata "Value Entry" = r;
+
+ var
+ CachedDocumentLineNos: Dictionary of [Integer, Integer];
+ CachedLineNoDocumentNo: Code[20];
+ CachedInvoiceLineToKeepNo: Code[20];
+ CachedCrMemoLineToKeepNo: Code[20];
+ CachedInvoiceLineToKeep: Boolean;
+ CachedCrMemoLineToKeep: Boolean;
+ UnitCodeOneTok: Label 'C62', Locked = true;
+
+ ///
+ /// Determines which structure an item charge line of a posted sales invoice is exported as.
+ /// A mapping override on the item charge takes precedence over the setting of the service.
+ ///
+ /// The service that exports the document. Its Item Charge E-Invoice Mapping can force a structure unless the item charge overrides it.
+ /// The posted sales invoice that is exported.
+ /// The item charge line to classify. Must be of type Charge (Item).
+ /// Return value: the invoice line the charge belongs to. Only set for a line level allowance/charge.
+ /// The structure the item charge is exported as.
+ procedure GetItemChargeStructure(EDocumentService: Record "E-Document Service"; SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; var TargetSalesInvoiceLine: Record "Sales Invoice Line") Structure: Enum "Item Charge E-Doc. Structure"
+ var
+ AssignedSalesInvoiceLine: Record "Sales Invoice Line";
+ Mapping: Enum "Item Charge E-Invoice Mapping";
+ AssignedLineNo: Integer;
+ TargetLineNo: Integer;
+ HasLineToKeep: Boolean;
+ AssignedLineHasSameVAT: Boolean;
+ begin
+ SalesInvoiceLine.TestField(Type, SalesInvoiceLine.Type::"Charge (Item)");
+ Clear(TargetSalesInvoiceLine);
+
+ Mapping := GetEffectiveMapping(EDocumentService, SalesInvoiceLine."No.");
+ if NeedsLineToKeep(Mapping) then
+ HasLineToKeep := HasLineToKeepInInvoice(SalesInvoiceHeader."No.");
+ if NeedsAssignedLine(Mapping) then begin
+ AssignedLineNo := FindSingleAssignedLineNo(SalesInvoiceHeader."No.", SalesInvoiceLine."Line No.", SalesInvoiceLine."No.");
+ if AssignedLineNo <> 0 then
+ if AssignedSalesInvoiceLine.Get(SalesInvoiceHeader."No.", AssignedLineNo) then
+ AssignedLineHasSameVAT :=
+ HasSameVAT(
+ SalesInvoiceLine."VAT Calculation Type", SalesInvoiceLine."VAT %",
+ AssignedSalesInvoiceLine."VAT Calculation Type", AssignedSalesInvoiceLine."VAT %")
+ else
+ AssignedLineNo := 0;
+ end;
+
+ Structure := GetStructure(Mapping, HasLineToKeep, AssignedLineNo, AssignedLineHasSameVAT, TargetLineNo);
+ if TargetLineNo <> 0 then
+ TargetSalesInvoiceLine.Get(SalesInvoiceHeader."No.", TargetLineNo);
+
+ OnAfterGetItemChargeStructure(EDocumentService, SalesInvoiceHeader, SalesInvoiceLine, Structure, TargetSalesInvoiceLine);
+ end;
+
+ ///
+ /// Determines which structure an item charge line of a posted sales credit memo is exported as.
+ /// A mapping override on the item charge takes precedence over the setting of the service.
+ ///
+ /// The service that exports the document. Its Item Charge E-Invoice Mapping can force a structure unless the item charge overrides it.
+ /// The posted sales credit memo that is exported.
+ /// The item charge line to classify. Must be of type Charge (Item).
+ /// Return value: the credit memo line the charge belongs to. Only set for a line level allowance/charge.
+ /// The structure the item charge is exported as.
+ procedure GetItemChargeStructure(EDocumentService: Record "E-Document Service"; SalesCrMemoHeader: Record "Sales Cr.Memo Header"; SalesCrMemoLine: Record "Sales Cr.Memo Line"; var TargetSalesCrMemoLine: Record "Sales Cr.Memo Line") Structure: Enum "Item Charge E-Doc. Structure"
+ var
+ AssignedSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ Mapping: Enum "Item Charge E-Invoice Mapping";
+ AssignedLineNo: Integer;
+ TargetLineNo: Integer;
+ HasLineToKeep: Boolean;
+ AssignedLineHasSameVAT: Boolean;
+ begin
+ SalesCrMemoLine.TestField(Type, SalesCrMemoLine.Type::"Charge (Item)");
+ Clear(TargetSalesCrMemoLine);
+
+ Mapping := GetEffectiveMapping(EDocumentService, SalesCrMemoLine."No.");
+ if NeedsLineToKeep(Mapping) then
+ HasLineToKeep := HasLineToKeepInCrMemo(SalesCrMemoHeader."No.");
+ if NeedsAssignedLine(Mapping) then begin
+ AssignedLineNo := FindSingleAssignedLineNo(SalesCrMemoHeader."No.", SalesCrMemoLine."Line No.", SalesCrMemoLine."No.");
+ if AssignedLineNo <> 0 then
+ if AssignedSalesCrMemoLine.Get(SalesCrMemoHeader."No.", AssignedLineNo) then
+ AssignedLineHasSameVAT :=
+ HasSameVAT(
+ SalesCrMemoLine."VAT Calculation Type", SalesCrMemoLine."VAT %",
+ AssignedSalesCrMemoLine."VAT Calculation Type", AssignedSalesCrMemoLine."VAT %")
+ else
+ AssignedLineNo := 0;
+ end;
+
+ Structure := GetStructure(Mapping, HasLineToKeep, AssignedLineNo, AssignedLineHasSameVAT, TargetLineNo);
+ if TargetLineNo <> 0 then
+ TargetSalesCrMemoLine.Get(SalesCrMemoHeader."No.", TargetLineNo);
+
+ OnAfterGetSalesCrMemoItemChargeStructure(EDocumentService, SalesCrMemoHeader, SalesCrMemoLine, Structure, TargetSalesCrMemoLine);
+ end;
+
+ ///
+ /// Gets the quantity to use when an item charge is exported as a regular document line.
+ ///
+ /// The quantity of the fallback document line.
+ procedure GetFallbackQuantity(): Decimal
+ begin
+ exit(1);
+ end;
+
+ ///
+ /// Gets the quantity to use when an item charge of the given net amount is exported as a regular document line.
+ /// A negative net amount is reported as a negative quantity, because the item net price of a document line must never be negative.
+ ///
+ /// The net amount that the fallback document line reports.
+ /// The quantity of the fallback document line, negative if the net amount is negative.
+ procedure GetFallbackQuantity(NetAmount: Decimal): Decimal
+ begin
+ if NetAmount < 0 then
+ exit(-GetFallbackQuantity());
+
+ exit(GetFallbackQuantity());
+ end;
+
+ ///
+ /// Gets the net price to use when an item charge of the given net amount is exported as a regular document line.
+ /// The fallback document line reports a single unit, so the price carries the whole net amount, and it is never negative.
+ ///
+ /// The net amount that the fallback document line reports.
+ /// The net price of the fallback document line. Never negative, so that the exported document satisfies BR-27.
+ procedure GetFallbackUnitPrice(NetAmount: Decimal): Decimal
+ var
+ FallbackQuantity: Decimal;
+ begin
+ FallbackQuantity := GetFallbackQuantity();
+ if FallbackQuantity = 0 then
+ exit(NetAmount);
+
+ exit(Abs(NetAmount) / Abs(FallbackQuantity));
+ end;
+
+ ///
+ /// Gets the unit code to use when the given item charge is exported as a regular document line.
+ ///
+ /// The item charge whose unit code override applies.
+ /// The E-Invoice Unit Code of the item charge, or C62 - the UN/ECE Recommendation 20 code for 'one' - if none is set.
+ procedure GetFallbackUnitOfMeasureCode(ItemChargeNo: Code[20]): Code[10]
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.SetLoadFields("E-Invoice Unit Code");
+ if ItemCharge.Get(ItemChargeNo) then
+ if ItemCharge."E-Invoice Unit Code" <> '' then
+ exit(ItemCharge."E-Invoice Unit Code");
+
+ exit(UnitCodeOneTok);
+ end;
+
+ ///
+ /// Gets the allowance/charge reason code and reason text to export for the given item charge.
+ /// Both are plain field reads without any resolution rule, so they are returned together to read the item charge only once.
+ /// An item charge that cannot be read yields empty values, so that a missing item charge behaves like an item charge without a reason.
+ ///
+ /// The item charge whose reason applies.
+ /// Return value: the E-Invoice Reason Code of the item charge, or an empty code if none is set.
+ /// Return value: the E-Invoice Reason Text of the item charge, or an empty text if none is set.
+ procedure GetItemChargeReason(ItemChargeNo: Code[20]; var ReasonCode: Code[10]; var ReasonText: Text[100])
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ReasonCode := '';
+ ReasonText := '';
+
+ ItemCharge.SetLoadFields("E-Invoice Reason Code", "E-Invoice Reason Text");
+ if not ItemCharge.Get(ItemChargeNo) then
+ exit;
+
+ ReasonCode := ItemCharge."E-Invoice Reason Code";
+ ReasonText := ItemCharge."E-Invoice Reason Text";
+ end;
+
+ local procedure GetEffectiveMapping(EDocumentService: Record "E-Document Service"; ItemChargeNo: Code[20]) Mapping: Enum "Item Charge E-Invoice Mapping"
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.SetLoadFields("E-Invoice Mapping");
+ if ItemCharge.Get(ItemChargeNo) then
+ case ItemCharge."E-Invoice Mapping" of
+ ItemCharge."E-Invoice Mapping"::Automatic:
+ exit(Mapping::Automatic);
+ ItemCharge."E-Invoice Mapping"::"Document Allowance/Charge":
+ exit(Mapping::"Document Allowance/Charge");
+ ItemCharge."E-Invoice Mapping"::"Line Allowance/Charge":
+ exit(Mapping::"Line Allowance/Charge");
+ ItemCharge."E-Invoice Mapping"::"Line with Unit Code":
+ exit(Mapping::"Line with Unit Code");
+ end;
+
+ exit(EDocumentService."Item Charge E-Invoice Mapping");
+ end;
+
+ local procedure NeedsAssignedLine(Mapping: Enum "Item Charge E-Invoice Mapping"): Boolean
+ begin
+ exit(Mapping in [Mapping::Automatic, Mapping::"Line Allowance/Charge"]);
+ end;
+
+ local procedure NeedsLineToKeep(Mapping: Enum "Item Charge E-Invoice Mapping"): Boolean
+ begin
+ exit(Mapping in [Mapping::Automatic, Mapping::"Document Allowance/Charge", Mapping::"Line Allowance/Charge"]);
+ end;
+
+ local procedure GetStructure(Mapping: Enum "Item Charge E-Invoice Mapping"; HasLineToKeep: Boolean; AssignedLineNo: Integer; AssignedLineHasSameVAT: Boolean; var TargetLineNo: Integer) Structure: Enum "Item Charge E-Doc. Structure"
+ begin
+ TargetLineNo := 0;
+
+ // Turning the only line of the document into an allowance/charge would leave the document without any document line.
+ if not HasLineToKeep then
+ exit(Structure::"Line with Unit Code");
+
+ case Mapping of
+ Mapping::Automatic:
+ begin
+ if AssignedLineNo = 0 then
+ exit(Structure::"Document Allowance/Charge");
+ if not AssignedLineHasSameVAT then
+ exit(Structure::"Document Allowance/Charge");
+ TargetLineNo := AssignedLineNo;
+ exit(Structure::"Line Allowance/Charge");
+ end;
+ Mapping::"Document Allowance/Charge":
+ exit(Structure::"Document Allowance/Charge");
+ Mapping::"Line Allowance/Charge":
+ begin
+ TargetLineNo := AssignedLineNo;
+ exit(Structure::"Line Allowance/Charge");
+ end;
+ end;
+
+ exit(Structure::"Line with Unit Code");
+ end;
+
+ local procedure HasLineToKeepInInvoice(DocumentNo: Code[20]): Boolean
+ var
+ SalesInvoiceLine: Record "Sales Invoice Line";
+ begin
+ // The answer depends on the document, not on the item charge, but the classification runs once per
+ // charge line. Caching it per document keeps a document with many item charges to a single query.
+ if (CachedInvoiceLineToKeepNo = DocumentNo) and (DocumentNo <> '') then
+ exit(CachedInvoiceLineToKeep);
+
+ SalesInvoiceLine.SetRange("Document No.", DocumentNo);
+ SalesInvoiceLine.SetFilter(Type, '<>%1&<>%2', SalesInvoiceLine.Type::" ", SalesInvoiceLine.Type::"Charge (Item)");
+ CachedInvoiceLineToKeep := not SalesInvoiceLine.IsEmpty();
+ CachedInvoiceLineToKeepNo := DocumentNo;
+ exit(CachedInvoiceLineToKeep);
+ end;
+
+ local procedure HasLineToKeepInCrMemo(DocumentNo: Code[20]): Boolean
+ var
+ SalesCrMemoLine: Record "Sales Cr.Memo Line";
+ begin
+ // Cached per document for the same reason as the invoice variant. Invoices and credit memos keep
+ // separate caches, so a document number that exists in both tables cannot return the wrong answer.
+ if (CachedCrMemoLineToKeepNo = DocumentNo) and (DocumentNo <> '') then
+ exit(CachedCrMemoLineToKeep);
+
+ SalesCrMemoLine.SetRange("Document No.", DocumentNo);
+ SalesCrMemoLine.SetFilter(Type, '<>%1&<>%2', SalesCrMemoLine.Type::" ", SalesCrMemoLine.Type::"Charge (Item)");
+ CachedCrMemoLineToKeep := not SalesCrMemoLine.IsEmpty();
+ CachedCrMemoLineToKeepNo := DocumentNo;
+ exit(CachedCrMemoLineToKeep);
+ end;
+
+ local procedure FindSingleAssignedLineNo(DocumentNo: Code[20]; ChargeLineNo: Integer; ItemChargeNo: Code[20]): Integer
+ var
+ AssignedLineNos: List of [Integer];
+ begin
+ CollectAssignedLineNos(DocumentNo, ChargeLineNo, ItemChargeNo, AssignedLineNos);
+ if AssignedLineNos.Count() <> 1 then
+ exit(0);
+ exit(AssignedLineNos.Get(1));
+ end;
+
+ local procedure CollectAssignedLineNos(DocumentNo: Code[20]; ChargeLineNo: Integer; ItemChargeNo: Code[20]; var AssignedLineNos: List of [Integer])
+ var
+ ChargeValueEntry: Record "Value Entry";
+ DocumentLineNos: Dictionary of [Integer, Integer];
+ AssignedLineNo: Integer;
+ begin
+ Clear(AssignedLineNos);
+
+ ChargeValueEntry.SetLoadFields("Item Ledger Entry No.");
+ ChargeValueEntry.SetRange("Document No.", DocumentNo);
+ ChargeValueEntry.SetRange("Document Line No.", ChargeLineNo);
+ ChargeValueEntry.SetRange("Item Charge No.", ItemChargeNo);
+ if not ChargeValueEntry.FindSet() then
+ exit;
+
+ CollectDocumentLineNosByItemLedgerEntry(DocumentNo, DocumentLineNos);
+ repeat
+ if DocumentLineNos.Get(ChargeValueEntry."Item Ledger Entry No.", AssignedLineNo) then
+ if (AssignedLineNo <> 0) and not AssignedLineNos.Contains(AssignedLineNo) then
+ AssignedLineNos.Add(AssignedLineNo);
+ until ChargeValueEntry.Next() = 0;
+ end;
+
+ local procedure CollectDocumentLineNosByItemLedgerEntry(DocumentNo: Code[20]; var DocumentLineNos: Dictionary of [Integer, Integer])
+ var
+ SaleValueEntry: Record "Value Entry";
+ begin
+ // Every item charge of a document resolves against the same map, so it is built once per document
+ // instead of once per charge line. The document number is the cache key, so a new document rebuilds it.
+ if (CachedLineNoDocumentNo = DocumentNo) and (DocumentNo <> '') then begin
+ DocumentLineNos := CachedDocumentLineNos;
+ exit;
+ end;
+
+ SaleValueEntry.SetLoadFields("Item Ledger Entry No.", "Document Line No.");
+ SaleValueEntry.SetRange("Document No.", DocumentNo);
+ SaleValueEntry.SetRange("Item Charge No.", '');
+ if SaleValueEntry.FindSet() then
+ repeat
+ if not DocumentLineNos.ContainsKey(SaleValueEntry."Item Ledger Entry No.") then
+ DocumentLineNos.Add(SaleValueEntry."Item Ledger Entry No.", SaleValueEntry."Document Line No.");
+ until SaleValueEntry.Next() = 0;
+
+ // A document without any matching value entry is cached as well, so that it is not looked up again.
+ CachedLineNoDocumentNo := DocumentNo;
+ CachedDocumentLineNos := DocumentLineNos;
+ end;
+
+ local procedure HasSameVAT(ChargeVATCalculationType: Enum "Tax Calculation Type"; ChargeVATPercent: Decimal; AssignedVATCalculationType: Enum "Tax Calculation Type"; AssignedVATPercent: Decimal): Boolean
+ begin
+ if ChargeVATCalculationType <> AssignedVATCalculationType then
+ exit(false);
+
+ exit(ChargeVATPercent = AssignedVATPercent);
+ end;
+
+ ///
+ /// Integration event that allows subscribers to override the structure an item charge of a posted sales invoice is exported as.
+ ///
+ /// The service that exports the document.
+ /// The posted sales invoice that is exported.
+ /// The item charge line that was classified.
+ /// The resolved structure. Change it to export the item charge differently.
+ /// The invoice line the charge belongs to. Set it when changing the structure to a line level allowance/charge.
+ [IntegrationEvent(false, false)]
+ local procedure OnAfterGetItemChargeStructure(EDocumentService: Record "E-Document Service"; SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; var Structure: Enum "Item Charge E-Doc. Structure"; var TargetSalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ end;
+
+ ///
+ /// Integration event that allows subscribers to override the structure an item charge of a posted sales credit memo is exported as.
+ ///
+ /// The service that exports the document.
+ /// The posted sales credit memo that is exported.
+ /// The item charge line that was classified.
+ /// The resolved structure. Change it to export the item charge differently.
+ /// The credit memo line the charge belongs to. Set it when changing the structure to a line level allowance/charge.
+ [IntegrationEvent(false, false)]
+ local procedure OnAfterGetSalesCrMemoItemChargeStructure(EDocumentService: Record "E-Document Service"; SalesCrMemoHeader: Record "Sales Cr.Memo Header"; SalesCrMemoLine: Record "Sales Cr.Memo Line"; var Structure: Enum "Item Charge E-Doc. Structure"; var TargetSalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/ItemChargeEDocStructure.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/ItemChargeEDocStructure.Enum.al
new file mode 100644
index 00000000000..e0e80ca4605
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/ItemChargeEDocStructure.Enum.al
@@ -0,0 +1,27 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument;
+
+///
+/// The structure that an item charge line is exported as in an e-document.
+///
+enum 6534 "Item Charge E-Doc. Structure"
+{
+ Extensible = true;
+ Caption = 'Item Charge E-Document Structure';
+
+ value(0; "Line with Unit Code")
+ {
+ Caption = 'Invoice Line with Unit Code';
+ }
+ value(1; "Document Allowance/Charge")
+ {
+ Caption = 'Document Level Allowance/Charge';
+ }
+ value(2; "Line Allowance/Charge")
+ {
+ Caption = 'Invoice Line Allowance/Charge';
+ }
+}
diff --git a/src/Apps/W1/EDocument/App/src/Service/EDocumentService.Table.al b/src/Apps/W1/EDocument/App/src/Service/EDocumentService.Table.al
index 3d04b46583a..8bbb96c8279 100644
--- a/src/Apps/W1/EDocument/App/src/Service/EDocumentService.Table.al
+++ b/src/Apps/W1/EDocument/App/src/Service/EDocumentService.Table.al
@@ -296,6 +296,12 @@ table 6103 "E-Document Service"
ToolTip = 'Specifies the evaluator that determines if a document is eligible for export via this service.';
DataClassification = SystemMetadata;
}
+ field(42; "Item Charge E-Invoice Mapping"; Enum "Item Charge E-Invoice Mapping")
+ {
+ Caption = 'Item Charge Mapping';
+ ToolTip = 'Specifies how item charges on posted documents are represented in the e-documents that this service exports. Automatic classifies each item charge based on its assignment to invoice lines. Document Level Allowance/Charge exports every item charge as an allowance or charge of the whole document. Invoice Line Allowance/Charge exports it as an allowance or charge of the invoice line it is assigned to. Invoice Line with Unit Code exports it as a separate invoice line with a unit code. An item charge with its own E-Invoice Mapping overrides this setting.';
+ DataClassification = SystemMetadata;
+ }
#region [60-80] are reserved for purchase draft document settings.
field(60; "Verify Purch. Total Amounts"; Boolean)
{
diff --git a/src/Apps/W1/EDocument/App/src/Service/EdocumentService.Page.al b/src/Apps/W1/EDocument/App/src/Service/EdocumentService.Page.al
index 208c87a0131..1257947d247 100644
--- a/src/Apps/W1/EDocument/App/src/Service/EdocumentService.Page.al
+++ b/src/Apps/W1/EDocument/App/src/Service/EdocumentService.Page.al
@@ -195,6 +195,15 @@ page 6133 "E-Document Service"
{
}
}
+ group(ItemChargeMapping)
+ {
+ ShowCaption = false;
+
+ field("Item Charge E-Invoice Mapping"; Rec."Item Charge E-Invoice Mapping")
+ {
+ Visible = false;
+ }
+ }
}
part(EDocumentDataExchDef; "E-Doc. Service Data Exch. Sub")
diff --git a/src/Apps/W1/EDocument/App/src/Service/ItemChargeEInvoiceMapping.Enum.al b/src/Apps/W1/EDocument/App/src/Service/ItemChargeEInvoiceMapping.Enum.al
new file mode 100644
index 00000000000..308efe9fba6
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Service/ItemChargeEInvoiceMapping.Enum.al
@@ -0,0 +1,39 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument;
+
+///
+/// Determines how item charge lines are represented in an exported e-document.
+///
+///
+/// The enum is extensible, but the built-in classification only resolves the members declared here.
+/// An extension that adds a member is responsible for turning it into a structure: subscribe to
+/// "E-Doc. Item Charge Mapping".OnAfterGetItemChargeStructure and OnAfterGetSalesCrMemoItemChargeStructure,
+/// read the value from the "E-Document Service" record the event passes, and set the Structure parameter
+/// accordingly. Both events are raised last, so the structure a subscriber sets wins over the built-in
+/// classification.
+///
+enum 6533 "Item Charge E-Invoice Mapping"
+{
+ Extensible = true;
+ Caption = 'Item Charge E-Invoice Mapping';
+
+ value(0; Automatic)
+ {
+ Caption = 'Automatic';
+ }
+ value(1; "Document Allowance/Charge")
+ {
+ Caption = 'Document Level Allowance/Charge';
+ }
+ value(2; "Line Allowance/Charge")
+ {
+ Caption = 'Invoice Line Allowance/Charge';
+ }
+ value(3; "Line with Unit Code")
+ {
+ Caption = 'Invoice Line with Unit Code';
+ }
+}
diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocItemChrgSubscriber.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocItemChrgSubscriber.Codeunit.al
new file mode 100644
index 00000000000..c953d985d81
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocItemChrgSubscriber.Codeunit.al
@@ -0,0 +1,35 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Test;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.Sales.History;
+
+codeunit 139787 "E-Doc. Item Chrg. Subscriber"
+{
+ EventSubscriberInstance = Manual;
+
+ var
+ StructureToReturn: Enum "Item Charge E-Doc. Structure";
+ Invoked: Boolean;
+
+ procedure SetStructure(NewStructure: Enum "Item Charge E-Doc. Structure")
+ begin
+ StructureToReturn := NewStructure;
+ end;
+
+ procedure WasInvoked(): Boolean
+ begin
+ exit(Invoked);
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Item Charge Mapping", 'OnAfterGetItemChargeStructure', '', false, false)]
+ local procedure OverrideStructureOnAfterGetItemChargeStructure(var Structure: Enum "Item Charge E-Doc. Structure"; var TargetSalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ Invoked := true;
+ Structure := StructureToReturn;
+ Clear(TargetSalesInvoiceLine);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeSetupTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeSetupTests.Codeunit.al
new file mode 100644
index 00000000000..fefb3beea2b
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeSetupTests.Codeunit.al
@@ -0,0 +1,99 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Test;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Integration;
+using Microsoft.Inventory.Item;
+
+codeunit 139788 "E-Doc. Item Charge Setup Tests"
+{
+ Subtype = Test;
+ TestType = Uncategorized;
+
+ trigger OnRun();
+ begin
+ // [FEATURE] [E-Document] [Item Charge]
+ end;
+
+ var
+ Assert: Codeunit Assert;
+ LibraryEDocument: Codeunit "Library - E-Document";
+ LibraryInventory: Codeunit "Library - Inventory";
+ IncorrectValueErr: Label 'Incorrect value for %1', Locked = true;
+
+ // E-Document Core declares the controls for these fields with Visible = false, and the extension for a format
+ // that evaluates them shows them with a page extension. These tests therefore stay at record level: a TestPage
+ // cannot reach a control that is statically hidden, so a page test here would only pass in a tenant that happens
+ // to have such an extension installed. The page bindings are covered where the controls are actually shown -
+ // codeunit 148502 "Item Charge UI DE Tests" in the E-Document for Germany test app.
+
+ #region Core owns the fields
+ [Test]
+ procedure ItemChargeMappingIsStoredOnTheService()
+ var
+ EDocumentService: Record "E-Document Service";
+ begin
+ // [SCENARIO] E-Document Core owns the Item Charge E-Invoice Mapping setting on the E-Document service, so any format that reads it gets a working setting
+ // [GIVEN] An E-Document service
+ EDocumentService.Get(LibraryEDocument.CreateService("E-Document Format"::"PEPPOL BIS 3.0", "Service Integration"::"No Integration"));
+
+ // [WHEN] A mapping is selected on the service
+ EDocumentService.Validate("Item Charge E-Invoice Mapping", EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+ EDocumentService.Modify(true);
+
+ // [THEN] The selected mapping is stored on the service record
+ EDocumentService.Find();
+ Assert.AreEqual(EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code", EDocumentService."Item Charge E-Invoice Mapping", StrSubstNo(IncorrectValueErr, EDocumentService.FieldCaption("Item Charge E-Invoice Mapping")));
+ end;
+
+ [Test]
+ procedure ItemChargeEInvoiceFieldsAreStoredIndependently()
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ // [SCENARIO] E-Document Core owns the four e-document override fields on the item charge and keeps each value in its own field
+ // [GIVEN] An item charge
+ LibraryInventory.CreateItemCharge(ItemCharge);
+
+ // [WHEN] A distinct value is entered in each of the four override fields
+ ItemCharge.Validate("E-Invoice Mapping", ItemCharge."E-Invoice Mapping"::"Document Allowance/Charge");
+ ItemCharge.Validate("E-Invoice Reason Text", 'Freight surcharge');
+ ItemCharge.Validate("E-Invoice Reason Code", 'FC');
+ ItemCharge.Validate("E-Invoice Unit Code", 'HUR');
+ ItemCharge.Modify(true);
+
+ // [THEN] Each value is stored in its own field on the item charge record
+ ItemCharge.Find();
+ Assert.AreEqual(ItemCharge."E-Invoice Mapping"::"Document Allowance/Charge", ItemCharge."E-Invoice Mapping", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Mapping")));
+ Assert.AreEqual('Freight surcharge', ItemCharge."E-Invoice Reason Text", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Reason Text")));
+ Assert.AreEqual('FC', ItemCharge."E-Invoice Reason Code", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Reason Code")));
+ Assert.AreEqual('HUR', ItemCharge."E-Invoice Unit Code", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Unit Code")));
+ end;
+
+ [Test]
+ procedure BlankMappingOverrideIsDistinctFromAutomatic()
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ // [SCENARIO] A blank mapping override (use the service setting) and the Automatic override are two distinct states, so an item charge that was never configured does not silently behave as Automatic
+ // [GIVEN] An item charge without a mapping override
+ LibraryInventory.CreateItemCharge(ItemCharge);
+
+ // [THEN] The override is blank, not Automatic
+ Assert.AreEqual(ItemCharge."E-Invoice Mapping"::" ", ItemCharge."E-Invoice Mapping", 'An item charge without override must keep a blank mapping');
+ Assert.AreNotEqual(ItemCharge."E-Invoice Mapping"::Automatic, ItemCharge."E-Invoice Mapping", 'A blank override must not be stored as Automatic');
+
+ // [WHEN] Automatic is selected
+ ItemCharge.Validate("E-Invoice Mapping", ItemCharge."E-Invoice Mapping"::Automatic);
+ ItemCharge.Modify(true);
+
+ // [THEN] The Automatic override is stored, distinct from the blank value
+ ItemCharge.Find();
+ Assert.AreEqual(ItemCharge."E-Invoice Mapping"::Automatic, ItemCharge."E-Invoice Mapping", StrSubstNo(IncorrectValueErr, ItemCharge.FieldCaption("E-Invoice Mapping")));
+ Assert.AreNotEqual(ItemCharge."E-Invoice Mapping"::" ", ItemCharge."E-Invoice Mapping", 'Automatic must be stored as an override, not as the blank value');
+ end;
+ #endregion
+}
diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeTests.Codeunit.al
new file mode 100644
index 00000000000..d2ef9bf17e4
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeTests.Codeunit.al
@@ -0,0 +1,1239 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.eServices.EDocument.Test;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.Finance.VAT.Setup;
+using Microsoft.Foundation.Enums;
+using Microsoft.Inventory.Item;
+using Microsoft.Inventory.Setup;
+using Microsoft.Sales.Customer;
+using Microsoft.Sales.Document;
+using Microsoft.Sales.History;
+
+codeunit 139786 "E-Doc. Item Charge Tests"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+
+ var
+ Assert: Codeunit Assert;
+ LibraryERM: Codeunit "Library - ERM";
+ LibraryInventory: Codeunit "Library - Inventory";
+ LibraryRandom: Codeunit "Library - Random";
+ LibrarySales: Codeunit "Library - Sales";
+ IsInitialized: Boolean;
+ UnitCodeOneTok: Label 'C62', Locked = true;
+
+ #region Automatic classification
+
+ [Test]
+ procedure ItemChargeAssignedToOneLineWithSameVATIsLineLevelAllowanceCharge()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge assigned to exactly one invoice line with the same VAT category and rate is classified as a line level allowance/charge.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge that carries the item's VAT setup and is assigned to that line
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as an invoice line allowance/charge on the assigned line
+ Assert.AreEqual(Structure::"Line Allowance/Charge", Structure, 'Item charge assigned to a single line with matching VAT must be a line level allowance/charge.');
+ Assert.AreEqual(ItemSalesInvoiceLine."Line No.", TargetSalesInvoiceLine."Line No.", 'The assigned invoice line must be returned as the target line.');
+ Assert.AreEqual(SalesInvoiceHeader."No.", TargetSalesInvoiceLine."Document No.", 'The target line must belong to the exported invoice.');
+ end;
+
+ [Test]
+ procedure ItemChargeOnShippedOrderIsLineLevelAllowanceCharge()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge on a sales order is resolved through the posted shipment that the invoice line was invoiced from.
+ Initialize();
+
+ // [GIVEN] A sales order with an item line and an item charge assigned to it, shipped and invoiced
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostOrderWithChargeAssignedToItemLine(Customer, Item);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as an invoice line allowance/charge on the assigned line
+ Assert.AreEqual(Structure::"Line Allowance/Charge", Structure, 'An item charge invoiced from a shipment must be a line level allowance/charge.');
+ Assert.AreEqual(ItemSalesInvoiceLine."Line No.", TargetSalesInvoiceLine."Line No.", 'The assigned invoice line must be returned as the target line.');
+ end;
+
+ [Test]
+ procedure ItemChargeAssignedToOneLineWithDifferentVATRateIsDocumentLevel()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge assigned to one invoice line but with a different VAT rate is not classified as a line level allowance/charge.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice with one item line and an item charge with a deviating VAT rate assigned to that line
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo :=
+ CreateAndPostInvoiceWithChargeAssignedToItemLines(
+ Customer, Item, CreateVATProdPostingGroupWithRate(Customer, Item, GetVATRate(Customer, Item) + 5), 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as a document level allowance/charge and no target line is returned
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'An item charge with a deviating VAT rate must not be a line level allowance/charge.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ItemChargeAssignedToOneLineWithDifferentVATCalcTypeIsDocumentLevel()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge assigned to one invoice line but with a different VAT category is not classified as a line level allowance/charge.
+ Initialize();
+
+ // [GIVEN] A zero rated item
+ CreateCustomerAndItem(Customer, Item);
+ Item.Validate("VAT Prod. Posting Group", CreateVATProdPostingGroupWithRate(Customer, Item, 0));
+ Item.Modify(true);
+
+ // [GIVEN] A posted sales invoice with that item line and a reverse charge VAT item charge assigned to that line
+ InvoiceNo :=
+ CreateAndPostInvoiceWithChargeAssignedToItemLines(
+ Customer, Item, CreateReverseChargeVATProdPostingGroup(Customer, Item, 0), 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] Both lines carry the same VAT rate, so only the VAT category differs
+ Assert.AreEqual(
+ ItemSalesInvoiceLine."VAT %", ChargeSalesInvoiceLine."VAT %", 'The scenario requires an identical VAT rate on both lines.');
+ Assert.AreNotEqual(
+ ItemSalesInvoiceLine."VAT Calculation Type", ChargeSalesInvoiceLine."VAT Calculation Type",
+ 'The scenario requires a different VAT calculation type.');
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as a document level allowance/charge
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'An item charge with a deviating VAT category must not be a line level allowance/charge.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ItemChargeAssignedToTwoLinesIsDocumentLevel()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge that is spread over more than one invoice line has no unambiguous line assignment and becomes a document level allowance/charge.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice with two item lines and an item charge assigned to both of them
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 2);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as a document level allowance/charge
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'An item charge assigned to several lines must be a document level allowance/charge.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ItemChargeWithoutAssignmentOnInvoiceIsDocumentLevel()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ShipmentNo: Code[20];
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge that is not assigned to any line of the exported invoice becomes a document level allowance/charge.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice that shipped an item
+ CreateCustomerAndItem(Customer, Item);
+ ShipmentNo := CreateAndPostShipmentOnly(Customer, Item);
+
+ // [GIVEN] A second posted sales invoice with an item line and an item charge assigned to the earlier shipment
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToShipment(Customer, Item, ShipmentNo, true);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as a document level allowance/charge
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'An item charge without an assignment on the invoice must be a document level allowance/charge.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ItemChargeOnInvoiceWithoutOtherLinesIsInvoiceLine()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ShipmentNo: Code[20];
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge on an invoice that has no other line cannot become an allowance/charge, because the invoice would be left without any invoice line.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice that shipped an item
+ CreateCustomerAndItem(Customer, Item);
+ ShipmentNo := CreateAndPostShipmentOnly(Customer, Item);
+
+ // [GIVEN] A second posted sales invoice that only contains an item charge assigned to the earlier shipment
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToShipment(Customer, Item, ShipmentNo, false);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that maps item charges automatically
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge falls back to an invoice line
+ Assert.AreEqual(Structure::"Line with Unit Code", Structure, 'An item charge on an invoice without other lines must fall back to an invoice line.');
+ end;
+
+ #endregion
+
+ #region Forced mapping
+
+ [Test]
+ procedure ForcedDocumentLevelMappingOverridesAutomatic()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] Forcing document level mapping overrides the automatic classification.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a line level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that forces document level allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as a document level allowance/charge
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'The forced document level mapping must win over the automatic classification.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure ForcedLineLevelMappingOverridesAutomatic()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] Forcing line level mapping overrides the automatic classification.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a document level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 2);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that forces invoice line allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Line Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as an invoice line allowance/charge
+ Assert.AreEqual(Structure::"Line Allowance/Charge", Structure, 'The forced line level mapping must win over the automatic classification.');
+ end;
+
+ [Test]
+ procedure ForcedInvoiceLineMappingOverridesAutomatic()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] Forcing the invoice line mapping overrides the automatic classification.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a line level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that forces an invoice line with a unit code
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is exported as a regular invoice line
+ Assert.AreEqual(Structure::"Line with Unit Code", Structure, 'The forced invoice line mapping must win over the automatic classification.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for an invoice line.');
+ end;
+
+ [Test]
+ procedure ForcedDocumentLevelMappingOnChargeOnlyInvoiceFallsBackToInvoiceLine()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ShipmentNo: Code[20];
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] A forced document level mapping cannot turn the only line of an invoice into an allowance/charge, because the invoice would be left without any invoice line.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice that shipped an item
+ CreateCustomerAndItem(Customer, Item);
+ ShipmentNo := CreateAndPostShipmentOnly(Customer, Item);
+
+ // [GIVEN] A second posted sales invoice that only contains an item charge assigned to the earlier shipment
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToShipment(Customer, Item, ShipmentNo, false);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that forces document level allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge falls back to an invoice line
+ Assert.AreEqual(Structure::"Line with Unit Code", Structure, 'A forced document level item charge on an invoice without other lines must fall back to an invoice line.');
+ end;
+
+ [Test]
+ procedure ForcedLineLevelMappingOnChargeOnlyInvoiceFallsBackToInvoiceLine()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ShipmentNo: Code[20];
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] A forced line level mapping cannot turn the only line of an invoice into an allowance/charge, because the invoice would be left without any invoice line.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice that shipped an item
+ CreateCustomerAndItem(Customer, Item);
+ ShipmentNo := CreateAndPostShipmentOnly(Customer, Item);
+
+ // [GIVEN] A second posted sales invoice that only contains an item charge assigned to the earlier shipment
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToShipment(Customer, Item, ShipmentNo, false);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] A service that forces invoice line allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Line Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge falls back to an invoice line
+ Assert.AreEqual(Structure::"Line with Unit Code", Structure, 'A forced line level item charge on an invoice without other lines must fall back to an invoice line.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for an invoice line.');
+ end;
+
+ [Test]
+ procedure ForcedDocumentLevelMappingOnChargeOnlyCrMemoFallsBackToCrMemoLine()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TargetSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ ShipmentNo: Code[20];
+ CrMemoNo: Code[20];
+ begin
+ // [SCENARIO] A forced document level mapping cannot turn the only line of a credit memo into an allowance/charge, because the credit memo would be left without any credit memo line.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice that shipped an item
+ CreateCustomerAndItem(Customer, Item);
+ ShipmentNo := CreateAndPostShipmentOnly(Customer, Item);
+
+ // [GIVEN] A posted sales credit memo that only contains an item charge assigned to the earlier shipment
+ CrMemoNo := CreateAndPostCrMemoWithChargeAssignedToShipment(Customer, Item, ShipmentNo);
+ GetPostedCrMemoLines(CrMemoNo, SalesCrMemoHeader, ChargeSalesCrMemoLine, ItemSalesCrMemoLine);
+
+ // [GIVEN] A service that forces document level allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesCrMemoHeader, ChargeSalesCrMemoLine, TargetSalesCrMemoLine);
+
+ // [THEN] The charge falls back to a credit memo line
+ Assert.AreEqual(Structure::"Line with Unit Code", Structure, 'A forced document level item charge on a credit memo without other lines must fall back to a credit memo line.');
+ end;
+
+ #endregion
+
+ #region Per-item-charge override
+
+ [Test]
+ procedure OverriddenDocumentLevelMappingOverridesServiceSetting()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ MappingOverride: Enum "Item Charge Mapping Override";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] A document level mapping override on the item charge wins over the service setting.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a line level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] The item charge overrides the mapping with a document level allowance/charge
+ SetItemChargeMapping(ChargeSalesInvoiceLine."No.", MappingOverride::"Document Allowance/Charge");
+
+ // [GIVEN] A service that forces an invoice line with a unit code
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Line with Unit Code");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as a document level allowance/charge
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'The item charge mapping override must win over the service setting.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure OverriddenLineLevelMappingOverridesServiceSetting()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ MappingOverride: Enum "Item Charge Mapping Override";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] A line level mapping override on the item charge wins over the service setting and returns the assigned line.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice with an item charge assigned to a single item line
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] The item charge overrides the mapping with an invoice line allowance/charge
+ SetItemChargeMapping(ChargeSalesInvoiceLine."No.", MappingOverride::"Line Allowance/Charge");
+
+ // [GIVEN] A service that forces document level allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified as an invoice line allowance/charge on the assigned line
+ Assert.AreEqual(Structure::"Line Allowance/Charge", Structure, 'The item charge mapping override must win over the service setting.');
+ Assert.AreEqual(ItemSalesInvoiceLine."Line No.", TargetSalesInvoiceLine."Line No.", 'The assigned invoice line must be returned as the target line.');
+ end;
+
+ [Test]
+ procedure OverriddenInvoiceLineMappingOverridesServiceSetting()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ MappingOverride: Enum "Item Charge Mapping Override";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An invoice line mapping override on the item charge wins over the service setting.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice with an item charge assigned to a single item line
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] The item charge overrides the mapping with an invoice line with a unit code
+ SetItemChargeMapping(ChargeSalesInvoiceLine."No.", MappingOverride::"Line with Unit Code");
+
+ // [GIVEN] A service that forces invoice line allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Line Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is exported as a regular invoice line and no target line is returned
+ Assert.AreEqual(Structure::"Line with Unit Code", Structure, 'The item charge mapping override must win over the service setting.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for an invoice line.');
+ end;
+
+ [Test]
+ procedure OverriddenAutomaticMappingForcesAutomaticClassification()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ MappingOverride: Enum "Item Charge Mapping Override";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An Automatic override on the item charge forces the automatic classification even if the service forces a structure.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a line level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] The item charge overrides the mapping with Automatic
+ SetItemChargeMapping(ChargeSalesInvoiceLine."No.", MappingOverride::Automatic);
+
+ // [GIVEN] A service that forces document level allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge is classified automatically as an invoice line allowance/charge on the assigned line
+ Assert.AreEqual(Structure::"Line Allowance/Charge", Structure, 'The Automatic override must force the automatic classification over the forced service setting.');
+ Assert.AreEqual(ItemSalesInvoiceLine."Line No.", TargetSalesInvoiceLine."Line No.", 'The assigned invoice line must be returned as the target line.');
+ end;
+
+ [Test]
+ procedure UnsetOverrideFallsThroughToServiceSetting()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ ItemCharge: Record "Item Charge";
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] An item charge without a mapping override follows the service setting.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a line level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+
+ // [GIVEN] The item charge has no mapping override
+ ItemCharge.Get(ChargeSalesInvoiceLine."No.");
+ Assert.AreEqual(ItemCharge."E-Invoice Mapping"::" ", ItemCharge."E-Invoice Mapping", 'The scenario requires an item charge without a mapping override.');
+
+ // [GIVEN] A service that forces document level allowance/charge
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::"Document Allowance/Charge");
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] The charge follows the service setting and is classified as a document level allowance/charge
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'An item charge without a mapping override must follow the service setting.');
+ Assert.AreEqual(0, TargetSalesInvoiceLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ [Test]
+ procedure PerChargeUnitCodeIsUsedForFallbackInvoiceLine()
+ var
+ ItemCharge: Record "Item Charge";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ begin
+ // [SCENARIO] A unit code set on the item charge replaces C62 on the fallback invoice line.
+ Initialize();
+
+ // [GIVEN] An item charge with the unit code HUR
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge."E-Invoice Unit Code" := 'HUR';
+ ItemCharge.Modify(false);
+
+ // [WHEN] The fallback unit code is resolved for the item charge
+ // [THEN] The unit code of the item charge is returned
+ Assert.AreEqual('HUR', EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(ItemCharge."No."), 'The unit code of the item charge must replace the default unit code.');
+ end;
+
+ [Test]
+ procedure FallbackUnitCodeIsC62WhenNoOverrideIsSet()
+ var
+ ItemCharge: Record "Item Charge";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ begin
+ // [SCENARIO] Without a unit code on the item charge the fallback invoice line uses C62.
+ Initialize();
+
+ // [GIVEN] An item charge without a unit code
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge.TestField("E-Invoice Unit Code", '');
+
+ // [WHEN] The fallback unit code is resolved for the item charge
+ // [THEN] C62 is returned
+ Assert.AreEqual(UnitCodeOneTok, EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(ItemCharge."No."), 'An item charge without a unit code must fall back to C62.');
+
+ // [THEN] C62 is also returned for an unknown item charge
+ Assert.AreEqual(UnitCodeOneTok, EDocItemChargeMapping.GetFallbackUnitOfMeasureCode('NONEXISTING'), 'An unknown item charge must fall back to C62.');
+ end;
+
+ [Test]
+ procedure ReasonTextAndReasonCodeRoundTripThroughApi()
+ var
+ ItemCharge: Record "Item Charge";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ ReasonCode: Code[10];
+ ReasonText: Text[100];
+ begin
+ // [SCENARIO] The reason text and reason code of an item charge are exposed through the mapping API.
+ Initialize();
+
+ // [GIVEN] An item charge with a reason text and a reason code
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge."E-Invoice Reason Text" := 'Freight surcharge';
+ ItemCharge."E-Invoice Reason Code" := 'FC';
+ ItemCharge.Modify(false);
+
+ // [WHEN] The reason of the item charge is resolved
+ EDocItemChargeMapping.GetItemChargeReason(ItemCharge."No.", ReasonCode, ReasonText);
+
+ // [THEN] The values of the item charge are returned
+ Assert.AreEqual('Freight surcharge', ReasonText, 'The reason text of the item charge must be returned.');
+ Assert.AreEqual('FC', ReasonCode, 'The reason code of the item charge must be returned.');
+
+ // [WHEN] The reason of an unknown item charge is resolved
+ EDocItemChargeMapping.GetItemChargeReason('NONEXISTING', ReasonCode, ReasonText);
+
+ // [THEN] Empty values are returned
+ Assert.AreEqual('', ReasonText, 'An unknown item charge must have an empty reason text.');
+ Assert.AreEqual('', ReasonCode, 'An unknown item charge must have an empty reason code.');
+ end;
+
+ [Test]
+ procedure SubscriberOverridesItemChargeOverride()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ EDocItemChargeSubscriber: Codeunit "E-Doc. Item Chrg. Subscriber";
+ MappingOverride: Enum "Item Charge Mapping Override";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] A subscriber can still override the classification when the item charge carries a mapping override.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice whose item charge overrides the mapping with an invoice line with a unit code
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+ SetItemChargeMapping(ChargeSalesInvoiceLine."No.", MappingOverride::"Line with Unit Code");
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A subscriber that forces a document level allowance/charge
+ EDocItemChargeSubscriber.SetStructure(Structure::"Document Allowance/Charge");
+ BindSubscription(EDocItemChargeSubscriber);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+ UnbindSubscription(EDocItemChargeSubscriber);
+
+ // [THEN] The subscriber was called and its classification wins over the item charge mapping override
+ Assert.IsTrue(EDocItemChargeSubscriber.WasInvoked(), 'The classification event must be raised.');
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'The subscriber must win over the item charge mapping override.');
+ end;
+
+ #endregion
+
+ #region Extensibility and fallback
+
+ [Test]
+ procedure SubscriberOverridesResolvedStructure()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ EDocItemChargeSubscriber: Codeunit "E-Doc. Item Chrg. Subscriber";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] A subscriber can override the resolved classification before the e-document is generated.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice where the item charge would automatically be a line level allowance/charge
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [GIVEN] A subscriber that forces a document level allowance/charge
+ EDocItemChargeSubscriber.SetStructure(Structure::"Document Allowance/Charge");
+ BindSubscription(EDocItemChargeSubscriber);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ChargeSalesInvoiceLine, TargetSalesInvoiceLine);
+ UnbindSubscription(EDocItemChargeSubscriber);
+
+ // [THEN] The subscriber was called and its classification is returned
+ Assert.IsTrue(EDocItemChargeSubscriber.WasInvoked(), 'The classification event must be raised.');
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'The subscriber must be able to override the resolved classification.');
+ end;
+
+ [Test]
+ procedure FallbackInvoiceLineUsesQuantityOneAndUnitCodeC62()
+ var
+ ItemCharge: Record "Item Charge";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ begin
+ // [SCENARIO] The invoice line fallback carries quantity 1 and the unit code C62, so that neither BR-23 nor BR-CL-23 is violated.
+ Initialize();
+
+ // [GIVEN] An item charge without a unit code override
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge.TestField("E-Invoice Unit Code", '');
+
+ // [WHEN] The fallback invoice line values are read
+ // [THEN] The quantity is 1 and the unit code is C62
+ Assert.AreEqual(1, EDocItemChargeMapping.GetFallbackQuantity(), 'The item charge fallback invoice line must have quantity 1.');
+ Assert.AreEqual(UnitCodeOneTok, EDocItemChargeMapping.GetFallbackUnitOfMeasureCode(ItemCharge."No."), 'The item charge fallback invoice line must use the unit code C62.');
+ end;
+
+ [Test]
+ procedure ClassifyingNonItemChargeLineFails()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ ChargeSalesInvoiceLine: Record "Sales Invoice Line";
+ ItemSalesInvoiceLine: Record "Sales Invoice Line";
+ TargetSalesInvoiceLine: Record "Sales Invoice Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ InvoiceNo: Code[20];
+ begin
+ // [SCENARIO] Only item charge lines can be classified.
+ Initialize();
+
+ // [GIVEN] A posted sales invoice with an item line and an item charge line
+ CreateCustomerAndItem(Customer, Item);
+ InvoiceNo := CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer, Item, Item."VAT Prod. Posting Group", 1);
+ GetPostedLines(InvoiceNo, SalesInvoiceHeader, ChargeSalesInvoiceLine, ItemSalesInvoiceLine);
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item line is classified
+ asserterror EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesInvoiceHeader, ItemSalesInvoiceLine, TargetSalesInvoiceLine);
+
+ // [THEN] A TestField error for the line type is raised
+ Assert.ExpectedTestFieldError(ItemSalesInvoiceLine.FieldCaption(Type), Format(ItemSalesInvoiceLine.Type::"Charge (Item)"));
+ end;
+
+ [Test]
+ procedure CrMemoChargeAssignedToSingleLineIsLineLevelAllowanceCharge()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TargetSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ CrMemoNo: Code[20];
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo that is assigned to a single credit memo line with the same VAT becomes a line level allowance/charge, which proves that the value entry recovery works for credit memos too.
+ Initialize();
+
+ // [GIVEN] A posted sales credit memo with one item line and an item charge assigned to it
+ CreateCustomerAndItem(Customer, Item);
+ CrMemoNo := CreateAndPostCrMemoWithChargeAssignedToItemLines(Customer, Item, 1);
+ GetPostedCrMemoLines(CrMemoNo, SalesCrMemoHeader, ChargeSalesCrMemoLine, ItemSalesCrMemoLine);
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesCrMemoHeader, ChargeSalesCrMemoLine, TargetSalesCrMemoLine);
+
+ // [THEN] The charge is a line level allowance/charge of the line it is assigned to
+ Assert.AreEqual(Structure::"Line Allowance/Charge", Structure, 'A credit memo charge assigned to a single line with the same VAT must become a line level allowance/charge.');
+ Assert.AreEqual(ItemSalesCrMemoLine."Line No.", TargetSalesCrMemoLine."Line No.", 'The target line must be the credit memo line the charge is assigned to.');
+ end;
+
+ [Test]
+ procedure CrMemoChargeAssignedToTwoLinesIsDocumentLevelAllowanceCharge()
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ EDocumentService: Record "E-Document Service";
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ ItemSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ TargetSalesCrMemoLine: Record "Sales Cr.Memo Line";
+ EDocItemChargeMapping: Codeunit "E-Doc. Item Charge Mapping";
+ Structure: Enum "Item Charge E-Doc. Structure";
+ CrMemoNo: Code[20];
+ begin
+ // [SCENARIO] An item charge of a posted sales credit memo that is assigned to more than one credit memo line becomes a document level allowance/charge.
+ Initialize();
+
+ // [GIVEN] A posted sales credit memo with two item lines and an item charge assigned to both of them
+ CreateCustomerAndItem(Customer, Item);
+ CrMemoNo := CreateAndPostCrMemoWithChargeAssignedToItemLines(Customer, Item, 2);
+ GetPostedCrMemoLines(CrMemoNo, SalesCrMemoHeader, ChargeSalesCrMemoLine, ItemSalesCrMemoLine);
+ InitService(EDocumentService, EDocumentService."Item Charge E-Invoice Mapping"::Automatic);
+
+ // [WHEN] The item charge line is classified
+ Structure := EDocItemChargeMapping.GetItemChargeStructure(EDocumentService, SalesCrMemoHeader, ChargeSalesCrMemoLine, TargetSalesCrMemoLine);
+
+ // [THEN] The charge is a document level allowance/charge without a target line
+ Assert.AreEqual(Structure::"Document Allowance/Charge", Structure, 'A credit memo charge assigned to two lines must become a document level allowance/charge.');
+ Assert.AreEqual(0, TargetSalesCrMemoLine."Line No.", 'No target line must be returned for a document level allowance/charge.');
+ end;
+
+ #endregion
+
+ #region Helpers
+
+ local procedure Initialize()
+ var
+ InventorySetup: Record "Inventory Setup";
+ begin
+ if IsInitialized then
+ exit;
+
+ LibrarySales.SetStockoutWarning(false);
+ LibrarySales.SetCreditWarningsToNoWarnings();
+ LibrarySales.SetCalcInvDiscount(false);
+ InventorySetup.Get();
+ InventorySetup.Validate("Prevent Negative Inventory", false);
+ InventorySetup.Modify(true);
+
+ IsInitialized := true;
+ end;
+
+ local procedure InitService(var EDocumentService: Record "E-Document Service"; ItemChargeMapping: Enum "Item Charge E-Invoice Mapping")
+ begin
+ EDocumentService.Init();
+ EDocumentService.Code := 'ITEMCHARGE';
+ EDocumentService."Item Charge E-Invoice Mapping" := ItemChargeMapping;
+ end;
+
+ local procedure CreateCustomerAndItem(var Customer: Record Customer; var Item: Record Item)
+ begin
+ LibrarySales.CreateCustomer(Customer);
+ LibraryInventory.CreateItem(Item);
+ end;
+
+ local procedure GetVATRate(Customer: Record Customer; Item: Record Item): Decimal
+ var
+ VATPostingSetup: Record "VAT Posting Setup";
+ begin
+ VATPostingSetup.Get(Customer."VAT Bus. Posting Group", Item."VAT Prod. Posting Group");
+ exit(VATPostingSetup."VAT %");
+ end;
+
+ local procedure CreateVATProdPostingGroupWithRate(Customer: Record Customer; Item: Record Item; VATRate: Decimal): Code[20]
+ var
+ VATPostingSetup: Record "VAT Posting Setup";
+ begin
+ CreateVATPostingSetup(VATPostingSetup, Customer, Item, VATPostingSetup."VAT Calculation Type"::"Normal VAT", VATRate);
+ exit(VATPostingSetup."VAT Prod. Posting Group");
+ end;
+
+ local procedure CreateReverseChargeVATProdPostingGroup(Customer: Record Customer; Item: Record Item; VATRate: Decimal): Code[20]
+ var
+ VATPostingSetup: Record "VAT Posting Setup";
+ begin
+ CreateVATPostingSetup(VATPostingSetup, Customer, Item, VATPostingSetup."VAT Calculation Type"::"Reverse Charge VAT", VATRate);
+ exit(VATPostingSetup."VAT Prod. Posting Group");
+ end;
+
+ local procedure CreateVATPostingSetup(var VATPostingSetup: Record "VAT Posting Setup"; Customer: Record Customer; Item: Record Item; VATCalculationType: Enum "Tax Calculation Type"; VATRate: Decimal)
+ var
+ ItemVATPostingSetup: Record "VAT Posting Setup";
+ VATProductPostingGroup: Record "VAT Product Posting Group";
+ begin
+ ItemVATPostingSetup.Get(Customer."VAT Bus. Posting Group", Item."VAT Prod. Posting Group");
+ LibraryERM.CreateVATProductPostingGroup(VATProductPostingGroup);
+ LibraryERM.CreateVATPostingSetup(VATPostingSetup, Customer."VAT Bus. Posting Group", VATProductPostingGroup.Code);
+ VATPostingSetup."VAT Identifier" := VATProductPostingGroup.Code;
+ VATPostingSetup.Validate("VAT Calculation Type", VATCalculationType);
+ VATPostingSetup.Validate("VAT %", VATRate);
+ VATPostingSetup.Validate("Sales VAT Account", ItemVATPostingSetup."Sales VAT Account");
+ VATPostingSetup.Validate("Purchase VAT Account", ItemVATPostingSetup."Purchase VAT Account");
+ VATPostingSetup.Validate("Reverse Chrg. VAT Acc.", ItemVATPostingSetup."Purchase VAT Account");
+ VATPostingSetup.Modify(true);
+ end;
+
+ local procedure SetItemChargeMapping(ItemChargeNo: Code[20]; MappingOverride: Enum "Item Charge Mapping Override")
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(ItemChargeNo);
+ ItemCharge."E-Invoice Mapping" := MappingOverride;
+ ItemCharge.Modify(false);
+ end;
+
+ local procedure CreateItemChargeNo(Item: Record Item; VATProdPostingGroupCode: Code[20]): Code[20]
+ var
+ ItemCharge: Record "Item Charge";
+ begin
+ ItemCharge.Get(LibraryInventory.CreateItemChargeNo());
+ ItemCharge.Validate("Gen. Prod. Posting Group", Item."Gen. Prod. Posting Group");
+ ItemCharge.Validate("VAT Prod. Posting Group", VATProdPostingGroupCode);
+ ItemCharge.Modify(true);
+ exit(ItemCharge."No.");
+ end;
+
+ local procedure CreateAndPostShipmentOnly(Customer: Record Customer; Item: Record Item): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ SalesLine: Record "Sales Line";
+ SalesShipmentHeader: Record "Sales Shipment Header";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
+ CreateItemLine(SalesLine, SalesHeader, Item);
+ LibrarySales.PostSalesDocument(SalesHeader, true, false);
+
+ SalesShipmentHeader.SetRange("Order No.", SalesHeader."No.");
+ SalesShipmentHeader.FindFirst();
+ exit(SalesShipmentHeader."No.");
+ end;
+
+ local procedure CreateAndPostOrderWithChargeAssignedToItemLine(Customer: Record Customer; Item: Record Item): Code[20]
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ ItemSalesLine: Record "Sales Line";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
+ CreateItemLine(ItemSalesLine, SalesHeader, Item);
+
+ LibrarySales.CreateSalesLine(
+ ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", CreateItemChargeNo(Item, Item."VAT Prod. Posting Group"), 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Modify(true);
+
+ LibraryInventory.CreateItemChargeAssignment(
+ ItemChargeAssignmentSales, ChargeSalesLine, SalesHeader."Document Type", SalesHeader."No.", ItemSalesLine."Line No.", Item."No.");
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", 1);
+ ItemChargeAssignmentSales.Modify(true);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostInvoiceWithChargeAssignedToItemLines(Customer: Record Customer; Item: Record Item; VATProdPostingGroupCode: Code[20]; NoOfItemLines: Integer): Code[20]
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ ItemSalesLine: Record "Sales Line";
+ ItemLineNo: array[2] of Integer;
+ Index: Integer;
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No.");
+ for Index := 1 to NoOfItemLines do begin
+ CreateItemLine(ItemSalesLine, SalesHeader, Item);
+ ItemLineNo[Index] := ItemSalesLine."Line No.";
+ end;
+
+ LibrarySales.CreateSalesLine(
+ ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", CreateItemChargeNo(Item, VATProdPostingGroupCode), NoOfItemLines);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Modify(true);
+
+ for Index := 1 to NoOfItemLines do begin
+ LibraryInventory.CreateItemChargeAssignment(
+ ItemChargeAssignmentSales, ChargeSalesLine, SalesHeader."Document Type", SalesHeader."No.", ItemLineNo[Index], Item."No.");
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", 1);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostInvoiceWithChargeAssignedToShipment(Customer: Record Customer; Item: Record Item; ShipmentNo: Code[20]; WithItemLine: Boolean): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ ItemSalesLine: Record "Sales Line";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No.");
+ if WithItemLine then
+ CreateItemLine(ItemSalesLine, SalesHeader, Item);
+
+ LibrarySales.CreateSalesLine(
+ ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", CreateItemChargeNo(Item, Item."VAT Prod. Posting Group"), 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Modify(true);
+
+ AssignItemChargeToShipment(ChargeSalesLine, ShipmentNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure AssignItemChargeToShipment(ChargeSalesLine: Record "Sales Line"; ShipmentNo: Code[20])
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ SalesShipmentLine: Record "Sales Shipment Line";
+ ItemChargeAssgntSales: Codeunit "Item Charge Assgnt. (Sales)";
+ begin
+ ItemChargeAssignmentSales.Init();
+ ItemChargeAssignmentSales.Validate("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.Validate("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.Validate("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.Validate("Item Charge No.", ChargeSalesLine."No.");
+ ItemChargeAssignmentSales.Validate("Unit Cost", ChargeSalesLine."Unit Price");
+ SalesShipmentLine.SetRange("Document No.", ShipmentNo);
+ SalesShipmentLine.FindFirst();
+ ItemChargeAssgntSales.CreateShptChargeAssgnt(SalesShipmentLine, ItemChargeAssignmentSales);
+
+ ItemChargeAssignmentSales.SetRange("Document Type", ChargeSalesLine."Document Type");
+ ItemChargeAssignmentSales.SetRange("Document No.", ChargeSalesLine."Document No.");
+ ItemChargeAssignmentSales.SetRange("Document Line No.", ChargeSalesLine."Line No.");
+ ItemChargeAssignmentSales.FindFirst();
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", ChargeSalesLine.Quantity);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ local procedure CreateItemLine(var SalesLine: Record "Sales Line"; SalesHeader: Record "Sales Header"; Item: Record Item)
+ begin
+ LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
+ SalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(100, 200, 2));
+ SalesLine.Modify(true);
+ end;
+
+ local procedure GetPostedLines(InvoiceNo: Code[20]; var SalesInvoiceHeader: Record "Sales Invoice Header"; var ChargeSalesInvoiceLine: Record "Sales Invoice Line"; var ItemSalesInvoiceLine: Record "Sales Invoice Line")
+ begin
+ SalesInvoiceHeader.Get(InvoiceNo);
+
+ ChargeSalesInvoiceLine.SetRange("Document No.", InvoiceNo);
+ ChargeSalesInvoiceLine.SetRange(Type, ChargeSalesInvoiceLine.Type::"Charge (Item)");
+ ChargeSalesInvoiceLine.FindFirst();
+
+ Clear(ItemSalesInvoiceLine);
+ ItemSalesInvoiceLine.SetRange("Document No.", InvoiceNo);
+ ItemSalesInvoiceLine.SetRange(Type, ItemSalesInvoiceLine.Type::Item);
+ if ItemSalesInvoiceLine.FindFirst() then;
+ end;
+
+ local procedure CreateAndPostCrMemoWithChargeAssignedToItemLines(Customer: Record Customer; Item: Record Item; NoOfItemLines: Integer): Code[20]
+ var
+ ItemChargeAssignmentSales: Record "Item Charge Assignment (Sales)";
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ ItemSalesLine: Record "Sales Line";
+ ItemLineNo: array[2] of Integer;
+ Index: Integer;
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Credit Memo", Customer."No.");
+ for Index := 1 to NoOfItemLines do begin
+ CreateItemLine(ItemSalesLine, SalesHeader, Item);
+ ItemLineNo[Index] := ItemSalesLine."Line No.";
+ end;
+
+ LibrarySales.CreateSalesLine(
+ ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", CreateItemChargeNo(Item, Item."VAT Prod. Posting Group"), NoOfItemLines);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Modify(true);
+
+ for Index := 1 to NoOfItemLines do begin
+ LibraryInventory.CreateItemChargeAssignment(
+ ItemChargeAssignmentSales, ChargeSalesLine, SalesHeader."Document Type", SalesHeader."No.", ItemLineNo[Index], Item."No.");
+ ItemChargeAssignmentSales.Validate("Qty. to Assign", 1);
+ ItemChargeAssignmentSales.Modify(true);
+ end;
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure CreateAndPostCrMemoWithChargeAssignedToShipment(Customer: Record Customer; Item: Record Item; ShipmentNo: Code[20]): Code[20]
+ var
+ SalesHeader: Record "Sales Header";
+ ChargeSalesLine: Record "Sales Line";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Credit Memo", Customer."No.");
+
+ LibrarySales.CreateSalesLine(
+ ChargeSalesLine, SalesHeader, ChargeSalesLine.Type::"Charge (Item)", CreateItemChargeNo(Item, Item."VAT Prod. Posting Group"), 1);
+ ChargeSalesLine.Validate("Unit Price", LibraryRandom.RandDecInRange(10, 50, 2));
+ ChargeSalesLine.Modify(true);
+
+ AssignItemChargeToShipment(ChargeSalesLine, ShipmentNo);
+
+ exit(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure GetPostedCrMemoLines(CrMemoNo: Code[20]; var SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var ChargeSalesCrMemoLine: Record "Sales Cr.Memo Line"; var ItemSalesCrMemoLine: Record "Sales Cr.Memo Line")
+ begin
+ SalesCrMemoHeader.Get(CrMemoNo);
+
+ ChargeSalesCrMemoLine.SetRange("Document No.", CrMemoNo);
+ ChargeSalesCrMemoLine.SetRange(Type, ChargeSalesCrMemoLine.Type::"Charge (Item)");
+ ChargeSalesCrMemoLine.FindFirst();
+
+ Clear(ItemSalesCrMemoLine);
+ ItemSalesCrMemoLine.SetRange("Document No.", CrMemoNo);
+ ItemSalesCrMemoLine.SetRange(Type, ItemSalesCrMemoLine.Type::Item);
+ if ItemSalesCrMemoLine.FindFirst() then;
+ end;
+
+ #endregion
+}