From 8721e6ea72ec5078c2e240da6c3fe98cfc0d3055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Sat, 5 Sep 2026 17:14:12 +0000 Subject: [PATCH] [Bug]: [DE] XRechnung/ZUGFeRD - Item Charge lines exported without a valid unit of measure code (BR-CL-23 / BR-23 on BT-130) (#9860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Item charge lines (Type = Charge (Item)) were exported as synthetic invoice lines carrying an empty unit code or PCE on BT-130, which fails EN 16931 validation with BR-23 and BR-CL-23. Semantically most item charges are allowances or charges and belong in the allowance/charge structures rather than in a fabricated invoice line. E-Document Core gains the classification: - Enum "Item Charge E-Invoice Mapping" with Automatic (default), Document Allowance/Charge, Line Allowance/Charge and Line with Unit Code, held on E-Document Service. - A per-item-charge override (mapping, reason text, reason code, fallback unit code) on a table extension of Item Charge. - Codeunit "E-Doc. Item Charge Mapping" resolves the structure for posted sales invoices and credit memos. Automatic exports a charge as line-level when it is assigned to exactly one line whose VAT calculation type and rate match, as document-level when the assignment is absent or ambiguous, and as a regular line otherwise. The assignment is recovered from Value Entry, because item charge assignments no longer exist once the document is posted. - Integration events let subscribers override the resolved structure. The controls are hidden in Core. E-Document for Germany makes them visible, and both German exporters consume the classification: - XRechnung emits cac:AllowanceCharge under Invoice or inside the target cac:InvoiceLine; ZUGFeRD emits ram:SpecifiedTradeAllowanceCharge in the header or line settlement. - A document-level allowance/charge carries its tax category, a line-level one does not. - Negative amounts are exported as allowances with a positive amount. - The fallback line reports quantity 1 with unit code C62; a negative charge uses a negative quantity with a non-negative net price so that BR-27 holds. - A document whose only lines are item charges keeps a regular line, so BR-16 is not violated. Document and line totals are corrected so that the monetary totals and tax subtotals still add up once charges leave the line set. Also removes a duplicate "features" key from the DE demo data manifest, which prevented that app from compiling. Fixes #8417 [AB#641600](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/641600) - [x] I read the full diff and it contains only changes I intended. - [x] I built the affected app(s) locally with no new analyzer warnings. - [x] I ran the change in Business Central and confirmed it behaves as expected. - [x] I added or updated tests for the new behavior, or explained below why none are needed. **What I tested and the outcome** *(required — be specific: scenarios, commands, screenshots for UI changes)* Manual, against a BC 29 DE container (bc29de), validating every exported document with online EN 16931 / XRechnung validators (KoSIT): - Exported a posted sales invoice with item charges under **all four** values of the new **Item Charge Mapping** service setting — Automatic, Document Level Allowance/Charge, Invoice Line Allowance/Charge and Invoice Line with Unit Code. All four produce documents that pass XSD, EN 16931 Schematron and the XRechnung CIUS rules. - Overrode the setting per item charge on the **Item Charges** page and confirmed the per-charge **E-Invoice Mapping** takes precedence over the service-level setting, including a blank override falling through to the service setting. Automated (all executed against the container, not just compiled): | Test codeunit | Result | |---|---| | `E-Doc. Item Charge Tests` (139520) | 27 / 0 | | `E-Doc. Item Charge UI Tests` (139522) | 3 / 0 | | `Item Charge UI DE Tests` (13927) | 5 / 0 | | `XRechnung XML Document Tests` (13918) | 93 / 2 | | `ZUGFeRD XML Document Tests` (13922) | 93 / 2 | 73 tests were added in total. The 2 + 2 failures are **pre-existing and unrelated** — `AdditionalDocumentReference` counts in two attachment tests, and `ram:GlobalID` in two ZUGFeRD tests. They were measured as failing on an unmodified tree before this work started, and the failure sets are unchanged by this PR. Coverage was verified by mutation: each classification branch, the sign convention, the totals corrections and the fallback quantity/unit code were deliberately broken and confirmed to fail a specific test. Two genuine bugs were caught this way — a signed amount reaching `AllowanceCharge`, and a missing guard that let a charge-only document export zero invoice lines (BR-16). - **Behaviour changes by default.** `Automatic` is the default mapping, so existing services start exporting item charges as document- or line-level allowances/charges instead of synthetic invoice lines. This is the point of the fix — the old output was invalid — but the XML shape changes for anyone already exporting item charges, and downstream systems that parse charges out of `cac:InvoiceLine` / `ram:IncludedSupplyChainTradeLineItem` will see them move. Reviewers should confirm this is acceptable as a default rather than opt-in. - **Schema change.** New field on `E-Document Service`, and a table extension on `Item Charge` with four new fields. Requires a schema sync; no data migration, all fields default to blank/`Automatic`, which preserves the intended behaviour. - **Monetary totals are recomputed.** `cac:LegalMonetaryTotal` / `ram:...HeaderMonetarySummation` and the tax subtotals are adjusted when charges leave the line set. Tax base and payable amounts are unchanged by construction and asserted in tests, but this is the area most worth a careful read. - **Known limitation:** a PEPPOL BIS 3.0 DE service shows the Item Charge Mapping setting, but that format's export ignores it — it delegates XML generation to the W1 PEPPOL BIS 3.0 implementation, which is deliberately out of scope here. Follow-up work if PEPPOL BIS DE should honour the setting. - **Deliberately out of scope:** the generic W1 PEPPOL BIS 3.0 export is untouched, and service documents keep opting out (they cannot carry item charges). - **New public surface:** integration events allow subscribers to override the resolved structure for invoices and credit memos. Once shipped these are a compatibility commitment. - **Permissions:** the new `E-Doc. Item Charge Mapping` codeunit is granted execute in `E-Doc. Core - Objects`, which flows to Read/User/Basic/Edit/Admin and the D365 permission set extensions. The codeunit itself elevates the table reads it needs (`Item Charge`, `Value Entry`, posted sales lines) via its `Permissions` property. --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: djukicmilica --- .../app/src/EDocItemChargesDE.PageExt.al | 30 + .../app/src/EDocumentDEHelper.Codeunit.al | 21 + .../app/src/EDocumentServiceDE.PageExt.al | 5 + .../ExportXRechnungDocument.Codeunit.al | 516 +++++- .../ZUGFeRD/ExportZUGFeRDDocument.Codeunit.al | 455 +++++- src/Apps/DE/EDocumentDE/demo data/app.json | 7 +- .../test/src/ItemChargeUIDETests.Codeunit.al | 162 ++ .../src/XRechnungXMLDocumentTests.Codeunit.al | 1391 +++++++++++++++++ .../src/ZUGFeRDXMLDocumentTests.Codeunit.al | 1382 +++++++++++++++- .../EDocCoreObjects.PermissionSet.al | 1 + .../src/Extensions/EDocItemCharge.TableExt.al | 38 + .../src/Extensions/EDocItemCharges.PageExt.al | 42 + .../ItemChargeMappingOverride.Enum.al | 45 + .../EDocItemChargeMapping.Codeunit.al | 378 +++++ .../ItemChargeEDocStructure.Enum.al | 27 + .../App/src/Service/EDocumentService.Table.al | 6 + .../App/src/Service/EdocumentService.Page.al | 9 + .../Service/ItemChargeEInvoiceMapping.Enum.al | 39 + .../Mock/EDocItemChrgSubscriber.Codeunit.al | 35 + .../EDocItemChargeSetupTests.Codeunit.al | 99 ++ .../EDocItemChargeTests.Codeunit.al | 1239 +++++++++++++++ 21 files changed, 5873 insertions(+), 54 deletions(-) create mode 100644 src/Apps/DE/EDocumentDE/app/src/EDocItemChargesDE.PageExt.al create mode 100644 src/Apps/DE/EDocumentDE/test/src/ItemChargeUIDETests.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharge.TableExt.al create mode 100644 src/Apps/W1/EDocument/App/src/Extensions/EDocItemCharges.PageExt.al create mode 100644 src/Apps/W1/EDocument/App/src/Extensions/ItemChargeMappingOverride.Enum.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/ItemChargeEDocStructure.Enum.al create mode 100644 src/Apps/W1/EDocument/App/src/Service/ItemChargeEInvoiceMapping.Enum.al create mode 100644 src/Apps/W1/EDocument/Test/src/Mock/EDocItemChrgSubscriber.Codeunit.al create mode 100644 src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeSetupTests.Codeunit.al create mode 100644 src/Apps/W1/EDocument/Test/src/Processing/EDocItemChargeTests.Codeunit.al 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 +}