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