diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index 41bcf312216..4feff711c4d 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -166,6 +166,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 659e2e7f862..5bbbd9da32e 100644 --- a/src/Apps/W1/EDocument/App/src/Service/EDocumentService.Table.al +++ b/src/Apps/W1/EDocument/App/src/Service/EDocumentService.Table.al @@ -295,6 +295,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 ba607b422a4..54b4fcb3d26 100644 --- a/src/Apps/W1/EDocument/App/src/Service/EdocumentService.Page.al +++ b/src/Apps/W1/EDocument/App/src/Service/EdocumentService.Page.al @@ -191,6 +191,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 +}