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