diff --git a/docs/superpowers/plans/2026-05-26-mllm-v2-implementation.md b/docs/superpowers/plans/2026-05-26-mllm-v2-implementation.md
new file mode 100644
index 0000000000..a73b481db7
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-26-mllm-v2-implementation.md
@@ -0,0 +1,1439 @@
+# MLLM V2 Agentic Extraction Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the single-pass MLLM extraction with an agentic plan-act-verify loop where the model identifies document structure, extracts from identified regions, and self-corrects by calling AL-implemented verification tools.
+
+**Architecture:** One agentic AOAI call loop (GPT-4.1 Mini) with 6 verification tools registered as AOAI Functions. The model plans (chain-of-thought), extracts, calls verify tools, sees failures with error details, corrects, and repeats. AL drives the tool dispatch loop; the model decides when to call what. New handler registered as `"MLLM V2"` enum value alongside V1.
+
+**Tech Stack:** AL (Business Central), System.AI (AOAI SDK), `AOAI Function` interface for tool adapters, `AOAIChatMessages.AddTool()` + `AppendFunctionResponsesToChatMessages()` for the loop.
+
+---
+
+## File Map
+
+| Action | Path | ID | Responsibility |
+|--------|------|----|----------------|
+| Modify | `App/src/Processing/Import/StructureReceivedEDoc.Enum.al` | enum 6103 | Add `"MLLM V2"` value |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyTools.Codeunit.al` | 6233 | All 6 verification logic methods |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyLineMathTool.Codeunit.al` | 6235 | `verify_line_math` AOAI Function adapter |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyTotalsTool.Codeunit.al` | 6236 | `verify_invoice_totals` AOAI Function adapter |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyVATTool.Codeunit.al` | 6237 | `verify_vat` AOAI Function adapter |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyDatesTool.Codeunit.al` | 6238 | `verify_dates` AOAI Function adapter |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyRequiredTool.Codeunit.al` | 6239 | `verify_required_fields` AOAI Function adapter |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyRangesTool.Codeunit.al` | 6243 | `verify_ranges` AOAI Function adapter |
+| Create | `App/.resources/Prompts/EDocMLLMExtractionV2-SystemPrompt.md` | — | Three-section V2 system prompt |
+| Create | `App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandlerV2.Codeunit.al` | 6244 | V2 handler: agentic loop + interface impl |
+| Create | `Test/src/Processing/EDocMLLMVerifyToolsTests.Codeunit.al` | 135648 | Unit tests for all 6 verify methods |
+
+All paths are relative to `src/Apps/W1/EDocument/`.
+
+---
+
+## Task 1: Register the "MLLM V2" enum value
+
+**Files:**
+- Modify: `src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al`
+
+- [ ] **Step 1: Add enum value**
+
+Open the file. After the `"MLLM"` value (value 3), add:
+
+```al
+ value(4; "MLLM V2")
+ {
+ Caption = 'MLLM Extraction V2';
+ Implementation = IStructureReceivedEDocument = "E-Document MLLM Handler V2";
+ }
+```
+
+The handler codeunit name `"E-Document MLLM Handler V2"` will be created in Task 5.
+
+- [ ] **Step 2: Verify file compiles**
+
+The file will not compile until Task 5 creates the referenced codeunit. That is expected — this step just records the intent. Come back and verify compilation after Task 5.
+
+---
+
+## Task 2: Create EDocMLLMVerifyTools with unit tests
+
+The core verification logic. No AOAI calls — pure math and validation. Test this codeunit thoroughly before building the tool adapters on top.
+
+**Files:**
+- Create: `src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyTools.Codeunit.al`
+- Create: `src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMVerifyToolsTests.Codeunit.al`
+
+- [ ] **Step 1: Create the stub codeunit**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+codeunit 6233 "E-Doc. MLLM Verify Tools"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ ///
+ /// Checks unit_price × quantity × (1 − discount_pct/100) ≈ line_extension_amount within 1%.
+ /// Returns false and sets ErrorText when the check fails.
+ ///
+ procedure VerifyLineMath(UnitPrice: Decimal; Quantity: Decimal; DiscountPct: Decimal; LineExtensionAmount: Decimal; var ErrorText: Text): Boolean
+ begin
+ exit(false); // stub
+ end;
+
+ ///
+ /// Checks sum(LineAmounts) ≈ TaxExclusiveAmount within 1%.
+ ///
+ procedure VerifyInvoiceTotals(LineAmounts: List of [Decimal]; TaxExclusiveAmount: Decimal; var ErrorText: Text): Boolean
+ begin
+ exit(false); // stub
+ end;
+
+ ///
+ /// Checks TaxExclusiveAmount × VATRate/100 ≈ TaxAmount within 1%.
+ ///
+ procedure VerifyVAT(TaxExclusiveAmount: Decimal; VATRate: Decimal; TaxAmount: Decimal; var ErrorText: Text): Boolean
+ begin
+ exit(false); // stub
+ end;
+
+ ///
+ /// Validates issue_date and due_date are parseable XML dates, year 1900-2100, due_date >= issue_date.
+ ///
+ procedure VerifyDates(IssueDateText: Text; DueDateText: Text; var ErrorText: Text): Boolean
+ begin
+ exit(false); // stub
+ end;
+
+ ///
+ /// Checks VendorName, InvoiceNo are non-empty and LineCount > 0.
+ ///
+ procedure VerifyRequiredFields(VendorName: Text; InvoiceNo: Text; LineCount: Integer; var ErrorText: Text): Boolean
+ begin
+ exit(false); // stub
+ end;
+
+ ///
+ /// Checks quantities and prices > 0, VAT rates and discount percentages 0-100.
+ /// Stops at first violation and reports the line index.
+ ///
+ procedure VerifyRanges(Quantities: List of [Decimal]; Prices: List of [Decimal]; VATRates: List of [Decimal]; DiscountPcts: List of [Decimal]; var ErrorText: Text): Boolean
+ begin
+ exit(false); // stub
+ end;
+
+ internal procedure IsWithinTolerance(Expected: Decimal; Actual: Decimal): Boolean
+ var
+ Denominator: Decimal;
+ begin
+ Denominator := Abs(Actual);
+ if Denominator < 1 then
+ Denominator := 1;
+ exit(Abs(Expected - Actual) / Denominator < 0.01);
+ end;
+}
+```
+
+- [ ] **Step 2: Write the test codeunit**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+codeunit 135648 "EDoc MLLM Verify Tools Tests"
+{
+ Subtype = Test;
+
+ var
+ Assert: Codeunit Assert;
+
+ // VerifyLineMath -----------------------------------------------------------
+
+ [Test]
+ procedure VerifyLineMath_Pass_NoDiscount()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // 5 × 40 × 1.00 = 200
+ Assert.IsTrue(VerifyTools.VerifyLineMath(40, 5, 0, 200, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Pass_WithDiscount()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // 3.65 × 1083 × 0.64 = 2529.888 ≈ 2529.89
+ Assert.IsTrue(VerifyTools.VerifyLineMath(3.65, 1083, 36, 2529.89, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Pass_WithinOnePct()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // Expected = 100, actual = 100.9 — within 1%
+ Assert.IsTrue(VerifyTools.VerifyLineMath(10, 10, 0, 100.9, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Fail_WrongPrice()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // net price 2.34 used instead of gross 3.65 with 20% discount → expected 2027, actual 2529
+ Assert.IsFalse(VerifyTools.VerifyLineMath(2.34, 1083, 20, 2529.89, ErrorText), 'Should fail');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be set');
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Pass_ZeroLineAmount()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // Zero line amount — skip check (freight lines sometimes have zero amount)
+ Assert.IsTrue(VerifyTools.VerifyLineMath(0, 0, 0, 0, ErrorText), ErrorText);
+ end;
+
+ // VerifyInvoiceTotals ------------------------------------------------------
+
+ [Test]
+ procedure VerifyInvoiceTotals_Pass()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Lines: List of [Decimal];
+ ErrorText: Text;
+ begin
+ Lines.Add(200);
+ Lines.Add(30);
+ Lines.Add(20);
+ Assert.IsTrue(VerifyTools.VerifyInvoiceTotals(Lines, 250, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyInvoiceTotals_Fail_MissingLine()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Lines: List of [Decimal];
+ ErrorText: Text;
+ begin
+ Lines.Add(200);
+ // missing 30 + 20
+ Assert.IsFalse(VerifyTools.VerifyInvoiceTotals(Lines, 250, ErrorText), 'Should fail');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be set');
+ end;
+
+ // VerifyVAT ----------------------------------------------------------------
+
+ [Test]
+ procedure VerifyVAT_Pass()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // 250 × 15% = 37.5
+ Assert.IsTrue(VerifyTools.VerifyVAT(250, 15, 37.5, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyVAT_Fail()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyVAT(250, 15, 100, ErrorText), 'Should fail');
+ end;
+
+ [Test]
+ procedure VerifyVAT_Skip_ZeroTax()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // tax_amount = 0 means zero-rated — skip check
+ Assert.IsTrue(VerifyTools.VerifyVAT(250, 0, 0, ErrorText), ErrorText);
+ end;
+
+ // VerifyDates --------------------------------------------------------------
+
+ [Test]
+ procedure VerifyDates_Pass_ValidDates()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsTrue(VerifyTools.VerifyDates('2024-03-15', '2024-04-15', ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyDates_Pass_NoDueDate()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsTrue(VerifyTools.VerifyDates('2024-03-15', '', ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyDates_Fail_DueDateBeforeIssue()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyDates('2024-04-15', '2024-03-15', ErrorText), 'Should fail');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be set');
+ end;
+
+ [Test]
+ procedure VerifyDates_Fail_InvalidDate()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyDates('not-a-date', '', ErrorText), 'Should fail');
+ end;
+
+ [Test]
+ procedure VerifyDates_Fail_MissingIssueDate()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyDates('', '', ErrorText), 'Should fail');
+ end;
+
+ // VerifyRequiredFields -----------------------------------------------------
+
+ [Test]
+ procedure VerifyRequiredFields_Pass()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsTrue(VerifyTools.VerifyRequiredFields('Contoso Ltd', 'INV-001', 2, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyRequiredFields_Fail_MissingVendor()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyRequiredFields('', 'INV-001', 2, ErrorText), 'Should fail');
+ Assert.IsSubstring(ErrorText, 'vendor');
+ end;
+
+ [Test]
+ procedure VerifyRequiredFields_Fail_NoLines()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyRequiredFields('Contoso Ltd', 'INV-001', 0, ErrorText), 'Should fail');
+ Assert.IsSubstring(ErrorText, 'line');
+ end;
+
+ // VerifyRanges -------------------------------------------------------------
+
+ [Test]
+ procedure VerifyRanges_Pass()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Qtys: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscPcts: List of [Decimal];
+ ErrorText: Text;
+ begin
+ Qtys.Add(5); Prices.Add(40); VATRates.Add(15); DiscPcts.Add(0);
+ Assert.IsTrue(VerifyTools.VerifyRanges(Qtys, Prices, VATRates, DiscPcts, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyRanges_Fail_NegativeQty()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Qtys: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscPcts: List of [Decimal];
+ ErrorText: Text;
+ begin
+ Qtys.Add(-1); Prices.Add(40); VATRates.Add(15); DiscPcts.Add(0);
+ Assert.IsFalse(VerifyTools.VerifyRanges(Qtys, Prices, VATRates, DiscPcts, ErrorText), 'Should fail');
+ Assert.IsSubstring(ErrorText, 'quantity');
+ end;
+
+ [Test]
+ procedure VerifyRanges_Fail_DiscountOver100()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Qtys: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscPcts: List of [Decimal];
+ ErrorText: Text;
+ begin
+ Qtys.Add(5); Prices.Add(40); VATRates.Add(15); DiscPcts.Add(150);
+ Assert.IsFalse(VerifyTools.VerifyRanges(Qtys, Prices, VATRates, DiscPcts, ErrorText), 'Should fail');
+ Assert.IsSubstring(ErrorText, 'discount');
+ end;
+
+ // IsWithinTolerance --------------------------------------------------------
+
+ [Test]
+ procedure IsWithinTolerance_Pass_ExactMatch()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ begin
+ Assert.IsTrue(VerifyTools.IsWithinTolerance(100, 100), 'Exact match');
+ end;
+
+ [Test]
+ procedure IsWithinTolerance_Pass_SmallDelta()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ begin
+ Assert.IsTrue(VerifyTools.IsWithinTolerance(2529.888, 2529.89), 'Rounding delta');
+ end;
+
+ [Test]
+ procedure IsWithinTolerance_Fail_LargeDelta()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ begin
+ Assert.IsFalse(VerifyTools.IsWithinTolerance(2027, 2529.89), 'Wrong value');
+ end;
+}
+```
+
+- [ ] **Step 3: Implement the 6 methods in EDocMLLMVerifyTools**
+
+Replace the stub body of each method with the real implementation:
+
+```al
+procedure VerifyLineMath(UnitPrice: Decimal; Quantity: Decimal; DiscountPct: Decimal; LineExtensionAmount: Decimal; var ErrorText: Text): Boolean
+var
+ Expected: Decimal;
+begin
+ if LineExtensionAmount = 0 then
+ exit(true);
+ Expected := UnitPrice * Quantity * (1 - DiscountPct / 100);
+ if IsWithinTolerance(Expected, LineExtensionAmount) then
+ exit(true);
+ ErrorText := StrSubstNo('%1 × %2 × (1 − %3/100) = %4, but line_extension_amount = %5. Re-check which price column is the gross (pre-discount) unit price.',
+ UnitPrice, Quantity, DiscountPct, Round(Expected, 0.01), LineExtensionAmount);
+ exit(false);
+end;
+
+procedure VerifyInvoiceTotals(LineAmounts: List of [Decimal]; TaxExclusiveAmount: Decimal; var ErrorText: Text): Boolean
+var
+ LineAmount: Decimal;
+ Sum: Decimal;
+begin
+ if TaxExclusiveAmount = 0 then
+ exit(true);
+ foreach LineAmount in LineAmounts do
+ Sum += LineAmount;
+ if IsWithinTolerance(Sum, TaxExclusiveAmount) then
+ exit(true);
+ ErrorText := StrSubstNo('Sum of line_extension_amounts = %1, but tax_exclusive_amount = %2. Check for missing or duplicated lines.',
+ Round(Sum, 0.01), TaxExclusiveAmount);
+ exit(false);
+end;
+
+procedure VerifyVAT(TaxExclusiveAmount: Decimal; VATRate: Decimal; TaxAmount: Decimal; var ErrorText: Text): Boolean
+var
+ Expected: Decimal;
+begin
+ if TaxAmount = 0 then
+ exit(true);
+ Expected := TaxExclusiveAmount * VATRate / 100;
+ if IsWithinTolerance(Expected, TaxAmount) then
+ exit(true);
+ ErrorText := StrSubstNo('%1 × %2% = %3, but tax_amount = %4. Re-check the VAT rate.',
+ TaxExclusiveAmount, VATRate, Round(Expected, 0.01), TaxAmount);
+ exit(false);
+end;
+
+procedure VerifyDates(IssueDateText: Text; DueDateText: Text; var ErrorText: Text): Boolean
+var
+ IssueDate: Date;
+ DueDate: Date;
+begin
+ if IssueDateText = '' then begin
+ ErrorText := 'issue_date is missing.';
+ exit(false);
+ end;
+ if not Evaluate(IssueDate, IssueDateText, 9) then begin
+ ErrorText := StrSubstNo('issue_date "%1" is not a valid XML date (expected YYYY-MM-DD).', IssueDateText);
+ exit(false);
+ end;
+ if (Date2DMY(IssueDate, 3) < 1900) or (Date2DMY(IssueDate, 3) > 2100) then begin
+ ErrorText := StrSubstNo('issue_date year %1 is out of expected range 1900–2100.', Date2DMY(IssueDate, 3));
+ exit(false);
+ end;
+ if DueDateText = '' then
+ exit(true);
+ if not Evaluate(DueDate, DueDateText, 9) then begin
+ ErrorText := StrSubstNo('due_date "%1" is not a valid XML date (expected YYYY-MM-DD).', DueDateText);
+ exit(false);
+ end;
+ if DueDate < IssueDate then begin
+ ErrorText := StrSubstNo('due_date %1 is before issue_date %2.', Format(DueDate, 0, 9), Format(IssueDate, 0, 9));
+ exit(false);
+ end;
+ exit(true);
+end;
+
+procedure VerifyRequiredFields(VendorName: Text; InvoiceNo: Text; LineCount: Integer; var ErrorText: Text): Boolean
+var
+ Missing: Text;
+begin
+ if VendorName = '' then
+ AppendMissing(Missing, 'vendor name');
+ if InvoiceNo = '' then
+ AppendMissing(Missing, 'invoice number');
+ if LineCount <= 0 then
+ AppendMissing(Missing, 'invoice lines (line_count = 0)');
+ if Missing <> '' then begin
+ ErrorText := 'Missing required fields: ' + Missing;
+ exit(false);
+ end;
+ exit(true);
+end;
+
+procedure VerifyRanges(Quantities: List of [Decimal]; Prices: List of [Decimal]; VATRates: List of [Decimal]; DiscountPcts: List of [Decimal]; var ErrorText: Text): Boolean
+var
+ i: Integer;
+ Value: Decimal;
+begin
+ for i := 1 to Quantities.Count() do begin
+ Quantities.Get(i, Value);
+ if Value <= 0 then begin
+ ErrorText := StrSubstNo('Line %1 quantity %2 must be > 0.', i, Value);
+ exit(false);
+ end;
+ end;
+ for i := 1 to Prices.Count() do begin
+ Prices.Get(i, Value);
+ if Value <= 0 then begin
+ ErrorText := StrSubstNo('Line %1 unit price %2 must be > 0.', i, Value);
+ exit(false);
+ end;
+ end;
+ for i := 1 to VATRates.Count() do begin
+ VATRates.Get(i, Value);
+ if (Value < 0) or (Value > 100) then begin
+ ErrorText := StrSubstNo('Line %1 VAT rate %2 must be between 0 and 100.', i, Value);
+ exit(false);
+ end;
+ end;
+ for i := 1 to DiscountPcts.Count() do begin
+ DiscountPcts.Get(i, Value);
+ if (Value < 0) or (Value > 100) then begin
+ ErrorText := StrSubstNo('Line %1 discount %2%% must be between 0 and 100.', i, Value);
+ exit(false);
+ end;
+ end;
+ exit(true);
+end;
+
+local procedure AppendMissing(var Missing: Text; Field: Text)
+begin
+ if Missing <> '' then
+ Missing += ', ';
+ Missing += Field;
+end;
+```
+
+
+## Task 3: Create the 6 AOAI Function tool adapters
+
+Each adapter is a thin codeunit implementing `AOAI Function`. It declares the tool's JSON schema in `GetPrompt()` and delegates to `EDocMLLMVerifyTools` in `Execute()`.
+
+**Files:** 6 new codeunits (6235–6239, 6243)
+
+- [ ] **Step 1: Create EDocMLLMVerifyLineMathTool.Codeunit.al (6235)**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6235 "E-Doc. MLLM VL Math Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_line_math');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'Gross unit price before discounts');
+ PropsObj.Add('unit_price', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'Quantity of units');
+ PropsObj.Add('quantity', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'Combined discount percentage 0-100 (use 0 if no discount)');
+ PropsObj.Add('discount_pct', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'line_extension_amount from the invoice');
+ PropsObj.Add('line_extension_amount', PropObj);
+ RequiredArr.Add('unit_price'); RequiredArr.Add('quantity'); RequiredArr.Add('discount_pct'); RequiredArr.Add('line_extension_amount');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that gross_unit_price × quantity × (1 − discount_pct/100) matches line_extension_amount within 1%. Call once per invoice line.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ UnitPrice: Decimal;
+ Quantity: Decimal;
+ DiscountPct: Decimal;
+ LineExtAmt: Decimal;
+ begin
+ GetDecimalArg(Arguments, 'unit_price', UnitPrice);
+ GetDecimalArg(Arguments, 'quantity', Quantity);
+ GetDecimalArg(Arguments, 'discount_pct', DiscountPct);
+ GetDecimalArg(Arguments, 'line_extension_amount', LineExtAmt);
+ if VerifyTools.VerifyLineMath(UnitPrice, Quantity, DiscountPct, LineExtAmt, ErrorText) then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ exit(ResultObj);
+ end;
+
+ local procedure GetDecimalArg(Arguments: JsonObject; PropertyName: Text; var Value: Decimal)
+ var
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ begin
+ if not Arguments.Get(PropertyName, Token) then
+ exit;
+ if Token.AsValue().IsNull() then
+ exit;
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then
+ Value := DecimalValue;
+ end;
+}
+```
+
+- [ ] **Step 2: Create EDocMLLMVerifyTotalsTool.Codeunit.al (6236)**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6236 "E-Doc. MLLM VL Totals Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_invoice_totals');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ ItemsObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line_extension_amount values');
+ PropsObj.Add('line_amounts', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_exclusive_amount from legal_monetary_total');
+ PropsObj.Add('tax_exclusive_amount', PropObj);
+ RequiredArr.Add('line_amounts'); RequiredArr.Add('tax_exclusive_amount');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that the sum of all line_extension_amounts matches tax_exclusive_amount within 1%.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ LineAmountsToken: JsonToken;
+ LineAmountsArray: JsonArray;
+ LineToken: JsonToken;
+ LineAmounts: List of [Decimal];
+ TaxExclusiveAmount: Decimal;
+ LineAmt: Decimal;
+ DecimalValue: Decimal;
+ Token: JsonToken;
+ begin
+ if Arguments.Get('line_amounts', LineAmountsToken) then begin
+ LineAmountsArray := LineAmountsToken.AsArray();
+ foreach LineToken in LineAmountsArray do begin
+ if Evaluate(DecimalValue, LineToken.AsValue().AsText(), 9) then
+ LineAmounts.Add(DecimalValue);
+ end;
+ end;
+ if Arguments.Get('tax_exclusive_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then
+ TaxExclusiveAmount := DecimalValue;
+
+ if VerifyTools.VerifyInvoiceTotals(LineAmounts, TaxExclusiveAmount, ErrorText) then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ exit(ResultObj);
+ end;
+}
+```
+
+- [ ] **Step 3: Create EDocMLLMVerifyVATTool.Codeunit.al (6237)**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6237 "E-Doc. MLLM VL VAT Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_vat');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_exclusive_amount');
+ PropsObj.Add('tax_exclusive_amount', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'VAT rate percentage 0-100');
+ PropsObj.Add('vat_rate', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_amount');
+ PropsObj.Add('tax_amount', PropObj);
+ RequiredArr.Add('tax_exclusive_amount'); RequiredArr.Add('vat_rate'); RequiredArr.Add('tax_amount');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that tax_exclusive_amount × vat_rate/100 ≈ tax_amount within 1%.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ TaxExcl: Decimal;
+ VATRate: Decimal;
+ TaxAmt: Decimal;
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ begin
+ if Arguments.Get('tax_exclusive_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then TaxExcl := DecimalValue;
+ if Arguments.Get('vat_rate', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then VATRate := DecimalValue;
+ if Arguments.Get('tax_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then TaxAmt := DecimalValue;
+
+ if VerifyTools.VerifyVAT(TaxExcl, VATRate, TaxAmt, ErrorText) then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ exit(ResultObj);
+ end;
+}
+```
+
+- [ ] **Step 4: Create EDocMLLMVerifyDatesTool.Codeunit.al (6238)**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6238 "E-Doc. MLLM VL Dates Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_dates');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'issue_date in YYYY-MM-DD format');
+ PropsObj.Add('issue_date', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'due_date in YYYY-MM-DD format, or empty string if not present');
+ PropsObj.Add('due_date', PropObj);
+ RequiredArr.Add('issue_date'); RequiredArr.Add('due_date');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that issue_date and due_date are valid XML dates (YYYY-MM-DD), year 1900-2100, and due_date >= issue_date if present.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ IssueDate: Text;
+ DueDate: Text;
+ Token: JsonToken;
+ begin
+ if Arguments.Get('issue_date', Token) then IssueDate := Token.AsValue().AsText();
+ if Arguments.Get('due_date', Token) then DueDate := Token.AsValue().AsText();
+
+ if VerifyTools.VerifyDates(IssueDate, DueDate, ErrorText) then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ exit(ResultObj);
+ end;
+}
+```
+
+- [ ] **Step 5: Create EDocMLLMVerifyRequiredTool.Codeunit.al (6239)**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6239 "E-Doc. MLLM VL Required Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_required_fields');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Supplier/vendor company name');
+ PropsObj.Add('vendor_name', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Invoice number / id');
+ PropsObj.Add('invoice_no', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'integer'); PropObj.Add('description', 'Number of invoice lines extracted');
+ PropsObj.Add('line_count', PropObj);
+ RequiredArr.Add('vendor_name'); RequiredArr.Add('invoice_no'); RequiredArr.Add('line_count');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that vendor name, invoice number, and at least one invoice line are present.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ VendorName: Text;
+ InvoiceNo: Text;
+ LineCount: Integer;
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ begin
+ if Arguments.Get('vendor_name', Token) then VendorName := Token.AsValue().AsText();
+ if Arguments.Get('invoice_no', Token) then InvoiceNo := Token.AsValue().AsText();
+ if Arguments.Get('line_count', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then
+ LineCount := Round(DecimalValue, 1);
+
+ if VerifyTools.VerifyRequiredFields(VendorName, InvoiceNo, LineCount, ErrorText) then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ exit(ResultObj);
+ end;
+}
+```
+
+- [ ] **Step 6: Create EDocMLLMVerifyRangesTool.Codeunit.al (6243)**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6243 "E-Doc. MLLM VL Ranges Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_ranges');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ ItemsObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line quantities');
+ PropsObj.Add('quantities', PropObj); Clear(PropObj); Clear(ItemsObj);
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line unit prices');
+ PropsObj.Add('prices', PropObj); Clear(PropObj); Clear(ItemsObj);
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line VAT rates (0-100)');
+ PropsObj.Add('vat_rates', PropObj); Clear(PropObj); Clear(ItemsObj);
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line discount percentages (0-100)');
+ PropsObj.Add('discount_pcts', PropObj);
+ RequiredArr.Add('quantities'); RequiredArr.Add('prices'); RequiredArr.Add('vat_rates'); RequiredArr.Add('discount_pcts');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that quantities > 0, unit prices > 0, VAT rates 0-100, discount percentages 0-100 for all lines.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ Quantities: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscountPcts: List of [Decimal];
+ begin
+ ParseDecimalArray(Arguments, 'quantities', Quantities);
+ ParseDecimalArray(Arguments, 'prices', Prices);
+ ParseDecimalArray(Arguments, 'vat_rates', VATRates);
+ ParseDecimalArray(Arguments, 'discount_pcts', DiscountPcts);
+
+ if VerifyTools.VerifyRanges(Quantities, Prices, VATRates, DiscountPcts, ErrorText) then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ exit(ResultObj);
+ end;
+
+ local procedure ParseDecimalArray(Arguments: JsonObject; PropertyName: Text; var Values: List of [Decimal])
+ var
+ ArrayToken: JsonToken;
+ ItemToken: JsonToken;
+ DecimalValue: Decimal;
+ begin
+ if not Arguments.Get(PropertyName, ArrayToken) then
+ exit;
+ foreach ItemToken in ArrayToken.AsArray() do
+ if Evaluate(DecimalValue, ItemToken.AsValue().AsText(), 9) then
+ Values.Add(DecimalValue);
+ end;
+}
+```
+
+
+## Task 4: Create the V2 system prompt
+
+**Files:**
+- Create: `src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtractionV2-SystemPrompt.md`
+
+- [ ] **Step 1: Create the prompt file**
+
+```markdown
+You are an invoice data extraction agent with access to verification tools.
+
+PHASE 1 — UNDERSTAND THE DOCUMENT:
+Before extracting any values, reason through the document's structure out loud. Cover:
+- What type of document is this and in what language?
+- What number format does this document use? (decimal separator, thousands separator — these vary by country)
+- What columns appear in the line item table? For each column, what does it represent? Some invoices show only a unit price; others show a gross price, one or more discount columns, and a net price. Some discounts are percentages, others are monetary amounts. Some apply sequentially. Describe exactly what you see.
+- Where are the header fields (supplier, buyer, invoice number, dates)?
+- Where is the totals section?
+- Is there anything unusual about this invoice's layout?
+
+Your analysis determines how you extract. Two invoices from different vendors may look completely different — your job is to understand each one on its own terms.
+
+PHASE 2 — EXTRACT FROM THE REGIONS YOU IDENTIFIED:
+Use your analysis from Phase 1 to extract values. Do not sweep left-to-right across the full text. Extract from the specific regions and columns you identified.
+
+Format rules (non-negotiable):
+- Numbers: XML decimal format — period (.) as decimal separator, no thousands separators (e.g. 1083 not "1 083", 2.34 not "2,34")
+- Dates: YYYY-MM-DD
+
+For everything else — how to represent the price, how to represent discounts, which column maps to which UBL field — let your Phase 1 analysis guide you. The verify tools in Phase 3 will tell you if your extraction is mathematically inconsistent.
+
+Output valid UBL JSON matching the schema provided.
+
+PHASE 3 — VERIFY YOUR OWN OUTPUT:
+Call the verification tools on what you extracted:
+- verify_line_math for each invoice line
+- verify_invoice_totals with all line amounts
+- verify_vat for the tax total
+- verify_dates with issue_date and due_date
+- verify_required_fields with vendor name, invoice number, line count
+- verify_ranges with all quantities, prices, VAT rates, and discount percentages
+
+If a tool returns { "pass": false }, read its error message. It will tell you specifically what does not add up. Reconsider your Phase 1 analysis if needed — the error may reveal that you misidentified a column role or misread a discount structure. Correct and call the tools again. Only finalise when all tools return { "pass": true }.
+
+Output ONLY valid JSON. No markdown, no explanation.
+```
+
+
+## Task 5: Create EDocumentMLLMHandlerV2
+
+This is the main handler. It implements the same three interfaces as V1 (`IStructureReceivedEDocument`, `IStructuredFormatReader`, `IStructuredDataType`) and uses the existing `EDocMLLMSchemaHelper` for JSON→draft mapping (unchanged from V1). The key difference is `StructureReceivedEDocument()` which runs the agentic loop.
+
+**Files:**
+- Create: `src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandlerV2.Codeunit.al`
+
+- [ ] **Step 1: Create the handler codeunit**
+
+```al
+// ------------------------------------------------------------------------------------------------
+// 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.Format;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Processing.Import;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using Microsoft.eServices.EDocument.Processing.Interfaces;
+using System.AI;
+using System.Azure.KeyVault;
+using System.Telemetry;
+using System.Utilities;
+
+codeunit 6244 "E-Document MLLM Handler V2" implements IStructureReceivedEDocument, IStructuredFormatReader, IStructuredDataType
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ Telemetry: Codeunit Telemetry;
+ StructuredData: Text;
+ FileFormat: Enum "E-Doc. File Format";
+ FeatureNameLbl: Label 'E-Document MLLM Extraction V2', Locked = true;
+ FileDataLbl: Label 'data:application/pdf;base64,%1', Locked = true;
+ SystemPromptV2ResourceTok: Label 'Prompts/EDocMLLMExtractionV2-SystemPrompt.md', Locked = true;
+ UserPromptLbl: Label 'Extract invoice data into this UBL JSON structure: %1. \n\nExtract ONLY visible values. Return JSON only. %2', Locked = true;
+ SecurityPromptAKVKeyTok: Label 'EDocMLLMExtraction-SecurityPromptV281', Locked = true;
+ MaxToolCallsTok: Integer;
+ BudgetExhaustedErr: Label 'The document could not be verified after %1 tool calls. The extraction was inconsistent.', Comment = '%1 = tool call count';
+ DocumentNotProcessedErr: Label 'The document could not be processed.';
+ InappropriateContentErr: Label 'The document could not be processed because it contains inappropriate content.';
+
+ procedure StructureReceivedEDocument(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType
+ var
+ ResponseJson: JsonObject;
+ ResponseText: Text;
+ begin
+ MaxToolCallsTok := 200;
+
+ RegisterCopilotCapabilityIfNeeded();
+
+ ResponseText := CallMLLMV2(EDocumentDataStorage);
+
+ if IsInappropriateContentResponse(ResponseText) then
+ Error(InappropriateContentErr);
+
+ if not ValidateAndUnwrapResponse(ResponseText, ResponseJson) then
+ exit(FallbackToADI(EDocumentDataStorage));
+
+ StructuredData := ResponseText;
+ FileFormat := "E-Doc. File Format"::JSON;
+ exit(this);
+ end;
+
+ [NonDebuggable]
+ local procedure CallMLLMV2(EDocumentDataStorage: Record "E-Doc. Data Storage"): Text
+ var
+ Base64Convert: Codeunit "Base64 Convert";
+ AzureOpenAI: Codeunit "Azure OpenAI";
+ AOAIChatMessages: Codeunit "AOAI Chat Messages";
+ AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
+ AOAIUserMessage: Codeunit "AOAI User Message";
+ AOAIOperationResponse: Codeunit "AOAI Operation Response";
+ AOAIDeployments: Codeunit "AOAI Deployments";
+ EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper";
+ VerifyLineMathTool: Codeunit "E-Doc. MLLM VL Math Tool";
+ VerifyTotalsTool: Codeunit "E-Doc. MLLM VL Totals Tool";
+ VerifyVATTool: Codeunit "E-Doc. MLLM VL VAT Tool";
+ VerifyDatesTool: Codeunit "E-Doc. MLLM VL Dates Tool";
+ VerifyRequiredTool: Codeunit "E-Doc. MLLM VL Required Tool";
+ VerifyRangesTool: Codeunit "E-Doc. MLLM VL Ranges Tool";
+ FromTempBlob: Codeunit "Temp Blob";
+ InStream: InStream;
+ Base64Data: Text;
+ ToolCallCount: Integer;
+ begin
+ // Load PDF as base64
+ FromTempBlob := EDocumentDataStorage.GetTempBlob();
+ FromTempBlob.CreateInStream(InStream, TextEncoding::UTF8);
+ Base64Data := Base64Convert.ToBase64(InStream);
+
+ // Configure AOAI
+ AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", AOAIDeployments.GetGPT41MiniPreview());
+ AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis");
+ AOAIChatCompletionParams.SetTemperature(0);
+ // Do NOT set JSON mode — tool-calling and JSON mode cannot be combined.
+ // The system prompt instructs the model to output UBL JSON as its final response.
+
+ // System prompt
+ AOAIChatMessages.SetPrimarySystemMessage(NavApp.GetResourceAsText(SystemPromptV2ResourceTok, TextEncoding::UTF8));
+
+ // Register 6 verification tools
+ AOAIChatMessages.AddTool(VerifyLineMathTool);
+ AOAIChatMessages.AddTool(VerifyTotalsTool);
+ AOAIChatMessages.AddTool(VerifyVATTool);
+ AOAIChatMessages.AddTool(VerifyDatesTool);
+ AOAIChatMessages.AddTool(VerifyRequiredTool);
+ AOAIChatMessages.AddTool(VerifyRangesTool);
+ AOAIChatMessages.SetToolChoice('auto');
+
+ // User message: PDF + UBL schema + security clause
+ AOAIUserMessage.AddFilePart(StrSubstNo(FileDataLbl, Base64Data));
+ AOAIUserMessage.AddTextPart(SecretText.SecretStrSubstNo(UserPromptLbl, EDocMLLMSchemaHelper.GetDefaultSchema(), GetSecurityClause()).Unwrap());
+ AOAIChatMessages.AddUserMessage(AOAIUserMessage);
+
+ // Agentic dispatch loop
+ repeat
+ AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse);
+
+ if not AOAIOperationResponse.IsSuccess() then
+ exit('');
+
+ if AOAIOperationResponse.IsFunctionCall() then begin
+ ToolCallCount += AOAIOperationResponse.GetFunctionResponses().Count();
+ if ToolCallCount > MaxToolCallsTok then
+ Error(BudgetExhaustedErr, ToolCallCount);
+ AOAIOperationResponse.AppendFunctionResponsesToChatMessages(AOAIChatMessages);
+ end;
+ until not AOAIOperationResponse.IsFunctionCall();
+
+ exit(AOAIOperationResponse.GetResult());
+ end;
+
+ [NonDebuggable]
+ local procedure GetSecurityClause() Result: SecretText
+ var
+ AzureKeyVault: Codeunit "Azure Key Vault";
+ begin
+ if not AzureKeyVault.GetAzureKeyVaultSecret(SecurityPromptAKVKeyTok, Result) then
+ Error(DocumentNotProcessedErr);
+ end;
+
+ // The following methods are identical to V1 (EDocumentMLLMHandler) --------
+
+ local procedure IsInappropriateContentResponse(ResponseText: Text): Boolean
+ var
+ ResponseJson: JsonObject;
+ ContentToken: JsonToken;
+ ErrorToken: JsonToken;
+ InnerText: Text;
+ begin
+ if ResponseText = '' then
+ exit(false);
+ if not ResponseJson.ReadFrom(ResponseText) then
+ exit(false);
+ if ResponseJson.Get('content', ContentToken) and ContentToken.IsValue() then begin
+ InnerText := ContentToken.AsValue().AsText();
+ Clear(ResponseJson);
+ if not ResponseJson.ReadFrom(InnerText) then
+ exit(false);
+ end;
+ exit(ResponseJson.Get('error', ErrorToken));
+ end;
+
+ local procedure ValidateAndUnwrapResponse(var ResponseText: Text; var ResponseJson: JsonObject): Boolean
+ var
+ ContentToken: JsonToken;
+ begin
+ if ResponseText = '' then
+ exit(false);
+ if not ResponseJson.ReadFrom(ResponseText) then
+ exit(false);
+ if ResponseJson.Get('content', ContentToken) then begin
+ ResponseText := ContentToken.AsValue().AsText();
+ if not ResponseJson.ReadFrom(ResponseText) then
+ exit(false);
+ end;
+ exit(ValidateMLLMResponse(ResponseJson));
+ end;
+
+ local procedure ValidateMLLMResponse(ResponseJson: JsonObject): Boolean
+ var
+ SupplierToken: JsonToken;
+ PartyToken: JsonToken;
+ NameToken: JsonToken;
+ AddressToken: JsonToken;
+ SupplierObj: JsonObject;
+ PartyObj: JsonObject;
+ NameObj: JsonObject;
+ VendorName: Text;
+ begin
+ if not ResponseJson.Get('accounting_supplier_party', SupplierToken) then exit(false);
+ if not SupplierToken.IsObject() then exit(false);
+ SupplierObj := SupplierToken.AsObject();
+ if not SupplierObj.Get('party', PartyToken) then exit(false);
+ if not PartyToken.IsObject() then exit(false);
+ PartyObj := PartyToken.AsObject();
+ if not PartyObj.Get('party_name', NameToken) then exit(false);
+ if not NameToken.IsObject() then exit(false);
+ NameObj := NameToken.AsObject();
+ if not NameObj.Get('name', NameToken) then exit(false);
+ VendorName := NameToken.AsValue().AsText();
+ if VendorName = '' then exit(false);
+ if not PartyObj.Get('postal_address', AddressToken) then exit(false);
+ if not AddressToken.IsObject() then exit(false);
+ exit(true);
+ end;
+
+ local procedure FallbackToADI(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType
+ var
+ ADIHandler: Codeunit "E-Document ADI Handler";
+ begin
+ exit(ADIHandler.StructureReceivedEDocument(EDocumentDataStorage));
+ end;
+
+ local procedure GetInvoiceLineCount(ResponseJson: JsonObject): Integer
+ var
+ LinesToken: JsonToken;
+ begin
+ if ResponseJson.Get('invoice_line', LinesToken) then
+ if LinesToken.IsArray() then
+ exit(LinesToken.AsArray().Count());
+ exit(0);
+ end;
+
+ procedure RegisterCopilotCapabilityIfNeeded()
+ var
+ CopilotCapability: Codeunit "Copilot Capability";
+ begin
+ if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"E-Document MLLM Analysis") then
+ CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis", '');
+ end;
+
+ // IStructuredFormatReader + IStructuredDataType (identical to V1) ----------
+
+ procedure GetFileFormat(): Enum "E-Doc. File Format"
+ begin
+ exit(this.FileFormat);
+ end;
+
+ procedure GetContent(): Text
+ begin
+ exit(this.StructuredData);
+ end;
+
+ procedure GetReadIntoDraftImpl(): Enum "E-Doc. Read into Draft"
+ begin
+ exit("E-Doc. Read into Draft"::MLLM);
+ end;
+
+#pragma warning disable AA0139
+ procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft"
+ var
+ TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary;
+ TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary;
+ EDocPurchaseDraftUtility: Codeunit "E-Doc. Purchase Draft Utility";
+ begin
+ ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ EDocPurchaseDraftUtility.PersistDraft(EDocument, TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice");
+ end;
+
+ local procedure ReadIntoBuffer(
+ EDocument: Record "E-Document";
+ TempBlob: Codeunit "Temp Blob";
+ var TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary;
+ var TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary)
+ var
+ EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper";
+ InStream: InStream;
+ SourceJsonObject: JsonObject;
+ LinesToken: JsonToken;
+ LinesArray: JsonArray;
+ BlobAsText: Text;
+ begin
+ TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
+ InStream.Read(BlobAsText);
+ SourceJsonObject.ReadFrom(BlobAsText);
+ EDocMLLMSchemaHelper.MapHeaderFromJson(SourceJsonObject, TempEDocPurchaseHeader);
+ TempEDocPurchaseHeader."E-Document Entry No." := EDocument."Entry No";
+ if SourceJsonObject.Get('invoice_line', LinesToken) then
+ if LinesToken.IsArray() then begin
+ LinesArray := LinesToken.AsArray();
+ EDocMLLMSchemaHelper.MapLinesFromJson(LinesArray, EDocument."Entry No", TempEDocPurchaseLine);
+ end;
+ end;
+
+ procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob")
+ var
+ TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary;
+ TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary;
+ EDocReadablePurchaseDoc: Page "E-Doc. Readable Purchase Doc.";
+ begin
+ ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ EDocReadablePurchaseDoc.SetBuffer(TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ EDocReadablePurchaseDoc.Run();
+ end;
+#pragma warning restore AA0139
+}
+```
+
+---
+
+## Task 6: Wire up app.json and verify full compilation
+
+The new codeunit files must be included in the app's file list (if the project uses explicit file lists) and the app version should be bumped.
+
+**Files:**
+- Modify: `src/Apps/W1/EDocument/App/app.json`
+
+- [ ] **Step 1: Bump app version patch number**
+
+In `app.json`, increment the 4th version segment (e.g. `29.0.0.1` → `29.0.0.2`).
+
+- [ ] **Step 2: Full compile of EDocument app**
+
+Run the compile command for the EDocument app. Expected: 0 errors, 0 warnings introduced by new code.
+
+Use: `dispatch 'Build-Application -AppName "E-Document Core" -CountryCode W1'`
+
+Expected output: build succeeds.
+
+- [ ] **Step 3: Run existing MLLM tests to verify nothing regressed**
+
+Run: `dispatch 'Run-Tests -AppName "E-Document Core Tests" -TestCodeunit "EDoc MLLM Tests"'`
+
+Expected: all tests pass (V1 tests are unaffected).
+
+- [ ] **Step 4: Run new verify tools tests**
+
+Run: `dispatch 'Run-Tests -AppName "E-Document Core Tests" -TestCodeunit "EDoc MLLM Verify Tools Tests"'`
+
+Expected: all tests pass.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/Apps/W1/EDocument/App/app.json
+git commit -m "Bump version and verify V2 compiles cleanly"
+```
diff --git a/docs/superpowers/specs/2026-05-26-mllm-v2-design.md b/docs/superpowers/specs/2026-05-26-mllm-v2-design.md
new file mode 100644
index 0000000000..6d89ad4849
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-26-mllm-v2-design.md
@@ -0,0 +1,152 @@
+# E-Document MLLM Extraction V2 — Design Spec
+
+**Date:** 2026-05-26
+**Status:** Draft
+**Replaces:** `EDocumentMLLMHandler.Codeunit.al` (V1)
+
+---
+
+## Problem
+
+V1 performs a single-pass extraction: one AOAI call, one system prompt, one UBL JSON response. It has no mechanism to detect or correct errors it is confident about. Known failure modes observed in production:
+
+- **Locale number formats** — Swedish `"2,34"` extracted as `234` (comma stripped by `AsDecimal()`)
+- **Discount ambiguity** — invoice shows both gross price (`Pris`) and net price (`Pris efter rab.`); model uses net price AND applies a discount percentage, double-counting the discount
+- **Silent wrong values** — extraction passes schema validation but produces semantically wrong output (wrong totals, wrong unit prices)
+
+The root cause is that V1 sweeps the document left-to-right without understanding its structure, and has no self-correction capability.
+
+---
+
+## Solution: Plan-Act-Verify Agentic Loop
+
+A single agentic AOAI call where the agent:
+
+1. **Plans** — identifies document structure as chain-of-thought reasoning (regions, column roles, locale, flags) *before* extracting any values
+2. **Acts** — extracts from the identified regions, guided by the structural understanding from the plan step
+3. **Verifies** — calls deterministic AL-implemented tools to check its own output; self-corrects if tools report failures; repeats until all tools pass or the tool call budget is exhausted
+
+The loop is entirely inside the model's reasoning turn. AL code sets up the tools and runs the agent; it does not orchestrate the plan/act/verify sequence.
+
+---
+
+## Architecture
+
+### Single Agentic Call
+
+```
+PDF (base64)
+ │
+ ▼
+┌─────────────────────────────────────────────────────┐
+│ AGENT REASONING (one AOAI call, tool-use loop) │
+│ │
+│ 1. PLAN (chain-of-thought) │
+│ "This is a Swedish invoice. Columns: Antal, │
+│ Pris, Rabatt, Rabatt, Pris efter rab., Belopp. │
+│ Decimal sep = comma. Two chained discount cols. │
+│ Net price column present." │
+│ │
+│ 2. ACT (targeted extraction) │
+│ Extract from identified regions using column │
+│ roles, not left-to-right text sweep. │
+│ │
+│ 3. VERIFY (tool calls) │
+│ verify_line_math() verify_totals() │
+│ verify_vat() verify_dates() │
+│ verify_required() verify_ranges() │
+│ │
+│ On failure → agent reads error, re-extracts, │
+│ calls tools again. Loops until pass or budget. │
+└─────────────────────────────────────────────────────┘
+ │ │
+ ▼ ▼
+Verified UBL JSON Error (budget
+→ BC Purchase Draft exhausted)
+```
+
+**Model:** GPT-4.1 Mini (chosen for vision capability — the agent reads the PDF visually, not as extracted text)
+**Tool call budget:** 200
+**Temperature:** 0
+
+### On Budget Exhaustion
+
+E-Document status set to `Error`. A log entry records which verify check was still failing on the last iteration. No draft is created. ADI is not used as a fallback for verify failures.
+
+ADI fallback is retained only for AOAI call failures (network error, content filter, empty response) — the same signal V1 uses today.
+
+---
+
+## New AL Components
+
+### `EDocMLLMHandlerV2.Codeunit.al`
+
+Implements `IStructureReceivedEDocument` (same interface as V1). Registered as enum value `"MLLM V2"` on `"Structure Data Impl."` — existing services using `"MLLM"` are unaffected until explicitly migrated.
+
+Responsibilities:
+- Build the AOAI chat messages (system prompt + PDF user message)
+- Register the 6 verify tools as AOAI function definitions (`AOAITools`)
+- Run the agentic dispatch loop in AL:
+ 1. Call `GenerateChatCompletion`
+ 2. If response contains tool call requests: execute via `EDocMLLMVerifyTools`, append results to `AOAIChatMessages`, increment call counter, go to 1
+ 3. If response contains no tool calls (model is done): extract final JSON
+ 4. If call counter exceeds budget: surface error
+- On success: pass the final JSON to the existing `EDocMLLMSchemaHelper.MapHeaderFromJson` / `MapLinesFromJson` pipeline unchanged
+
+The tool dispatch loop runs in AL, not inside the SDK. Each iteration is a new `GenerateChatCompletion` call with the tool results appended to the conversation history.
+
+### `EDocMLLMVerifyTools.Codeunit.al`
+
+Six methods, each returning `JsonObject` with `{ "pass": bool, "error": string }`:
+
+| Tool | Inputs | Check |
+|------|--------|-------|
+| `verify_line_math` | unit_price, quantity, discount_pct, line_extension_amount | `unit_price × qty × (1 − disc/100) ≈ line_total` (within 1% relative tolerance) |
+| `verify_invoice_totals` | line_amounts[], tax_exclusive_amount | `sum(lines) ≈ sub_total` (within 1% relative tolerance) |
+| `verify_vat` | tax_exclusive_amount, vat_rate, tax_amount | `sub_total × rate/100 ≈ tax_amount` (within 1% relative tolerance) |
+| `verify_dates` | issue_date, due_date | Both parse as valid dates; `due_date ≥ issue_date`; year in 1900–2100 |
+| `verify_required_fields` | vendor_name, invoice_no, line_count | None are blank/zero |
+| `verify_ranges` | quantities[], prices[], vat_rates[], discount_pcts[] | All > 0 (qty, price); 0–100 (vat, discount) |
+
+Numeric tolerance for amount comparisons: 1% relative (`|expected − actual| / max(|actual|, 1) < 0.01`). A fixed absolute tolerance fails on large-quantity invoices where per-unit rounding accumulates (e.g. 1083 items × 0.005 rounding = 5.4 max error).
+
+### `EDocMLLMExtractionV2-SystemPrompt.md`
+
+New prompt resource. Three explicit sections:
+
+1. **Structure identification** — "Before extracting any values, describe in your reasoning: document type, language, decimal separator, thousands separator, line item table column names and their roles (gross price, discount %, net price, quantity, line total), header and totals regions, and any flags (e.g. multiple discount columns, net price column present)."
+
+2. **Targeted extraction** — "Extract data from the regions you identified. Do not sweep left-to-right across the full page. Use the column roles you identified to assign values correctly. Use XML decimal format (period as decimal separator, no thousands separators)."
+
+3. **Verification** — "After producing the UBL JSON, call the verify tools on your output. If any tool reports a failure, read the error message, correct the relevant fields, and call the tools again. Finalize only when all tools pass."
+
+---
+
+## What Is Unchanged
+
+- `EDocMLLMSchemaHelper.Codeunit.al` — `MapHeaderFromJson`, `MapLinesFromJson`, `GetDecimal` (with `Evaluate(..., 9)` from the V1 fix), `GetDate`
+- `ubl_example.json` — UBL schema template (updated by V1 fix to use numeric `0` placeholders)
+- `EDocMLLMHandler.Codeunit.al` — V1 stays registered under `"MLLM"` until removed
+
+---
+
+## What Is Retired
+
+V1 (`"MLLM"` enum value and `EDocumentMLLMHandler.Codeunit.al`) is not removed in this change — existing service configurations keep working. A follow-up cleanup removes V1 once all services have migrated to `"MLLM V2"`.
+
+---
+
+## Error Flow
+
+```
+AOAI call fails entirely → FallbackToADI() (existing path)
+AOAI returns bad JSON → FallbackToADI() (existing path)
+Verify tools never all pass → EDocument.Status = Error + log entry
+Vendor fields missing (schema) → FallbackToADI() (existing V1 ValidateMLLMResponse path)
+```
+
+---
+
+## Open Questions
+
+None — all design decisions confirmed.
diff --git a/src/Apps/W1/EDocument/App/.resources/DataExchange/eDocPEPPOLCrMemoImportV2.xml b/src/Apps/W1/EDocument/App/.resources/DataExchange/eDocPEPPOLCrMemoImportV2.xml
new file mode 100644
index 0000000000..8b1a49d8c7
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/.resources/DataExchange/eDocPEPPOLCrMemoImportV2.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Apps/W1/EDocument/App/.resources/DataExchange/eDocPEPPOLInvoiceImportV2.xml b/src/Apps/W1/EDocument/App/.resources/DataExchange/eDocPEPPOLInvoiceImportV2.xml
new file mode 100644
index 0000000000..947f50a861
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/.resources/DataExchange/eDocPEPPOLInvoiceImportV2.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtractionV2-SystemPrompt.md b/src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtractionV2-SystemPrompt.md
new file mode 100644
index 0000000000..e283a516fa
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtractionV2-SystemPrompt.md
@@ -0,0 +1,53 @@
+You are an invoice data extraction agent with access to verification tools.
+
+PHASE 1 — UNDERSTAND AND RECORD:
+Call `analyze_invoice` FIRST. Before extracting any values, study the document carefully and record:
+- doc_type, language, decimal_sep, thousands_sep
+- line_columns: describe each column in the line table and its role (gross price, discount %, net price, quantity, line total, etc.)
+- line_ids: the id values of all invoice lines you see on the document
+- notes: describe the totals section at the bottom of the invoice — what labelled amounts are shown? Look for: subtotal (sum of lines before header discount), header-level discount (a deduction applied to the whole invoice, not a line), VAT amount, and the final payable/total amount. Record the label and value of each field you find. Also note whether the invoice shows both a pre-discount and post-discount line price.
+
+This call initialises your verification checklist. You will receive the full list of items to verify.
+
+PHASE 2 — EXTRACT FROM THE REGIONS YOU IDENTIFIED:
+Use your analysis from Phase 1 to extract values. Do not sweep left-to-right across the full text. Extract from the specific regions and columns you identified.
+
+Format rules (non-negotiable):
+- Numbers: XML decimal format — period (.) as decimal separator, no thousands separators (e.g. 1083 not "1 083", 2.34 not "2,34")
+- Dates: YYYY-MM-DD
+
+For the totals section, read each labelled amount directly from the document:
+- legal_monetary_total.line_extension_amount: the sum-of-lines subtotal as printed
+- legal_monetary_total.allowance_total_amount: the header discount amount as printed (0 if none)
+- legal_monetary_total.tax_exclusive_amount: the pre-VAT total as printed (= line_extension_amount − allowance_total_amount)
+- tax_total.tax_amount: the VAT amount as printed
+- legal_monetary_total.payable_amount: the final amount due as printed
+These are read from the document — do not calculate them from other fields.
+
+For everything else — how to represent line prices and discounts — let your Phase 1 analysis guide you. The verify tools in Phase 3 will tell you if your extraction is mathematically inconsistent.
+
+After extracting, call submit_extraction(json) with the complete UBL JSON to save it.
+
+PHASE 3 — VERIFY YOUR OWN OUTPUT:
+The checklist is your source of truth. Follow it strictly:
+
+1. Call get_checklist() to see all pending items.
+2. For each item with status "pending", call the matching verify tool then immediately call mark_item with the result:
+ - verify_line_math(line_id, unit_price, quantity, discount_pct, line_extension_amount) → mark_item(item_id="verify_line_", passed=..., error=...)
+ - verify_invoice_totals(line_amounts[], tax_exclusive_amount, allowance_total_amount) → mark_item(item_id="verify_invoice_totals", ...)
+ - verify_vat(tax_exclusive_amount, vat_rate, tax_amount) → mark_item(item_id="verify_vat", ...)
+ - verify_dates(issue_date, due_date) → mark_item(item_id="verify_dates", ...)
+ - verify_required_fields(vendor_name, invoice_no, line_count) → mark_item(item_id="verify_required_fields", ...)
+ - verify_ranges(quantities[], prices[], vat_rates[], discount_pcts[]) → mark_item(item_id="verify_ranges", ...)
+ - verify_payable(tax_exclusive_amount, tax_amount, payable_amount) → mark_item(item_id="verify_payable", ...)
+3. After working through the pending items, call get_checklist() again.
+4. If any items are still "pending" or "failed", repeat from step 2.
+5. When get_checklist() shows ALL items as "passed", call submit_extraction(json) one final time with the complete corrected UBL JSON, then output the same JSON as your final response.
+
+If a verify tool returns { "pass": false }:
+1. State out loud what the error tells you: which value is wrong and what it should be.
+2. State which specific field you are changing, to what value, and why.
+3. Call submit_extraction with the complete corrected UBL JSON (not just the changed field).
+4. Re-call the verify tool for that item, call mark_item with the new result, then call get_checklist() to confirm.
+
+Output ONLY valid JSON. No markdown, no explanation.
diff --git a/src/Apps/W1/EDocument/App/app.json b/src/Apps/W1/EDocument/App/app.json
index 4ed9bcb273..f806a03046 100644
--- a/src/Apps/W1/EDocument/App/app.json
+++ b/src/Apps/W1/EDocument/App/app.json
@@ -4,7 +4,7 @@
"publisher": "Microsoft",
"brief": "The Dynamics 365 Business Central E-Documents module enables different models of electronic invoicing, available for additional localizations.",
"description": "Business Central's E-Documents module is the foundation layer for all e-invoicing standards covering most common processes, but it can be used for other electronic documents. The module is easily extendable with the country-based e-invoicing apps. The E-Documents app covers both sales and purchase processes and can have different lifecycles from standard invoices in Business Central.",
- "version": "29.0.0.0",
+ "version": "29.0.0.2",
"privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009",
"EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
"help": "https://go.microsoft.com/fwlink/?linkid=2204541",
@@ -76,4 +76,4 @@
"features": [
"TranslationFile"
]
-}
+}
\ No newline at end of file
diff --git a/src/Apps/W1/EDocument/App/src/EDocumentInstall.Codeunit.al b/src/Apps/W1/EDocument/App/src/EDocumentInstall.Codeunit.al
index 1ce97e76e5..af8eb31fb1 100644
--- a/src/Apps/W1/EDocument/App/src/EDocumentInstall.Codeunit.al
+++ b/src/Apps/W1/EDocument/App/src/EDocumentInstall.Codeunit.al
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
@@ -18,6 +18,7 @@ codeunit 6161 "E-Document Install"
trigger OnInstallAppPerCompany()
begin
InsertDataExch();
+ InsertDataExchV2();
end;
trigger OnInstallAppPerDatabase()
@@ -47,6 +48,19 @@ codeunit 6161 "E-Document Install"
UpgradeTag.SetUpgradeTag(GetEDOCDataExchUpdateTag());
end;
+ internal procedure InsertDataExchV2()
+ var
+ UpgradeTag: Codeunit "Upgrade Tag";
+ begin
+ if UpgradeTag.HasUpgradeTag(GetEDOCDataExchV2UpdateTag()) then
+ exit;
+
+ ImportInvoiceV2XML();
+ ImportCreditMemoV2XML();
+
+ UpgradeTag.SetUpgradeTag(GetEDOCDataExchV2UpdateTag());
+ end;
+
internal procedure ImportServiceInvoiceXML()
var
DataExchDef: Record "Data Exch. Def";
@@ -167,10 +181,53 @@ codeunit 6161 "E-Document Install"
Clear(TempBlob);
end;
+ internal procedure ImportInvoiceV2XML()
+ var
+ DataExchDef: Record "Data Exch. Def";
+ TempBlob: Codeunit "Temp Blob";
+ XMLOutStream: OutStream;
+ XMLInStream: InStream;
+ ResInStream: InStream;
+ begin
+ if DataExchDef.Get('EDOCPEPINVIMPV2') then
+ DataExchDef.Delete(true);
+ if DataExchDef.Get('EDOCPEPINVPURCHDRAFT') then
+ DataExchDef.Delete(true);
+
+ NavApp.GetResource('DataExchange/eDocPEPPOLInvoiceImportV2.xml', ResInStream);
+ TempBlob.CreateOutStream(XMLOutStream);
+ CopyStream(XMLOutStream, ResInStream);
+ TempBlob.CreateInStream(XMLInStream);
+ Xmlport.Import(Xmlport::"Imp / Exp Data Exch Def & Map", XMLInStream);
+ Clear(TempBlob);
+ end;
+
+ internal procedure ImportCreditMemoV2XML()
+ var
+ DataExchDef: Record "Data Exch. Def";
+ TempBlob: Codeunit "Temp Blob";
+ XMLOutStream: OutStream;
+ XMLInStream: InStream;
+ ResInStream: InStream;
+ begin
+ if DataExchDef.Get('EDOCPEPCRMEMOIMPV2') then
+ DataExchDef.Delete(true);
+ if DataExchDef.Get('EDOCPEPCMPURCHDRAFT') then
+ DataExchDef.Delete(true);
+
+ NavApp.GetResource('DataExchange/eDocPEPPOLCrMemoImportV2.xml', ResInStream);
+ TempBlob.CreateOutStream(XMLOutStream);
+ CopyStream(XMLOutStream, ResInStream);
+ TempBlob.CreateInStream(XMLInStream);
+ Xmlport.Import(Xmlport::"Imp / Exp Data Exch Def & Map", XMLInStream);
+ Clear(TempBlob);
+ end;
+
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)]
local procedure RegisterUpgradeTags(var PerCompanyUpgradeTags: List of [Code[250]])
begin
PerCompanyUpgradeTags.Add(GetEDOCDataExchUpdateTag());
+ PerCompanyUpgradeTags.Add(GetEDOCDataExchV2UpdateTag());
end;
local procedure GetEDOCDataExchUpdateTag(): Code[250]
@@ -178,6 +235,11 @@ codeunit 6161 "E-Document Install"
exit('MS-365688-EDOCDataExchPEPPOL-20231113');
end;
+ local procedure GetEDOCDataExchV2UpdateTag(): Code[250]
+ begin
+ exit('MS-EDOCDataExchPEPPOLV2-20260414');
+ end;
+
var
DataExchangeInvXML1Txt: Label '';
DataExchangeCrMXML1Txt: Label '', Locked = true;
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al
index fa0fe2fb0b..ce558f9aff 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al
@@ -40,4 +40,9 @@ enum 6109 "E-Doc. Read into Draft" implements IStructuredFormatReader
Caption = 'MLLM';
Implementation = IStructuredFormatReader = "E-Document MLLM Handler";
}
+ value(5; "Data Exchange Purchase")
+ {
+ Caption = 'Data Exchange Purchase';
+ Implementation = IStructuredFormatReader = "E-Doc. DataExch. Purch Handler";
+ }
}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al
index 693cdf90ad..71f16c1fbe 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al
@@ -10,6 +10,7 @@ using Microsoft.eServices.EDocument.Processing.Import.Purchase;
using Microsoft.eServices.EDocument.Processing.Interfaces;
using Microsoft.Finance.GeneralLedger.Setup;
using Microsoft.Purchases.Document;
+using Microsoft.Purchases.History;
using Microsoft.Purchases.Payables;
using System.Telemetry;
@@ -58,6 +59,19 @@ codeunit 6404 "E-Doc. Create Purch. Cr. Memo" implements IEDocumentFinishDraft,
EDocPurchaseDocumentHelper.RevertCreatedDocument(EDocument);
end;
+ local procedure ResolveAppliesToFromExtInvoiceNo(ExtInvoiceNo: Text[100]; var PurchaseHeader: Record "Purchase Header")
+ var
+ PurchInvHeader: Record "Purch. Inv. Header";
+ begin
+ if PurchaseHeader."Pay-to Vendor No." <> '' then
+ PurchInvHeader.SetRange("Buy-from Vendor No.", PurchaseHeader."Pay-to Vendor No.");
+ PurchInvHeader.SetRange("Vendor Invoice No.", ExtInvoiceNo);
+ if PurchInvHeader.FindFirst() then begin
+ PurchaseHeader."Applies-to Doc. Type" := PurchaseHeader."Applies-to Doc. Type"::Invoice;
+ PurchaseHeader."Applies-to Doc. No." := PurchInvHeader."No.";
+ end;
+ end;
+
procedure CreatePurchaseCreditMemo(EDocument: Record "E-Document"): Record "Purchase Header"
var
PurchaseHeader: Record "Purchase Header";
@@ -108,7 +122,10 @@ codeunit 6404 "E-Doc. Create Purch. Cr. Memo" implements IEDocumentFinishDraft,
EDocPurchaseDocumentHelper.ValidateFieldWithContext(PurchaseHeader, PurchaseHeader.FieldNo("Currency Code"), EDocumentPurchaseHeader."Currency Code");
if EDocumentPurchaseHeader."Applies-to Doc. No." <> '' then
- PurchaseHeader."Applies-to Doc. No." := CopyStr(EDocumentPurchaseHeader."Applies-to Doc. No.", 1, MaxStrLen(PurchaseHeader."Applies-to Doc. No."));
+ PurchaseHeader."Applies-to Doc. No." := CopyStr(EDocumentPurchaseHeader."Applies-to Doc. No.", 1, MaxStrLen(PurchaseHeader."Applies-to Doc. No."))
+ else
+ if EDocumentPurchaseHeader."Applies-to Ext. Invoice No." <> '' then
+ ResolveAppliesToFromExtInvoiceNo(EDocumentPurchaseHeader."Applies-to Ext. Invoice No.", PurchaseHeader);
PurchaseHeader.Modify();
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al
index a49e0ab6aa..4f49042704 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al
@@ -386,17 +386,12 @@ page 6183 "E-Doc. Purchase Draft Subform"
TotalEDocPurchaseLine: Record "E-Document Purchase Line";
EDocumentImportHelper: Codeunit "E-Document Import Helper";
LineSubtotal: Decimal;
- DiscountExceedsSubtotalErr: Label 'Discount should not exceed the subtotal of the line';
begin
LineSubtotal := Rec.Quantity * Rec."Unit Price";
- LineAmount := LineSubtotal - Rec."Total Discount";
- if LineSubtotal = 0 then begin
- if Rec."Total Discount" > 0 then
- Error(DiscountExceedsSubtotalErr)
- end
+ if Rec."Total Discount" > LineSubtotal then
+ LineAmount := 0
else
- if Rec."Total Discount" / LineSubtotal > 1 then
- Error(DiscountExceedsSubtotalErr);
+ LineAmount := LineSubtotal - Rec."Total Discount";
if not UpdateParentRecord then
exit;
if not EDocumentPurchaseHeader.Get(Rec."E-Document Entry No.") then
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocReadablePurchaseDoc.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocReadablePurchaseDoc.Page.al
index b7be850408..21a786dad1 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocReadablePurchaseDoc.Page.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocReadablePurchaseDoc.Page.al
@@ -131,6 +131,11 @@ page 6182 "E-Doc. Readable Purchase Doc."
Caption = 'Purchase Order No.';
ToolTip = 'Specifies the purchase order number.';
}
+ field("Applies-to Ext. Invoice No."; Rec."Applies-to Ext. Invoice No.")
+ {
+ Caption = 'Applies-to Ext. Invoice No.';
+ ToolTip = 'Specifies the vendor''s original invoice number that this credit memo references.';
+ }
field("Invoice Date"; Rec."Invoice Date")
{
Caption = 'Invoice Date';
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al
index 167ad97160..60c3eb6528 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al
@@ -160,13 +160,6 @@ page 6181 "E-Document Purchase Draft"
CurrPage.Update();
end;
}
- field("Vendor Invoice No."; EDocumentPurchaseHeader."Vendor Invoice No.")
- {
- Caption = 'Vendor Invoice No.';
- ToolTip = 'Specifies the vendor''s invoice number referenced in the credit memo billing reference.';
- Visible = IsCreditMemo;
- Editable = false;
- }
field("Applies-to Doc. No."; EDocumentPurchaseHeader."Applies-to Doc. No.")
{
Caption = 'Applies-to Doc. No.';
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al
index 67dcfbdc46..06a2997843 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al
@@ -229,9 +229,9 @@ table 6100 "E-Document Purchase Header"
Caption = 'Applies-to Doc. No.';
DataClassification = CustomerContent;
}
- field(40; "Vendor Invoice No."; Text[100])
+ field(40; "Applies-to Ext. Invoice No."; Text[100])
{
- Caption = 'Vendor Invoice No.';
+ Caption = 'Applies-to Ext. Invoice No.';
DataClassification = CustomerContent;
}
#endregion Purchase fields
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al
index 194b0d9adf..138a838f85 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al
@@ -31,4 +31,9 @@ enum 6103 "Structure Received E-Doc." implements IStructureReceivedEDocument
Caption = 'MLLM Extraction';
Implementation = IStructureReceivedEDocument = "E-Document MLLM Handler";
}
+ value(4; "MLLM V2")
+ {
+ Caption = 'MLLM Extraction V2';
+ Implementation = IStructureReceivedEDocument = "E-Document MLLM Handler V2";
+ }
}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocDataExchPurchHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocDataExchPurchHandler.Codeunit.al
new file mode 100644
index 0000000000..1d7866427f
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocDataExchPurchHandler.Codeunit.al
@@ -0,0 +1,403 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.IO.Peppol;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using Microsoft.eServices.EDocument.Processing.Interfaces;
+using Microsoft.Finance.GeneralLedger.Setup;
+using Microsoft.Foundation.Attachment;
+using System.IO;
+using System.Text;
+using System.Utilities;
+
+codeunit 6407 "E-Doc. DataExch. Purch Handler" implements IStructuredFormatReader
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft"
+ var
+ BestDefCode: Code[20];
+ BestDocType: Enum "E-Document Type";
+ begin
+ FindBestDataExchDef(EDocument, TempBlob, BestDefCode, BestDocType);
+ RunPipelineAndBridge(EDocument, TempBlob, BestDefCode);
+ exit(MapDocumentTypeToProcessDraft(BestDocType));
+ end;
+
+ procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob")
+ begin
+ Error(ViewNotImplementedErr);
+ end;
+
+ #region Auto-Detection
+
+ ///
+ /// Determines the Data Exchange Definition code by matching the document's XML root
+ /// namespace against the Namespace field on each definition configured for the
+ /// E-Document Service in EDocServiceDataExchDef.
+ ///
+ local procedure FindBestDataExchDef(EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var BestDefCode: Code[20]; var BestDocType: Enum "E-Document Type")
+ var
+ EDocServiceDataExchDef: Record "E-Doc. Service Data Exch. Def.";
+ DataExchLineDef: Record "Data Exch. Line Def";
+ EDocumentService: Record "E-Document Service";
+ DocumentNamespace: Text;
+ begin
+ DocumentNamespace := GetDocumentRootNamespace(TempBlob);
+ EDocumentService := EDocument.GetEDocumentService();
+
+ EDocServiceDataExchDef.SetRange("E-Document Format Code", EDocumentService.Code);
+ if not EDocServiceDataExchDef.FindSet() then
+ Error(NoDataExchDefsConfiguredErr, EDocumentService.Code);
+
+ repeat
+ if EDocServiceDataExchDef."Impt. Data Exchange Def. Code" <> '' then begin
+ DataExchLineDef.SetRange("Data Exch. Def Code", EDocServiceDataExchDef."Impt. Data Exchange Def. Code");
+ DataExchLineDef.SetRange("Parent Code", '');
+ if DataExchLineDef.FindFirst() then
+ if DataExchLineDef.Namespace = DocumentNamespace then begin
+ BestDefCode := EDocServiceDataExchDef."Impt. Data Exchange Def. Code";
+ BestDocType := EDocServiceDataExchDef."Document Type";
+ exit;
+ end;
+ end;
+ until EDocServiceDataExchDef.Next() = 0;
+
+ Error(UnrecognisedNamespaceErr, DocumentNamespace);
+ end;
+
+ local procedure GetDocumentRootNamespace(var TempBlob: Codeunit "Temp Blob"): Text
+ var
+ XmlDoc: XmlDocument;
+ RootElement: XmlElement;
+ Stream: InStream;
+ begin
+ TempBlob.CreateInStream(Stream);
+ if not XmlDocument.ReadFrom(Stream, XmlDoc) then
+ exit('');
+ XmlDoc.GetRoot(RootElement);
+ exit(RootElement.NamespaceUri());
+ end;
+
+ #endregion Auto-Detection
+
+ #region Pipeline and Bridge
+
+ ///
+ /// Runs the full Data Exchange pipeline via ProcessDataExchange, then
+ /// bridge-maps intermediate data to v2 staging tables.
+ /// The v2 definitions target staging tables (6100/6101) directly and have
+ /// no pre-mapping codeunit — conformant with the Data Exchange framework.
+ ///
+ local procedure RunPipelineAndBridge(EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; DataExchDefCode: Code[20])
+ var
+ DataExch: Record "Data Exch.";
+ DataExchDef: Record "Data Exch. Def";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ Stream: InStream;
+ begin
+ DataExchDef.Get(DataExchDefCode);
+
+ // Insert header record before pipeline so the Post-Mapping codeunit can write to it.
+ EDocumentPurchaseHeader.InsertForEDocument(EDocument);
+
+ TempBlob.CreateInStream(Stream);
+ DataExch.Init();
+ DataExch.InsertRec('', Stream, DataExchDef.Code);
+ DataExch."Related Record" := EDocument.RecordId;
+ DataExch.Modify(true);
+
+ DataExch.ImportToDataExch(DataExchDef);
+ // ProcessDataExchange runs DataHandlingCodeunit (1214) to populate Intermediate Data Import,
+ // then the Post-Mapping codeunit registered on the header mapping for format-specific work.
+ DataExchDef.ProcessDataExchange(DataExch);
+ BridgeToStagingTables(EDocument, DataExch);
+
+ DeleteIntermediateData(DataExch);
+ end;
+
+ local procedure BridgeToStagingTables(EDocument: Record "E-Document"; DataExch: Record "Data Exch.")
+ var
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ begin
+ EDocumentPurchaseHeader.GetFromEDocument(EDocument);
+
+ MapIntermediateToHeader(DataExch, EDocumentPurchaseHeader);
+ PostProcessHeader(EDocumentPurchaseHeader);
+ EDocumentPurchaseHeader.Modify();
+
+ MapIntermediateToLines(EDocument, DataExch);
+ ProcessAttachments(EDocument, DataExch);
+
+ OnAfterBridgeToStagingTables(DataExch."Entry No.", EDocumentPurchaseHeader);
+ end;
+
+ #endregion Pipeline and Bridge
+
+ #region Header Mapping
+
+ ///
+ /// Reads intermediate data targeting table 6100 (E-Document Purchase Header)
+ /// and assigns values to the staging record using the configured field mappings.
+ ///
+ local procedure MapIntermediateToHeader(DataExch: Record "Data Exch."; var EDocumentPurchaseHeader: Record "E-Document Purchase Header")
+ var
+ IntermediateDataImport: Record "Intermediate Data Import";
+ RecordRef: RecordRef;
+ FieldRef: FieldRef;
+ FieldValue: Text;
+ begin
+ IntermediateDataImport.SetRange("Data Exch. No.", DataExch."Entry No.");
+ IntermediateDataImport.SetRange("Table ID", Database::"E-Document Purchase Header");
+ IntermediateDataImport.SetRange("Parent Record No.", 0);
+ if not IntermediateDataImport.FindSet() then
+ exit;
+
+ RecordRef.GetTable(EDocumentPurchaseHeader);
+ repeat
+ if RecordRef.FieldExist(IntermediateDataImport."Field ID") then begin
+ FieldRef := RecordRef.Field(IntermediateDataImport."Field ID");
+ FieldValue := CopyStr(IntermediateDataImport.GetValue(), 1, GetFieldMaxLength(FieldRef));
+ if FieldValue <> '' then
+ AssignFieldValue(FieldRef, FieldValue);
+ end;
+ until IntermediateDataImport.Next() = 0;
+ RecordRef.SetTable(EDocumentPurchaseHeader);
+ end;
+
+ ///
+ /// Post-processes header fields that cannot be handled by Data Exchange alone:
+ /// - Total VAT: calculated from Total - Sub Total - Total Discount
+ /// - Amount Due: copied from Total
+ /// - Currency Code: LCY-blank convention
+ ///
+ local procedure PostProcessHeader(var EDocumentPurchaseHeader: Record "E-Document Purchase Header")
+ begin
+ EDocumentPurchaseHeader."Total VAT" := EDocumentPurchaseHeader.Total - EDocumentPurchaseHeader."Sub Total" - EDocumentPurchaseHeader."Total Discount";
+ EDocumentPurchaseHeader."Amount Due" := EDocumentPurchaseHeader.Total;
+ ApplyLCYBlankConvention(EDocumentPurchaseHeader."Currency Code");
+ end;
+
+ #endregion Header Mapping
+
+ #region Line Mapping
+
+ ///
+ /// Reads intermediate data targeting table 6101 (E-Document Purchase Line)
+ /// and creates staging line records. Each distinct Record No. in intermediate
+ /// data becomes a separate staging line.
+ ///
+ local procedure MapIntermediateToLines(EDocument: Record "E-Document"; DataExch: Record "Data Exch.")
+ var
+ IntermediateDataImport: Record "Intermediate Data Import";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ RecordRef: RecordRef;
+ FieldRef: FieldRef;
+ FieldValue: Text;
+ CurrRecordNo: Integer;
+ begin
+ IntermediateDataImport.SetRange("Data Exch. No.", DataExch."Entry No.");
+ IntermediateDataImport.SetRange("Table ID", Database::"E-Document Purchase Line");
+ IntermediateDataImport.SetFilter("Parent Record No.", '>%1', 0);
+ IntermediateDataImport.SetCurrentKey("Record No.");
+
+ if not IntermediateDataImport.FindSet() then
+ exit;
+
+ CurrRecordNo := -1;
+ repeat
+ if CurrRecordNo <> IntermediateDataImport."Record No." then begin
+ if CurrRecordNo <> -1 then begin
+ RecordRef.SetTable(EDocumentPurchaseLine);
+ PostProcessLine(EDocumentPurchaseLine);
+ EDocumentPurchaseLine.Insert();
+ OnAfterMapLineToStaging(DataExch."Entry No.", CurrRecordNo, EDocumentPurchaseLine);
+ end;
+
+ Clear(EDocumentPurchaseLine);
+ EDocumentPurchaseLine."E-Document Entry No." := EDocument."Entry No";
+ EDocumentPurchaseLine."Line No." := EDocumentPurchaseLine.GetNextLineNo(EDocument."Entry No");
+ RecordRef.GetTable(EDocumentPurchaseLine);
+ CurrRecordNo := IntermediateDataImport."Record No.";
+ end;
+
+ if RecordRef.FieldExist(IntermediateDataImport."Field ID") then begin
+ FieldRef := RecordRef.Field(IntermediateDataImport."Field ID");
+ FieldValue := CopyStr(IntermediateDataImport.GetValue(), 1, GetFieldMaxLength(FieldRef));
+ if FieldValue <> '' then
+ AssignFieldValue(FieldRef, FieldValue);
+ RecordRef.SetTable(EDocumentPurchaseLine);
+ RecordRef.GetTable(EDocumentPurchaseLine);
+ end;
+ until IntermediateDataImport.Next() = 0;
+
+ // Insert last line
+ RecordRef.SetTable(EDocumentPurchaseLine);
+ PostProcessLine(EDocumentPurchaseLine);
+ EDocumentPurchaseLine.Insert();
+ OnAfterMapLineToStaging(DataExch."Entry No.", CurrRecordNo, EDocumentPurchaseLine);
+ end;
+
+ local procedure PostProcessLine(var EDocumentPurchaseLine: Record "E-Document Purchase Line")
+ begin
+ ApplyLCYBlankConvention(EDocumentPurchaseLine."Currency Code");
+ end;
+
+ #endregion Line Mapping
+
+ #region Attachment Processing
+
+ local procedure ProcessAttachments(EDocument: Record "E-Document"; DataExch: Record "Data Exch.")
+ var
+ DocumentAttachment: Record "Document Attachment";
+ IntermediateDataImport: Record "Intermediate Data Import";
+ EDocAttachmentProcessor: Codeunit "E-Doc. Attachment Processor";
+ AttachmentTempBlob: Codeunit "Temp Blob";
+ Base64Convert: Codeunit "Base64 Convert";
+ InStream: InStream;
+ OutStream: OutStream;
+ FileName: Text;
+ Base64Data: Text;
+ CurrRecordNo: Integer;
+ begin
+ IntermediateDataImport.SetRange("Data Exch. No.", DataExch."Entry No.");
+ IntermediateDataImport.SetRange("Table ID", Database::"Document Attachment");
+ IntermediateDataImport.SetCurrentKey("Record No.");
+
+ if not IntermediateDataImport.FindSet() then
+ exit;
+
+ CurrRecordNo := -1;
+ repeat
+ if CurrRecordNo <> IntermediateDataImport."Record No." then begin
+ if CurrRecordNo <> -1 then
+ if FileName <> '' then begin
+ AttachmentTempBlob.CreateInStream(InStream);
+ EDocAttachmentProcessor.Insert(EDocument, InStream, FileName);
+ FileName := '';
+ end;
+ Clear(AttachmentTempBlob);
+ CurrRecordNo := IntermediateDataImport."Record No.";
+ end;
+
+ case IntermediateDataImport."Field ID" of
+ DocumentAttachment.FieldNo("File Name"):
+ FileName := IntermediateDataImport.Value;
+ DocumentAttachment.FieldNo("Document Reference ID"):
+ begin
+ IntermediateDataImport.CalcFields("Value BLOB");
+ Base64Data := IntermediateDataImport.GetValue();
+ AttachmentTempBlob.CreateOutStream(OutStream);
+ Base64Convert.FromBase64(Base64Data, OutStream);
+ end;
+ end;
+ until IntermediateDataImport.Next() = 0;
+
+ if FileName <> '' then begin
+ AttachmentTempBlob.CreateInStream(InStream);
+ EDocAttachmentProcessor.Insert(EDocument, InStream, FileName);
+ end;
+ end;
+
+ #endregion Attachment Processing
+
+ #region Field Value Helpers
+
+ local procedure AssignFieldValue(var FieldRef: FieldRef; FieldValue: Text)
+ var
+ DateVar: Date;
+ DecimalVar: Decimal;
+ begin
+ case FieldRef.Type of
+ FieldType::Text, FieldType::Code:
+ FieldRef.Value := CopyStr(FieldValue, 1, FieldRef.Length);
+ FieldType::Date:
+ if Evaluate(DateVar, FieldValue, 9) then
+ FieldRef.Value := DateVar;
+ FieldType::Decimal:
+ if Evaluate(DecimalVar, FieldValue, 9) then
+ FieldRef.Value := DecimalVar;
+ end;
+ end;
+
+ local procedure GetFieldMaxLength(FieldRef: FieldRef): Integer
+ begin
+ if FieldRef.Type in [FieldType::Text, FieldType::Code] then
+ exit(FieldRef.Length);
+ exit(250);
+ end;
+
+ ///
+ /// BC convention: blank Currency Code means LCY. Blanks the field when it matches LCY.
+ ///
+ local procedure ApplyLCYBlankConvention(var CurrencyCode: Code[10])
+ var
+ GLSetup: Record "General Ledger Setup";
+ begin
+ if CurrencyCode = '' then
+ exit;
+
+ GLSetup.GetRecordOnce();
+ if GLSetup."LCY Code" = CurrencyCode then
+ CurrencyCode := '';
+ end;
+
+ #endregion Field Value Helpers
+
+ #region Document Type Mapping
+
+ local procedure MapDocumentTypeToProcessDraft(DocumentType: Enum "E-Document Type"): Enum "E-Doc. Process Draft"
+ begin
+ case DocumentType of
+ DocumentType::"Purchase Invoice":
+ exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice");
+ DocumentType::"Purchase Credit Memo":
+ exit(Enum::"E-Doc. Process Draft"::"Purchase Credit Memo");
+ else
+ exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice");
+ end;
+ end;
+
+ #endregion Document Type Mapping
+
+ #region Cleanup
+
+ local procedure DeleteIntermediateData(var DataExch: Record "Data Exch.")
+ var
+ DataExchField: Record "Data Exch. Field";
+ IntermediateDataImport: Record "Intermediate Data Import";
+ begin
+ DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.");
+ DataExchField.DeleteAll();
+ IntermediateDataImport.SetRange("Data Exch. No.", DataExch."Entry No.");
+ IntermediateDataImport.DeleteAll();
+ DataExch.Delete();
+ end;
+
+ #endregion Cleanup
+
+ #region Integration Events
+
+ [IntegrationEvent(false, false)]
+ local procedure OnAfterBridgeToStagingTables(DataExchNo: Integer; var EDocumentPurchaseHeader: Record "E-Document Purchase Header")
+ begin
+ end;
+
+ [IntegrationEvent(false, false)]
+ local procedure OnAfterMapLineToStaging(DataExchNo: Integer; RecordNo: Integer; var EDocumentPurchaseLine: Record "E-Document Purchase Line")
+ begin
+ end;
+
+ #endregion Integration Events
+
+ var
+ ViewNotImplementedErr: Label 'A view is not implemented for this handler.', Comment = 'Error shown when View is called on a handler that does not support viewing.';
+ UnrecognisedNamespaceErr: Label 'No configured Data Exchange Definition matches the XML root namespace ''%1''. Configure a definition with a matching namespace in the E-Document Service setup.', Comment = '%1 = XML root namespace URI';
+ NoDataExchDefsConfiguredErr: Label 'No import Data Exchange Definitions are configured for E-Document Service ''%1''.', Comment = '%1 = E-Document Service code';
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMExtractionPlan.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMExtractionPlan.Codeunit.al
new file mode 100644
index 0000000000..74b3429514
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMExtractionPlan.Codeunit.al
@@ -0,0 +1,115 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+codeunit 6340 "E-Doc. MLLM Extraction Plan"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ SingleInstance = true;
+
+ var
+ ItemStatus: Dictionary of [Text, Text];
+ ItemErrors: Dictionary of [Text, Text];
+ AnalysisPayload: Text;
+ CurrentJson: Text;
+ FixedItemsTok: Label 'verify_invoice_totals,verify_vat,verify_dates,verify_required_fields,verify_ranges,verify_payable', Locked = true;
+
+ procedure Reset()
+ begin
+ Clear(ItemStatus);
+ Clear(ItemErrors);
+ AnalysisPayload := '';
+ CurrentJson := '';
+ end;
+
+ procedure InitializePlan(LineIds: List of [Text]; Analysis: Text)
+ var
+ LineId: Text;
+ FixedIds: List of [Text];
+ FixedId: Text;
+ begin
+ Reset();
+ AnalysisPayload := Analysis;
+ ItemStatus.Add('analyze_invoice', 'passed');
+ foreach LineId in LineIds do
+ ItemStatus.Add('verify_line_' + LineId, 'pending');
+ FixedIds.AddRange(FixedItemsTok.Split(','));
+ foreach FixedId in FixedIds do
+ ItemStatus.Add(FixedId, 'pending');
+ end;
+
+ procedure MarkItem(ItemId: Text; Passed: Boolean; ErrorMsg: Text)
+ var
+ Status: Text;
+ begin
+ if Passed then Status := 'passed' else Status := 'failed';
+ if ItemStatus.ContainsKey(ItemId) then
+ ItemStatus.Set(ItemId, Status)
+ else
+ ItemStatus.Add(ItemId, Status);
+ if not Passed then begin
+ if ItemErrors.ContainsKey(ItemId) then
+ ItemErrors.Set(ItemId, ErrorMsg)
+ else
+ ItemErrors.Add(ItemId, ErrorMsg);
+ end else
+ if ItemErrors.ContainsKey(ItemId) then
+ ItemErrors.Remove(ItemId);
+ end;
+
+ procedure SetCurrentJson(Json: Text)
+ var
+ ItemId, Status : Text;
+ begin
+ CurrentJson := Json;
+ // Reset failed items to pending so the model re-verifies after correction
+ foreach ItemId in ItemStatus.Keys() do begin
+ ItemStatus.Get(ItemId, Status);
+ if Status = 'failed' then
+ ItemStatus.Set(ItemId, 'pending');
+ end;
+ if ItemErrors.Count() > 0 then
+ Clear(ItemErrors);
+ end;
+
+ procedure GetCurrentJson(): Text
+ begin
+ exit(CurrentJson);
+ end;
+
+ procedure HasCurrentJson(): Boolean
+ begin
+ exit(CurrentJson <> '');
+ end;
+
+ procedure GetChecklistJson(): Text
+ var
+ ResultArr: JsonArray;
+ ItemObj: JsonObject;
+ ItemId, Status, ErrorMsg : Text;
+ ResultText: Text;
+ begin
+ foreach ItemId in ItemStatus.Keys() do begin
+ Clear(ItemObj);
+ ItemStatus.Get(ItemId, Status);
+ ItemObj.Add('id', ItemId);
+ ItemObj.Add('status', Status);
+ if (Status = 'failed') and ItemErrors.ContainsKey(ItemId) then begin
+ ItemErrors.Get(ItemId, ErrorMsg);
+ ItemObj.Add('error', ErrorMsg);
+ end;
+ ResultArr.Add(ItemObj);
+ end;
+ ResultArr.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+
+ procedure IsInitialized(): Boolean
+ begin
+ exit(ItemStatus.Count() > 0);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanAnalyzeTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanAnalyzeTool.Codeunit.al
new file mode 100644
index 0000000000..339bde2339
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanAnalyzeTool.Codeunit.al
@@ -0,0 +1,85 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6320 "E-Doc. MLLM Plan Analyze Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('analyze_invoice');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj, FunctionObj, ParamsObj, PropsObj, PropObj : JsonObject;
+ ItemsObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Document type (invoice, credit_memo, etc.)');
+ PropsObj.Add('doc_type', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Document language code (e.g. sv, en, de)');
+ PropsObj.Add('language', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Decimal separator used in this document (. or ,)');
+ PropsObj.Add('decimal_sep', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Thousands separator used in this document (space, . or ,)');
+ PropsObj.Add('thousands_sep', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Description of each line item column and its role (e.g. which column is gross price, discount %, net price, quantity, line total)');
+ PropsObj.Add('line_columns', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Any other observations about the document layout or unusual features');
+ PropsObj.Add('notes', PropObj); Clear(PropObj);
+ ItemsObj.Add('type', 'string');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'IDs of all invoice lines visible on the document (e.g. ["1","2","3"])');
+ PropsObj.Add('line_ids', PropObj);
+ RequiredArr.Add('doc_type'); RequiredArr.Add('language'); RequiredArr.Add('decimal_sep');
+ RequiredArr.Add('thousands_sep'); RequiredArr.Add('line_columns'); RequiredArr.Add('line_ids');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'CALL THIS FIRST before extracting any values. Records your structural analysis of the document and initializes the verification checklist. Returns the full checklist of items to verify.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ ExtractionPlan: Codeunit "E-Doc. MLLM Extraction Plan";
+ ResultObj: JsonObject;
+ LineIdsToken: JsonToken;
+ LineIdToken: JsonToken;
+ LineIds: List of [Text];
+ AnalysisObj: JsonObject;
+ Token: JsonToken;
+ AnalysisText, ResultText : Text;
+ DocType, Language, DecimalSep, ThousandsSep, LineColumns, Notes : Text;
+ begin
+ if Arguments.Get('doc_type', Token) then DocType := Token.AsValue().AsText();
+ if Arguments.Get('language', Token) then Language := Token.AsValue().AsText();
+ if Arguments.Get('decimal_sep', Token) then DecimalSep := Token.AsValue().AsText();
+ if Arguments.Get('thousands_sep', Token) then ThousandsSep := Token.AsValue().AsText();
+ if Arguments.Get('line_columns', Token) then LineColumns := Token.AsValue().AsText();
+ if Arguments.Get('notes', Token) then Notes := Token.AsValue().AsText();
+ if Arguments.Get('line_ids', LineIdsToken) then
+ foreach LineIdToken in LineIdsToken.AsArray() do
+ LineIds.Add(LineIdToken.AsValue().AsText());
+
+ AnalysisObj.Add('doc_type', DocType); AnalysisObj.Add('language', Language);
+ AnalysisObj.Add('decimal_sep', DecimalSep); AnalysisObj.Add('thousands_sep', ThousandsSep);
+ AnalysisObj.Add('line_columns', LineColumns); AnalysisObj.Add('notes', Notes);
+ AnalysisObj.WriteTo(AnalysisText);
+
+ ExtractionPlan.InitializePlan(LineIds, AnalysisText);
+
+ ResultObj.Add('status', 'analysis_recorded');
+ ResultObj.Add('checklist', ExtractionPlan.GetChecklistJson());
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanMarkTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanMarkTool.Codeunit.al
new file mode 100644
index 0000000000..4d04fc50ba
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanMarkTool.Codeunit.al
@@ -0,0 +1,72 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6344 "E-Doc. MLLM Plan Mark Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('mark_item');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj, FunctionObj, ParamsObj, PropsObj, PropObj : JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string');
+ PropObj.Add('description', 'The checklist item id to mark (e.g. "verify_line_1", "verify_invoice_totals")');
+ PropsObj.Add('item_id', PropObj);
+ Clear(PropObj);
+ PropObj.Add('type', 'boolean');
+ PropObj.Add('description', 'true if the verification passed, false if it failed');
+ PropsObj.Add('passed', PropObj);
+ Clear(PropObj);
+ PropObj.Add('type', 'string');
+ PropObj.Add('description', 'Error message if passed=false, empty string if passed=true');
+ PropsObj.Add('error', PropObj);
+ RequiredArr.Add('item_id');
+ RequiredArr.Add('passed');
+ RequiredArr.Add('error');
+ ParamsObj.Add('type', 'object');
+ ParamsObj.Add('properties', PropsObj);
+ ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Record the result of a verification on the checklist. Call this after every verify tool call to mark the item as passed or failed.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function');
+ ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ ExtractionPlan: Codeunit "E-Doc. MLLM Extraction Plan";
+ ResultObj: JsonObject;
+ Token: JsonToken;
+ ItemId, ErrorMsg, ResultText : Text;
+ Passed: Boolean;
+ begin
+ if Arguments.Get('item_id', Token) then ItemId := Token.AsValue().AsText();
+ if Arguments.Get('passed', Token) then Passed := Token.AsValue().AsBoolean();
+ if Arguments.Get('error', Token) then ErrorMsg := Token.AsValue().AsText();
+
+ ExtractionPlan.MarkItem(ItemId, Passed, ErrorMsg);
+
+ ResultObj.Add('item_id', ItemId);
+ if Passed then
+ ResultObj.Add('status', 'passed')
+ else
+ ResultObj.Add('status', 'failed');
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanStatusTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanStatusTool.Codeunit.al
new file mode 100644
index 0000000000..7aeef4921a
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanStatusTool.Codeunit.al
@@ -0,0 +1,40 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6341 "E-Doc. MLLM Plan Status Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('get_checklist');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj, FunctionObj, ParamsObj, PropsObj : JsonObject;
+ begin
+ ParamsObj.Add('type', 'object');
+ ParamsObj.Add('properties', PropsObj);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Returns the current verification checklist showing status (pending/passed/failed) for each item. Call this to see what verifications remain before finalising.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function');
+ ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ ExtractionPlan: Codeunit "E-Doc. MLLM Extraction Plan";
+ begin
+ exit(ExtractionPlan.GetChecklistJson());
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanSubmitTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanSubmitTool.Codeunit.al
new file mode 100644
index 0000000000..4e814d7129
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMPlanSubmitTool.Codeunit.al
@@ -0,0 +1,55 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6346 "E-Doc. MLLM Plan Submit Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('submit_extraction');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj, FunctionObj, ParamsObj, PropsObj, PropObj : JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string');
+ PropObj.Add('description', 'The complete UBL JSON you have extracted. Must be the full document, not a snippet.');
+ PropsObj.Add('json', PropObj);
+ RequiredArr.Add('json');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Save the current complete UBL JSON extraction. Call this after Phase 2 and after every correction. The saved JSON is the final result — do not output JSON as a text response.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ ExtractionPlan: Codeunit "E-Doc. MLLM Extraction Plan";
+ ResultObj: JsonObject;
+ Token: JsonToken;
+ Json, ResultText : Text;
+ begin
+ if Arguments.Get('json', Token) then
+ if Token.IsObject() then
+ Token.WriteTo(Json) // model passed the JSON as an object — serialize it
+ else
+ Json := Token.AsValue().AsText(); // model passed it as a string — use as-is
+ ExtractionPlan.SetCurrentJson(Json);
+ ResultObj.Add('status', 'saved');
+ ResultObj.Add('checklist', ExtractionPlan.GetChecklistJson());
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al
index 93468d3d62..eb4b521c3c 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al
@@ -133,6 +133,7 @@ codeunit 6232 "E-Doc. MLLM Schema Helper"
NestedObj: JsonObject;
NestedObj2: JsonObject;
LineNumber: Integer;
+ DiscountPct: Decimal;
begin
TempLine.DeleteAll();
@@ -164,9 +165,16 @@ codeunit 6232 "E-Doc. MLLM Schema Helper"
GetDecimal(LineObj, 'line_extension_amount', TempLine."Sub Total");
- if GetNestedObject(LineObj, 'allowance_charge', NestedObj) then
- if GetNestedObject(NestedObj, 'amount', NestedObj2) then
- GetDecimal(NestedObj2, 'value', TempLine."Total Discount");
+ if GetNestedObject(LineObj, 'allowance_charge', NestedObj) then begin
+ DiscountPct := 0;
+ GetDecimal(NestedObj, 'percent', DiscountPct);
+ if DiscountPct <> 0 then
+ TempLine."Total Discount" := TempLine."Unit Price" * TempLine.Quantity * DiscountPct / 100
+ else begin
+ if GetNestedObject(NestedObj, 'amount', NestedObj2) then
+ GetDecimal(NestedObj2, 'value', TempLine."Total Discount");
+ end;
+ end;
TempLine.Insert();
end;
@@ -208,12 +216,14 @@ codeunit 6232 "E-Doc. MLLM Schema Helper"
local procedure GetDecimal(JsonObj: JsonObject; PropertyName: Text; var FieldValue: Decimal)
var
JsonToken: JsonToken;
+ DecimalValue: Decimal;
begin
if not JsonObj.Get(PropertyName, JsonToken) then
exit;
if JsonToken.AsValue().IsNull() then
exit;
- FieldValue := JsonToken.AsValue().AsDecimal();
+ if Evaluate(DecimalValue, JsonToken.AsValue().AsText(), 9) then
+ FieldValue := DecimalValue;
end;
local procedure GetNestedObject(JsonObj: JsonObject; PropertyName: Text; var NestedObj: JsonObject): Boolean
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLDatesTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLDatesTool.Codeunit.al
new file mode 100644
index 0000000000..15688ee0ee
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLDatesTool.Codeunit.al
@@ -0,0 +1,65 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6315 "E-Doc. MLLM VL Dates Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_dates');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'issue_date in YYYY-MM-DD format');
+ PropsObj.Add('issue_date', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'due_date in YYYY-MM-DD format, or empty string if not present');
+ PropsObj.Add('due_date', PropObj);
+ RequiredArr.Add('issue_date'); RequiredArr.Add('due_date');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that issue_date and due_date are valid XML dates (YYYY-MM-DD), year 1900-2100, and due_date >= issue_date if present.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ ResultText: Text;
+ IssueDate: Text;
+ DueDate: Text;
+ Token: JsonToken;
+ Passed: Boolean;
+ begin
+ if Arguments.Get('issue_date', Token) then IssueDate := Token.AsValue().AsText();
+ if Arguments.Get('due_date', Token) then DueDate := Token.AsValue().AsText();
+ Passed := VerifyTools.VerifyDates(IssueDate, DueDate, ErrorText);
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLMathTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLMathTool.Codeunit.al
new file mode 100644
index 0000000000..cf404e79a2
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLMathTool.Codeunit.al
@@ -0,0 +1,104 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6339 "E-Doc. MLLM VL Math Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_line_math');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string');
+ PropObj.Add('description', 'The id field of the invoice line being verified');
+ PropsObj.Add('line_id', PropObj);
+ Clear(PropObj);
+ PropObj.Add('type', 'number');
+ PropObj.Add('description', 'Gross unit price before discounts');
+ PropsObj.Add('unit_price', PropObj);
+ Clear(PropObj);
+ PropObj.Add('type', 'number');
+ PropObj.Add('description', 'Quantity of units');
+ PropsObj.Add('quantity', PropObj);
+ Clear(PropObj);
+ PropObj.Add('type', 'number');
+ PropObj.Add('description', 'Combined discount percentage 0-100 (use 0 if no discount)');
+ PropsObj.Add('discount_pct', PropObj);
+ Clear(PropObj);
+ PropObj.Add('type', 'number');
+ PropObj.Add('description', 'line_extension_amount from the invoice');
+ PropsObj.Add('line_extension_amount', PropObj);
+ RequiredArr.Add('line_id');
+ RequiredArr.Add('unit_price');
+ RequiredArr.Add('quantity');
+ RequiredArr.Add('discount_pct');
+ RequiredArr.Add('line_extension_amount');
+ ParamsObj.Add('type', 'object');
+ ParamsObj.Add('properties', PropsObj);
+ ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that gross_unit_price × quantity × (1 − discount_pct/100) matches line_extension_amount within 1%. Call once per invoice line.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function');
+ ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText, ResultText, LineId : Text;
+ UnitPrice, Quantity, DiscountPct, LineExtAmt : Decimal;
+ Token: JsonToken;
+ Passed: Boolean;
+ begin
+ GetDecimalArg(Arguments, 'unit_price', UnitPrice);
+ GetDecimalArg(Arguments, 'quantity', Quantity);
+ GetDecimalArg(Arguments, 'discount_pct', DiscountPct);
+ GetDecimalArg(Arguments, 'line_extension_amount', LineExtAmt);
+ if Arguments.Get('line_id', Token) then
+ LineId := Token.AsValue().AsText();
+
+ Passed := VerifyTools.VerifyLineMath(UnitPrice, Quantity, DiscountPct, LineExtAmt, ErrorText);
+
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+
+ local procedure GetDecimalArg(Arguments: JsonObject; PropertyName: Text; var Value: Decimal)
+ var
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ begin
+ if not Arguments.Get(PropertyName, Token) then
+ exit;
+ if Token.AsValue().IsNull() then
+ exit;
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then
+ Value := DecimalValue;
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLPayableTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLPayableTool.Codeunit.al
new file mode 100644
index 0000000000..945b4497dc
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLPayableTool.Codeunit.al
@@ -0,0 +1,69 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6345 "E-Doc. MLLM VL Payable Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_payable');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj, FunctionObj, ParamsObj, PropsObj, PropObj : JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_exclusive_amount from legal_monetary_total');
+ PropsObj.Add('tax_exclusive_amount', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_amount from tax_total');
+ PropsObj.Add('tax_amount', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'payable_amount from legal_monetary_total');
+ PropsObj.Add('payable_amount', PropObj);
+ RequiredArr.Add('tax_exclusive_amount'); RequiredArr.Add('tax_amount'); RequiredArr.Add('payable_amount');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that tax_exclusive_amount + tax_amount ≈ payable_amount within 1%.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ExtractionPlan: Codeunit "E-Doc. MLLM Extraction Plan";
+ ResultObj: JsonObject;
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ TaxExcl, TaxAmt, Payable : Decimal;
+ ErrorText, ResultText : Text;
+ Passed: Boolean;
+ begin
+ if Arguments.Get('tax_exclusive_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then TaxExcl := DecimalValue;
+ if Arguments.Get('tax_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then TaxAmt := DecimalValue;
+ if Arguments.Get('payable_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then Payable := DecimalValue;
+
+ Passed := VerifyTools.VerifyPayable(TaxExcl, TaxAmt, Payable, ErrorText);
+
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLRangesTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLRangesTool.Codeunit.al
new file mode 100644
index 0000000000..d5d5b018bf
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLRangesTool.Codeunit.al
@@ -0,0 +1,90 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6317 "E-Doc. MLLM VL Ranges Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_ranges');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ ItemsObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line quantities');
+ PropsObj.Add('quantities', PropObj); Clear(PropObj); Clear(ItemsObj);
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line unit prices');
+ PropsObj.Add('prices', PropObj); Clear(PropObj); Clear(ItemsObj);
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line VAT rates (0-100)');
+ PropsObj.Add('vat_rates', PropObj); Clear(PropObj); Clear(ItemsObj);
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line discount percentages (0-100)');
+ PropsObj.Add('discount_pcts', PropObj);
+ RequiredArr.Add('quantities'); RequiredArr.Add('prices'); RequiredArr.Add('vat_rates'); RequiredArr.Add('discount_pcts');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that quantities > 0, unit prices > 0, VAT rates 0-100, discount percentages 0-100 for all lines.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ ResultText: Text;
+ Quantities: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscountPcts: List of [Decimal];
+ Passed: Boolean;
+ begin
+ ParseDecimalArray(Arguments, 'quantities', Quantities);
+ ParseDecimalArray(Arguments, 'prices', Prices);
+ ParseDecimalArray(Arguments, 'vat_rates', VATRates);
+ ParseDecimalArray(Arguments, 'discount_pcts', DiscountPcts);
+ Passed := VerifyTools.VerifyRanges(Quantities, Prices, VATRates, DiscountPcts, ErrorText);
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+
+ local procedure ParseDecimalArray(Arguments: JsonObject; PropertyName: Text; var Values: List of [Decimal])
+ var
+ ArrayToken: JsonToken;
+ ItemToken: JsonToken;
+ DecimalValue: Decimal;
+ begin
+ if not Arguments.Get(PropertyName, ArrayToken) then
+ exit;
+ foreach ItemToken in ArrayToken.AsArray() do
+ if Evaluate(DecimalValue, ItemToken.AsValue().AsText(), 9) then
+ Values.Add(DecimalValue);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLRequiredTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLRequiredTool.Codeunit.al
new file mode 100644
index 0000000000..3c600be871
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLRequiredTool.Codeunit.al
@@ -0,0 +1,72 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6316 "E-Doc. MLLM VL Required Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_required_fields');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Supplier/vendor company name');
+ PropsObj.Add('vendor_name', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'string'); PropObj.Add('description', 'Invoice number / id');
+ PropsObj.Add('invoice_no', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'integer'); PropObj.Add('description', 'Number of invoice lines extracted');
+ PropsObj.Add('line_count', PropObj);
+ RequiredArr.Add('vendor_name'); RequiredArr.Add('invoice_no'); RequiredArr.Add('line_count');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that vendor name, invoice number, and at least one invoice line are present.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ ResultText: Text;
+ VendorName: Text;
+ InvoiceNo: Text;
+ LineCount: Integer;
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ Passed: Boolean;
+ begin
+ if Arguments.Get('vendor_name', Token) then VendorName := Token.AsValue().AsText();
+ if Arguments.Get('invoice_no', Token) then InvoiceNo := Token.AsValue().AsText();
+ if Arguments.Get('line_count', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then
+ LineCount := Round(DecimalValue, 1);
+ Passed := VerifyTools.VerifyRequiredFields(VendorName, InvoiceNo, LineCount, ErrorText);
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLTotalsTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLTotalsTool.Codeunit.al
new file mode 100644
index 0000000000..ebc815cb66
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLTotalsTool.Codeunit.al
@@ -0,0 +1,83 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6313 "E-Doc. MLLM VL Totals Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_invoice_totals');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ ItemsObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ ItemsObj.Add('type', 'number');
+ PropObj.Add('type', 'array'); PropObj.Add('items', ItemsObj); PropObj.Add('description', 'All line_extension_amount values');
+ PropsObj.Add('line_amounts', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_exclusive_amount from legal_monetary_total');
+ PropsObj.Add('tax_exclusive_amount', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'allowance_total_amount from legal_monetary_total (header-level discount, 0 if none)');
+ PropsObj.Add('allowance_total_amount', PropObj);
+ RequiredArr.Add('line_amounts'); RequiredArr.Add('tax_exclusive_amount'); RequiredArr.Add('allowance_total_amount');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that the sum of all line_extension_amounts matches tax_exclusive_amount within 1%.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ ResultText: Text;
+ LineAmountsToken: JsonToken;
+ LineAmountsArray: JsonArray;
+ LineToken: JsonToken;
+ LineAmounts: List of [Decimal];
+ TaxExclusiveAmount: Decimal;
+ AllowanceTotalAmount: Decimal;
+ DecimalValue: Decimal;
+ Token: JsonToken;
+ Passed: Boolean;
+ begin
+ if Arguments.Get('line_amounts', LineAmountsToken) then begin
+ LineAmountsArray := LineAmountsToken.AsArray();
+ foreach LineToken in LineAmountsArray do
+ if Evaluate(DecimalValue, LineToken.AsValue().AsText(), 9) then
+ LineAmounts.Add(DecimalValue);
+ end;
+ if Arguments.Get('tax_exclusive_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then
+ TaxExclusiveAmount := DecimalValue;
+ if Arguments.Get('allowance_total_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then AllowanceTotalAmount := DecimalValue;
+ Passed := VerifyTools.VerifyInvoiceTotals(LineAmounts, TaxExclusiveAmount, AllowanceTotalAmount, ErrorText);
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLVATTool.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLVATTool.Codeunit.al
new file mode 100644
index 0000000000..fcc7118abd
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVLVATTool.Codeunit.al
@@ -0,0 +1,73 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using System.AI;
+
+codeunit 6314 "E-Doc. MLLM VL VAT Tool" implements "AOAI Function"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure GetName(): Text
+ begin
+ exit('verify_vat');
+ end;
+
+ procedure GetPrompt(): JsonObject
+ var
+ ToolObj: JsonObject;
+ FunctionObj: JsonObject;
+ ParamsObj: JsonObject;
+ PropsObj: JsonObject;
+ PropObj: JsonObject;
+ RequiredArr: JsonArray;
+ begin
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_exclusive_amount');
+ PropsObj.Add('tax_exclusive_amount', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'VAT rate percentage 0-100');
+ PropsObj.Add('vat_rate', PropObj); Clear(PropObj);
+ PropObj.Add('type', 'number'); PropObj.Add('description', 'tax_amount');
+ PropsObj.Add('tax_amount', PropObj);
+ RequiredArr.Add('tax_exclusive_amount'); RequiredArr.Add('vat_rate'); RequiredArr.Add('tax_amount');
+ ParamsObj.Add('type', 'object'); ParamsObj.Add('properties', PropsObj); ParamsObj.Add('required', RequiredArr);
+ FunctionObj.Add('name', GetName());
+ FunctionObj.Add('description', 'Verify that tax_exclusive_amount × vat_rate/100 ≈ tax_amount within 1%.');
+ FunctionObj.Add('parameters', ParamsObj);
+ ToolObj.Add('type', 'function'); ToolObj.Add('function', FunctionObj);
+ exit(ToolObj);
+ end;
+
+ procedure Execute(Arguments: JsonObject): Variant
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ResultObj: JsonObject;
+ ErrorText: Text;
+ ResultText: Text;
+ TaxExcl: Decimal;
+ VATRate: Decimal;
+ TaxAmt: Decimal;
+ Token: JsonToken;
+ DecimalValue: Decimal;
+ Passed: Boolean;
+ begin
+ if Arguments.Get('tax_exclusive_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then TaxExcl := DecimalValue;
+ if Arguments.Get('vat_rate', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then VATRate := DecimalValue;
+ if Arguments.Get('tax_amount', Token) then
+ if Evaluate(DecimalValue, Token.AsValue().AsText(), 9) then TaxAmt := DecimalValue;
+ Passed := VerifyTools.VerifyVAT(TaxExcl, VATRate, TaxAmt, ErrorText);
+ if Passed then
+ ResultObj.Add('pass', true)
+ else begin
+ ResultObj.Add('pass', false);
+ ResultObj.Add('error', ErrorText);
+ end;
+ ResultObj.WriteTo(ResultText);
+ exit(ResultText);
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyTools.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyTools.Codeunit.al
new file mode 100644
index 0000000000..7ad456fa4e
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMVerifyTools.Codeunit.al
@@ -0,0 +1,195 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+codeunit 6311 "E-Doc. MLLM Verify Tools"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ procedure VerifyLineMath(UnitPrice: Decimal; Quantity: Decimal; DiscountPct: Decimal; LineExtensionAmount: Decimal; var ErrorText: Text): Boolean
+ var
+ Expected: Decimal;
+ ImpliedGrossPrice: Decimal;
+ LineMathErrLbl: Label '%1 × %2 × (1 − %3/100) = %4, but line_extension_amount = %5. If discount_pct = %3 is correct, unit_price should be ≈ %6. Verify that unit_price is the pre-discount price and that discount_pct is the combined effective discount.', Comment = '%1=UnitPrice, %2=Quantity, %3=DiscountPct, %4=Expected, %5=LineExtensionAmount, %6=ImpliedGrossPrice';
+ begin
+ if LineExtensionAmount = 0 then
+ exit(true);
+
+ Expected := UnitPrice * Quantity * (1 - DiscountPct / 100);
+
+ if IsWithinTolerance(Expected, LineExtensionAmount) then
+ exit(true);
+
+ if (Quantity <> 0) and (DiscountPct < 100) then
+ ImpliedGrossPrice := Round(LineExtensionAmount / (Quantity * (1 - DiscountPct / 100)), 0.001)
+ else
+ ImpliedGrossPrice := 0;
+
+ ErrorText := StrSubstNo(LineMathErrLbl, UnitPrice, Quantity, DiscountPct, Round(Expected, 0.01), LineExtensionAmount, ImpliedGrossPrice);
+ exit(false);
+ end;
+
+ procedure VerifyInvoiceTotals(LineAmounts: List of [Decimal]; TaxExclusiveAmount: Decimal; AllowanceTotalAmount: Decimal; var ErrorText: Text): Boolean
+ var
+ LineAmount: Decimal;
+ SumOfLines: Decimal;
+ InvoiceTotalsErrLbl: Label 'Sum of lines (%1) minus header discount (%2) = %3, but tax_exclusive_amount = %4. Check for missing lines or incorrect header discount.', Comment = '%1=SumOfLines, %2=AllowanceTotalAmount, %3=Net, %4=TaxExclusiveAmount';
+ begin
+ if TaxExclusiveAmount = 0 then
+ exit(true);
+
+ SumOfLines := 0;
+ foreach LineAmount in LineAmounts do
+ SumOfLines += LineAmount;
+
+ if IsWithinTolerance(SumOfLines - AllowanceTotalAmount, TaxExclusiveAmount) then
+ exit(true);
+
+ ErrorText := StrSubstNo(InvoiceTotalsErrLbl, Round(SumOfLines, 0.01), AllowanceTotalAmount, Round(SumOfLines - AllowanceTotalAmount, 0.01), TaxExclusiveAmount);
+ exit(false);
+ end;
+
+ procedure VerifyVAT(TaxExclusiveAmount: Decimal; VATRate: Decimal; TaxAmount: Decimal; var ErrorText: Text): Boolean
+ var
+ Expected: Decimal;
+ VATErrLbl: Label '%1 × %2% = %3, but tax_amount = %4. Re-check the VAT rate.', Comment = '%1=TaxExclusiveAmount, %2=VATRate, %3=Expected, %4=TaxAmount';
+ begin
+ if TaxAmount = 0 then
+ exit(true);
+
+ Expected := TaxExclusiveAmount * VATRate / 100;
+
+ if IsWithinTolerance(Expected, TaxAmount) then
+ exit(true);
+
+ ErrorText := StrSubstNo(VATErrLbl, TaxExclusiveAmount, VATRate, Expected, TaxAmount);
+ exit(false);
+ end;
+
+ procedure VerifyDates(IssueDateText: Text; DueDateText: Text; var ErrorText: Text): Boolean
+ var
+ IssueDate: Date;
+ DueDate: Date;
+ MissingIssueDateErrLbl: Label 'issue_date is missing.';
+ InvalidIssueDateErrLbl: Label 'issue_date ''%1'' is not a valid date.', Comment = '%1=IssueDateText';
+ IssueDateYearErrLbl: Label 'issue_date ''%1'' has year %2 which is outside the expected range 1900–2100.', Comment = '%1=IssueDateText, %2=Year';
+ InvalidDueDateErrLbl: Label 'due_date ''%1'' is not a valid date.', Comment = '%1=DueDateText';
+ DueDateBeforeIssueDateErrLbl: Label 'due_date %1 is before issue_date %2.', Comment = '%1=DueDate, %2=IssueDate';
+ begin
+ if IssueDateText = '' then begin
+ ErrorText := MissingIssueDateErrLbl;
+ exit(false);
+ end;
+
+ if not Evaluate(IssueDate, IssueDateText, 9) then begin
+ ErrorText := StrSubstNo(InvalidIssueDateErrLbl, IssueDateText);
+ exit(false);
+ end;
+
+ if (Date2DMY(IssueDate, 3) < 1900) or (Date2DMY(IssueDate, 3) > 2100) then begin
+ ErrorText := StrSubstNo(IssueDateYearErrLbl, IssueDateText, Date2DMY(IssueDate, 3));
+ exit(false);
+ end;
+
+ if DueDateText <> '' then begin
+ if not Evaluate(DueDate, DueDateText, 9) then begin
+ ErrorText := StrSubstNo(InvalidDueDateErrLbl, DueDateText);
+ exit(false);
+ end;
+
+ if DueDate < IssueDate then begin
+ ErrorText := StrSubstNo(DueDateBeforeIssueDateErrLbl, DueDate, IssueDate);
+ exit(false);
+ end;
+ end;
+
+ exit(true);
+ end;
+
+ procedure VerifyRequiredFields(VendorName: Text; InvoiceNo: Text; LineCount: Integer; var ErrorText: Text): Boolean
+ var
+ Missing: Text;
+ MissingFieldsErrLbl: Label 'Missing required fields: %1', Comment = '%1=comma-separated list of missing fields';
+ VendorNameLbl: Label 'vendor name', Locked = true;
+ InvoiceNumberLbl: Label 'invoice number', Locked = true;
+ InvoiceLineCountLbl: Label 'invoice lines (line_count = 0)', Locked = true;
+ begin
+ Missing := '';
+
+ if VendorName = '' then
+ AppendMissing(Missing, VendorNameLbl);
+
+ if InvoiceNo = '' then
+ AppendMissing(Missing, InvoiceNumberLbl);
+
+ if LineCount = 0 then
+ AppendMissing(Missing, InvoiceLineCountLbl);
+
+ if Missing = '' then
+ exit(true);
+
+ ErrorText := StrSubstNo(MissingFieldsErrLbl, Missing);
+ exit(false);
+ end;
+
+ procedure VerifyRanges(Quantities: List of [Decimal]; Prices: List of [Decimal]; VATRates: List of [Decimal]; DiscountPcts: List of [Decimal]; var ErrorText: Text): Boolean
+ var
+ i: Integer;
+ Value: Decimal;
+ VATRateRangeErrLbl: Label 'Line %1: VAT rate %2 is outside the range 0–100.', Comment = '%1=LineIndex, %2=Value';
+ DiscountRangeErrLbl: Label 'Line %1: discount %2 is outside the range 0–100.', Comment = '%1=LineIndex, %2=Value';
+ begin
+ for i := 1 to VATRates.Count() do begin
+ VATRates.Get(i, Value);
+ if (Value < 0) or (Value > 100) then begin
+ ErrorText := StrSubstNo(VATRateRangeErrLbl, i, Value);
+ exit(false);
+ end;
+ end;
+
+ for i := 1 to DiscountPcts.Count() do begin
+ DiscountPcts.Get(i, Value);
+ if (Value < 0) or (Value > 100) then begin
+ ErrorText := StrSubstNo(DiscountRangeErrLbl, i, Value);
+ exit(false);
+ end;
+ end;
+
+ exit(true);
+ end;
+
+ procedure VerifyPayable(TaxExclusiveAmount: Decimal; TaxAmount: Decimal; PayableAmount: Decimal; var ErrorText: Text): Boolean
+ var
+ Expected: Decimal;
+ PayableErrLbl: Label '%1 (tax_exclusive) + %2 (tax_amount) = %3, but payable_amount = %4.', Comment = '%1=TaxExclusiveAmount, %2=TaxAmount, %3=Expected, %4=PayableAmount';
+ begin
+ if PayableAmount = 0 then
+ exit(true);
+ Expected := TaxExclusiveAmount + TaxAmount;
+ if IsWithinTolerance(Expected, PayableAmount) then
+ exit(true);
+ ErrorText := StrSubstNo(PayableErrLbl, TaxExclusiveAmount, TaxAmount, Round(Expected, 0.01), PayableAmount);
+ exit(false);
+ end;
+
+ procedure IsWithinTolerance(Expected: Decimal; Actual: Decimal): Boolean
+ var
+ Denominator: Decimal;
+ begin
+ Denominator := Abs(Actual);
+ if Denominator < 1 then
+ Denominator := 1;
+ exit(Abs(Expected - Actual) / Denominator < 0.01);
+ end;
+
+ local procedure AppendMissing(var Missing: Text; Field: Text)
+ begin
+ if Missing <> '' then
+ Missing += ', ';
+ Missing += Field;
+ end;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocPEPPOLDXPostMapping.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocPEPPOLDXPostMapping.Codeunit.al
new file mode 100644
index 0000000000..3fd384924b
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocPEPPOLDXPostMapping.Codeunit.al
@@ -0,0 +1,228 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using System.IO;
+
+///
+/// Post-Mapping codeunit for PEPPOL BIS 3.0 Data Exchange import.
+/// Registered as PostMappingCodeunit on the header DataExchMapping in the V2 definitions.
+/// Runs inside ProcessDataExchange after the DataHandlingCodeunit (1214) has populated
+/// Intermediate Data Import. Handles two things:
+///
+/// 1. Compound header fields (schemeID:value endpoint identifier) that cannot be
+/// expressed as a declarative field mapping.
+///
+/// 2. Document-level AllowanceCharge elements (PEPPOLCHARGELINES line def): writes
+/// them directly into Intermediate Data Import with Record Nos. above the existing
+/// invoice/credit-note lines, so the generic MapIntermediateToLines bridge picks
+/// them up in the correct order.
+///
+codeunit 6408 "E-Doc. PEPPOL DX Post-Mapping"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ TableNo = "Data Exch.";
+
+ trigger OnRun()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentRecordId: RecordId;
+ begin
+ EDocumentRecordId := Rec."Related Record";
+ EDocument := EDocumentRecordId.GetRecord();
+
+ EDocumentPurchaseHeader.GetFromEDocument(EDocument);
+ BuildEndpointIdentifiers(Rec, EDocumentPurchaseHeader);
+ EDocumentPurchaseHeader.Modify();
+
+ WriteChargeLinesToIntermediate(Rec);
+ end;
+
+ #region Endpoint Identifiers
+
+ local procedure BuildEndpointIdentifiers(DataExch: Record "Data Exch."; var EDocumentPurchaseHeader: Record "E-Document Purchase Header")
+ var
+ DataExchField: Record "Data Exch. Field";
+ DataExchColumnDef: Record "Data Exch. Column Def";
+ EndpointValue: Text;
+ EndpointScheme: Text;
+ ValueColNo: Integer;
+ SchemeColNo: Integer;
+ begin
+ DataExchColumnDef.SetRange("Data Exch. Def Code", DataExch."Data Exch. Def Code");
+ DataExchColumnDef.SetRange(Name, 'CustomerEndpointID');
+ if DataExchColumnDef.FindFirst() then
+ ValueColNo := DataExchColumnDef."Column No.";
+
+ DataExchColumnDef.SetRange(Name, 'CustomerEndpointSchemeID');
+ if DataExchColumnDef.FindFirst() then
+ SchemeColNo := DataExchColumnDef."Column No.";
+
+ if (ValueColNo = 0) or (SchemeColNo = 0) then
+ exit;
+
+ DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.");
+ DataExchField.SetRange("Column No.", ValueColNo);
+ if DataExchField.FindFirst() then
+ EndpointValue := DataExchField.Value;
+
+ DataExchField.SetRange("Column No.", SchemeColNo);
+ if DataExchField.FindFirst() then
+ EndpointScheme := DataExchField.Value;
+
+ if (EndpointValue <> '') and (EndpointScheme <> '') then
+ EDocumentPurchaseHeader."Customer Company Id" :=
+ CopyStr(EndpointScheme + ':' + EndpointValue, 1, MaxStrLen(EDocumentPurchaseHeader."Customer Company Id"));
+ end;
+
+ #endregion Endpoint Identifiers
+
+ #region Charge Lines via Intermediate
+
+ ///
+ /// Reads PEPPOLCHARGELINES Data Exch. Field records and writes charge line data
+ /// into Intermediate Data Import (Table ID 6101) with Record Nos. above the existing
+ /// invoice/credit-note lines. The generic MapIntermediateToLines bridge then processes
+ /// them in Record No. order, placing charge lines after invoice lines.
+ ///
+ local procedure WriteChargeLinesToIntermediate(DataExch: Record "Data Exch.")
+ var
+ DataExchField: Record "Data Exch. Field";
+ DataExchColumnDef: Record "Data Exch. Column Def";
+ IntermediateDataImport: Record "Intermediate Data Import";
+ DescColNo: Integer;
+ AmountColNo: Integer;
+ VATRateColNo: Integer;
+ CurrencyColNo: Integer;
+ IndicatorColNo: Integer;
+ ParentRecordNo: Integer;
+ NextRecordNo: Integer;
+ CurrLineNo: Integer;
+ IsCharge: Boolean;
+ Description: Text[100];
+ Amount: Text[250];
+ VATRate: Text[250];
+ CurrencyCode: Text[250];
+ begin
+ DataExchColumnDef.SetRange("Data Exch. Def Code", DataExch."Data Exch. Def Code");
+ DataExchColumnDef.SetRange("Data Exch. Line Def Code", ChargeLineDefCodeTok);
+ DataExchColumnDef.SetRange(Name, 'ChargeDescription');
+ if DataExchColumnDef.FindFirst() then DescColNo := DataExchColumnDef."Column No.";
+ DataExchColumnDef.SetRange(Name, 'ChargeAmount');
+ if DataExchColumnDef.FindFirst() then AmountColNo := DataExchColumnDef."Column No.";
+ DataExchColumnDef.SetRange(Name, 'ChargeVATRate');
+ if DataExchColumnDef.FindFirst() then VATRateColNo := DataExchColumnDef."Column No.";
+ DataExchColumnDef.SetRange(Name, 'ChargeCurrencyCode');
+ if DataExchColumnDef.FindFirst() then CurrencyColNo := DataExchColumnDef."Column No.";
+ DataExchColumnDef.SetRange(Name, 'ChargeIndicator');
+ if DataExchColumnDef.FindFirst() then IndicatorColNo := DataExchColumnDef."Column No.";
+
+ if DescColNo = 0 then
+ exit;
+
+ // Find the header's intermediate Record No. to use as Parent Record No. for lines.
+ IntermediateDataImport.SetRange("Data Exch. No.", DataExch."Entry No.");
+ IntermediateDataImport.SetRange("Table ID", Database::"E-Document Purchase Header");
+ IntermediateDataImport.SetRange("Parent Record No.", 0);
+ if not IntermediateDataImport.FindFirst() then
+ exit;
+ ParentRecordNo := IntermediateDataImport."Record No.";
+
+ // Start charge lines after all existing purchase line intermediate records.
+ IntermediateDataImport.SetRange("Table ID", Database::"E-Document Purchase Line");
+ IntermediateDataImport.SetRange("Parent Record No.");
+ if IntermediateDataImport.FindLast() then
+ NextRecordNo := IntermediateDataImport."Record No." + 1
+ else
+ NextRecordNo := 1;
+
+ DataExchField.SetRange("Data Exch. No.", DataExch."Entry No.");
+ DataExchField.SetRange("Data Exch. Line Def Code", ChargeLineDefCodeTok);
+ DataExchField.SetCurrentKey("Line No.", "Column No.");
+ if not DataExchField.FindSet() then
+ exit;
+
+ CurrLineNo := -1;
+ IsCharge := false;
+ repeat
+ if CurrLineNo <> DataExchField."Line No." then begin
+ if (CurrLineNo <> -1) and IsCharge then
+ InsertChargeLineIntermediate(DataExch."Entry No.", NextRecordNo, ParentRecordNo,
+ Description, Amount, VATRate, CurrencyCode);
+ CurrLineNo := DataExchField."Line No.";
+ IsCharge := false;
+ Clear(Description);
+ Clear(Amount);
+ Clear(VATRate);
+ Clear(CurrencyCode);
+ end;
+
+ case DataExchField."Column No." of
+ DescColNo:
+ Description := CopyStr(DataExchField.Value, 1, MaxStrLen(Description));
+ AmountColNo:
+ Amount := DataExchField.Value;
+ VATRateColNo:
+ VATRate := DataExchField.Value;
+ CurrencyColNo:
+ CurrencyCode := DataExchField.Value;
+ IndicatorColNo:
+ IsCharge := LowerCase(DataExchField.Value) = 'true';
+ end;
+ until DataExchField.Next() = 0;
+
+ if (CurrLineNo <> -1) and IsCharge then
+ InsertChargeLineIntermediate(DataExch."Entry No.", NextRecordNo, ParentRecordNo,
+ Description, Amount, VATRate, CurrencyCode);
+ end;
+
+ local procedure InsertChargeLineIntermediate(DataExchNo: Integer; var NextRecordNo: Integer; ParentRecordNo: Integer; Description: Text[100]; Amount: Text[250]; VATRate: Text[250]; CurrencyCode: Text[250])
+ var
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ begin
+ InsertIntermediateField(DataExchNo, NextRecordNo, ParentRecordNo,
+ EDocumentPurchaseLine.FieldNo(Quantity), '1');
+ if Description <> '' then
+ InsertIntermediateField(DataExchNo, NextRecordNo, ParentRecordNo,
+ EDocumentPurchaseLine.FieldNo(Description), Description);
+ if Amount <> '' then begin
+ InsertIntermediateField(DataExchNo, NextRecordNo, ParentRecordNo,
+ EDocumentPurchaseLine.FieldNo("Unit Price"), Amount);
+ InsertIntermediateField(DataExchNo, NextRecordNo, ParentRecordNo,
+ EDocumentPurchaseLine.FieldNo("Sub Total"), Amount);
+ end;
+ if VATRate <> '' then
+ InsertIntermediateField(DataExchNo, NextRecordNo, ParentRecordNo,
+ EDocumentPurchaseLine.FieldNo("VAT Rate"), VATRate);
+ if CurrencyCode <> '' then
+ InsertIntermediateField(DataExchNo, NextRecordNo, ParentRecordNo,
+ EDocumentPurchaseLine.FieldNo("Currency Code"), CurrencyCode);
+ NextRecordNo += 1;
+ end;
+
+ local procedure InsertIntermediateField(DataExchNo: Integer; RecordNo: Integer; ParentRecordNo: Integer; FieldId: Integer; Value: Text)
+ var
+ IntermediateDataImport: Record "Intermediate Data Import";
+ begin
+ IntermediateDataImport.Init();
+ IntermediateDataImport.Validate("Data Exch. No.", DataExchNo);
+ IntermediateDataImport.Validate("Table ID", Database::"E-Document Purchase Line");
+ IntermediateDataImport.Validate("Record No.", RecordNo);
+ IntermediateDataImport.Validate("Field ID", FieldId);
+ IntermediateDataImport.Validate("Parent Record No.", ParentRecordNo);
+ IntermediateDataImport.SetValueWithoutModifying(Value);
+ IntermediateDataImport.Insert(true);
+ end;
+
+ #endregion Charge Lines via Intermediate
+
+ var
+ ChargeLineDefCodeTok: Label 'PEPPOLCHARGELINES', Locked = true;
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandlerV2.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandlerV2.Codeunit.al
new file mode 100644
index 0000000000..b1b3ad7734
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandlerV2.Codeunit.al
@@ -0,0 +1,296 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Format;
+
+using Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.Processing.Import;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using Microsoft.eServices.EDocument.Processing.Interfaces;
+using System.AI;
+using System.Azure.KeyVault;
+using System.Text;
+using System.Utilities;
+
+codeunit 6318 "E-Document MLLM Handler V2" implements IStructureReceivedEDocument, IStructuredFormatReader, IStructuredDataType
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ StructuredData: Text;
+ FileFormat: Enum "E-Doc. File Format";
+ FileDataLbl: Label 'data:application/pdf;base64,%1', Locked = true;
+ SystemPromptV2ResourceTok: Label 'Prompts/EDocMLLMExtractionV2-SystemPrompt.md', Locked = true;
+ UserPromptLbl: Label 'Extract invoice data into this UBL JSON structure: %1. \n\nExtract ONLY visible values. Return JSON only. %2', Locked = true;
+ SecurityPromptAKVKeyTok: Label 'EDocMLLMExtraction-SecurityPromptV281', Locked = true;
+ MaxToolCallsTok: Integer;
+ BudgetExhaustedErr: Label 'The document could not be verified after %1 tool calls. The extraction was inconsistent.', Comment = '%1 = tool call count';
+ DocumentNotProcessedErr: Label 'The document could not be processed.';
+ InappropriateContentErr: Label 'The document could not be processed because it contains inappropriate content.';
+
+ procedure StructureReceivedEDocument(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType
+ var
+ ResponseJson: JsonObject;
+ ResponseText: Text;
+ begin
+ MaxToolCallsTok := 200;
+
+ RegisterCopilotCapabilityIfNeeded();
+
+ ResponseText := CallMLLMV2(EDocumentDataStorage);
+
+ if IsInappropriateContentResponse(ResponseText) then
+ Error(InappropriateContentErr);
+
+ if not ValidateAndUnwrapResponse(ResponseText, ResponseJson) then
+ exit(FallbackToADI(EDocumentDataStorage));
+
+ StructuredData := ResponseText;
+ FileFormat := "E-Doc. File Format"::JSON;
+ exit(this);
+ end;
+
+ local procedure CallMLLMV2(EDocumentDataStorage: Record "E-Doc. Data Storage"): Text
+ var
+ Base64Convert: Codeunit "Base64 Convert";
+ AzureOpenAI: Codeunit "Azure OpenAI";
+ AOAIChatMessages: Codeunit "AOAI Chat Messages";
+ AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
+ AOAIUserMessage: Codeunit "AOAI User Message";
+ AOAIOperationResponse: Codeunit "AOAI Operation Response";
+ AOAIDeployments: Codeunit "AOAI Deployments";
+ EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper";
+ VerifyLineMathTool: Codeunit "E-Doc. MLLM VL Math Tool";
+ VerifyTotalsTool: Codeunit "E-Doc. MLLM VL Totals Tool";
+ VerifyVATTool: Codeunit "E-Doc. MLLM VL VAT Tool";
+ VerifyDatesTool: Codeunit "E-Doc. MLLM VL Dates Tool";
+ VerifyRequiredTool: Codeunit "E-Doc. MLLM VL Required Tool";
+ VerifyRangesTool: Codeunit "E-Doc. MLLM VL Ranges Tool";
+ PayableTool: Codeunit "E-Doc. MLLM VL Payable Tool";
+ PlanSubmitTool: Codeunit "E-Doc. MLLM Plan Submit Tool";
+ ExtractionPlan: Codeunit "E-Doc. MLLM Extraction Plan";
+ PlanAnalyzeTool: Codeunit "E-Doc. MLLM Plan Analyze Tool";
+ PlanStatusTool: Codeunit "E-Doc. MLLM Plan Status Tool";
+ PlanMarkTool: Codeunit "E-Doc. MLLM Plan Mark Tool";
+ FromTempBlob: Codeunit "Temp Blob";
+ InStream: InStream;
+ Base64Data: Text;
+ ToolCallCount: Integer;
+ begin
+ ExtractionPlan.Reset();
+
+ // Load PDF as base64
+ FromTempBlob := EDocumentDataStorage.GetTempBlob();
+ FromTempBlob.CreateInStream(InStream, TextEncoding::UTF8);
+ Base64Data := Base64Convert.ToBase64(InStream);
+
+ // Configure AOAI
+ AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", AOAIDeployments.GetGPT41MiniPreview());
+ AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis");
+ AOAIChatCompletionParams.SetTemperature(0);
+ AOAIChatMessages.SetHistoryLength(500);
+ // Do NOT set JSON mode — tool-calling and JSON mode cannot be combined.
+ // The system prompt instructs the model to output UBL JSON as its final response.
+
+ // System prompt
+ AOAIChatMessages.SetPrimarySystemMessage(NavApp.GetResourceAsText(SystemPromptV2ResourceTok, TextEncoding::UTF8));
+
+ // Register 6 verification tools
+ AOAIChatMessages.AddTool(VerifyLineMathTool);
+ AOAIChatMessages.AddTool(VerifyTotalsTool);
+ AOAIChatMessages.AddTool(VerifyVATTool);
+ AOAIChatMessages.AddTool(VerifyDatesTool);
+ AOAIChatMessages.AddTool(VerifyRequiredTool);
+ AOAIChatMessages.AddTool(VerifyRangesTool);
+ AOAIChatMessages.AddTool(PlanAnalyzeTool);
+ AOAIChatMessages.AddTool(PlanStatusTool);
+ AOAIChatMessages.AddTool(PlanMarkTool);
+ AOAIChatMessages.AddTool(PayableTool);
+ AOAIChatMessages.AddTool(PlanSubmitTool);
+ AOAIChatMessages.SetToolChoice('auto');
+
+ // User message: PDF + UBL schema + security clause
+ AOAIUserMessage.AddFilePart(StrSubstNo(FileDataLbl, Base64Data));
+ AOAIUserMessage.AddTextPart(GetUserPromptText(EDocMLLMSchemaHelper.GetDefaultSchema()));
+ AOAIChatMessages.AddUserMessage(AOAIUserMessage);
+
+ // Agentic dispatch loop
+ repeat
+ AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse);
+
+ if not AOAIOperationResponse.IsSuccess() then
+ exit('');
+
+ if AOAIOperationResponse.IsFunctionCall() then begin
+ ToolCallCount += AOAIOperationResponse.GetFunctionResponses().Count();
+ if ToolCallCount > MaxToolCallsTok then
+ Error(BudgetExhaustedErr, ToolCallCount);
+ // Tool results are already appended to AOAIChatMessages by GenerateChatCompletion
+ // internally (ProcessChatCompletionResponse calls AppendFunctionResponsesToChatMessages
+ // for "Invoke Tools Only" preference). We just loop to get the next model response.
+ end;
+ until not AOAIOperationResponse.IsFunctionCall();
+
+ if ExtractionPlan.HasCurrentJson() then
+ exit(ExtractionPlan.GetCurrentJson());
+ exit(AOAIOperationResponse.GetResult());
+ end;
+
+ [NonDebuggable]
+ local procedure GetUserPromptText(Schema: Text): Text
+ var
+ AzureKeyVault: Codeunit "Azure Key Vault";
+ SecurityClause: SecretText;
+ begin
+ if not AzureKeyVault.GetAzureKeyVaultSecret(SecurityPromptAKVKeyTok, SecurityClause) then
+ Error(DocumentNotProcessedErr);
+ exit(SecretText.SecretStrSubstNo(UserPromptLbl, Schema, SecurityClause).Unwrap());
+ end;
+
+ local procedure IsInappropriateContentResponse(ResponseText: Text): Boolean
+ var
+ ResponseJson: JsonObject;
+ ContentToken: JsonToken;
+ ErrorToken: JsonToken;
+ InnerText: Text;
+ begin
+ if ResponseText = '' then
+ exit(false);
+ if not ResponseJson.ReadFrom(ResponseText) then
+ exit(false);
+ if ResponseJson.Get('content', ContentToken) and ContentToken.IsValue() then begin
+ InnerText := ContentToken.AsValue().AsText();
+ Clear(ResponseJson);
+ if not ResponseJson.ReadFrom(InnerText) then
+ exit(false);
+ end;
+ exit(ResponseJson.Get('error', ErrorToken));
+ end;
+
+ local procedure ValidateAndUnwrapResponse(var ResponseText: Text; var ResponseJson: JsonObject): Boolean
+ var
+ ContentToken: JsonToken;
+ begin
+ if ResponseText = '' then
+ exit(false);
+ if not ResponseJson.ReadFrom(ResponseText) then
+ exit(false);
+ if ResponseJson.Get('content', ContentToken) then begin
+ ResponseText := ContentToken.AsValue().AsText();
+ if not ResponseJson.ReadFrom(ResponseText) then
+ exit(false);
+ end;
+ exit(ValidateMLLMResponse(ResponseJson));
+ end;
+
+ local procedure ValidateMLLMResponse(ResponseJson: JsonObject): Boolean
+ var
+ SupplierToken: JsonToken;
+ PartyToken: JsonToken;
+ NameToken: JsonToken;
+ AddressToken: JsonToken;
+ SupplierObj: JsonObject;
+ PartyObj: JsonObject;
+ NameObj: JsonObject;
+ VendorName: Text;
+ begin
+ if not ResponseJson.Get('accounting_supplier_party', SupplierToken) then exit(false);
+ if not SupplierToken.IsObject() then exit(false);
+ SupplierObj := SupplierToken.AsObject();
+ if not SupplierObj.Get('party', PartyToken) then exit(false);
+ if not PartyToken.IsObject() then exit(false);
+ PartyObj := PartyToken.AsObject();
+ if not PartyObj.Get('party_name', NameToken) then exit(false);
+ if not NameToken.IsObject() then exit(false);
+ NameObj := NameToken.AsObject();
+ if not NameObj.Get('name', NameToken) then exit(false);
+ VendorName := NameToken.AsValue().AsText();
+ if VendorName = '' then exit(false);
+ if not PartyObj.Get('postal_address', AddressToken) then exit(false);
+ if not AddressToken.IsObject() then exit(false);
+ exit(true);
+ end;
+
+ local procedure FallbackToADI(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType
+ var
+ ADIHandler: Codeunit "E-Document ADI Handler";
+ begin
+ exit(ADIHandler.StructureReceivedEDocument(EDocumentDataStorage));
+ end;
+
+
+ procedure RegisterCopilotCapabilityIfNeeded()
+ var
+ CopilotCapability: Codeunit "Copilot Capability";
+ begin
+ if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"E-Document MLLM Analysis") then
+ CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis", '');
+ end;
+
+ procedure GetFileFormat(): Enum "E-Doc. File Format"
+ begin
+ exit(this.FileFormat);
+ end;
+
+ procedure GetContent(): Text
+ begin
+ exit(this.StructuredData);
+ end;
+
+ procedure GetReadIntoDraftImpl(): Enum "E-Doc. Read into Draft"
+ begin
+ exit("E-Doc. Read into Draft"::MLLM);
+ end;
+
+#pragma warning disable AA0139
+ procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft"
+ var
+ TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary;
+ TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary;
+ EDocPurchaseDraftUtility: Codeunit "E-Doc. Purchase Draft Utility";
+ begin
+ ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ EDocPurchaseDraftUtility.PersistDraft(EDocument, TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice");
+ end;
+
+ local procedure ReadIntoBuffer(
+ EDocument: Record "E-Document";
+ TempBlob: Codeunit "Temp Blob";
+ var TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary;
+ var TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary)
+ var
+ EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper";
+ InStream: InStream;
+ SourceJsonObject: JsonObject;
+ LinesToken: JsonToken;
+ LinesArray: JsonArray;
+ BlobAsText: Text;
+ begin
+ TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
+ InStream.Read(BlobAsText);
+ SourceJsonObject.ReadFrom(BlobAsText);
+ EDocMLLMSchemaHelper.MapHeaderFromJson(SourceJsonObject, TempEDocPurchaseHeader);
+ TempEDocPurchaseHeader."E-Document Entry No." := EDocument."Entry No";
+ if SourceJsonObject.Get('invoice_line', LinesToken) then
+ if LinesToken.IsArray() then begin
+ LinesArray := LinesToken.AsArray();
+ EDocMLLMSchemaHelper.MapLinesFromJson(LinesArray, EDocument."Entry No", TempEDocPurchaseLine);
+ end;
+ end;
+
+ procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob")
+ var
+ TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary;
+ TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary;
+ EDocReadablePurchaseDoc: Page "E-Doc. Readable Purchase Doc.";
+ begin
+ ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ EDocReadablePurchaseDoc.SetBuffer(TempEDocPurchaseHeader, TempEDocPurchaseLine);
+ EDocReadablePurchaseDoc.Run();
+ end;
+#pragma warning restore AA0139
+}
diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al
index f9af9884ce..f7d82d018e 100644
--- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al
+++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al
@@ -113,8 +113,8 @@ codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader
if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/cre:CreditNote/cac:OrderReference/cbc:ID', Value) then
Header."Purchase Order No." := CopyStr(Value, 1, MaxStrLen(Header."Purchase Order No."));
if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/cre:CreditNote/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID', Value) then
- Header."Vendor Invoice No." := CopyStr(Value, 1, MaxStrLen(Header."Vendor Invoice No."));
- if Header."Vendor Invoice No." = '' then
+ Header."Applies-to Ext. Invoice No." := CopyStr(Value, 1, MaxStrLen(Header."Applies-to Ext. Invoice No."));
+ if Header."Applies-to Ext. Invoice No." = '' then
Session.LogMessage('0000SNJ', BillingReferenceEmptyTelemetryTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', 'E-Document');
end;
diff --git a/src/Apps/W1/EDocument/App/src/SampleInvoice/EDocSamplePurchInvoice3.docx b/src/Apps/W1/EDocument/App/src/SampleInvoice/EDocSamplePurchInvoice3.docx
index 58fd3d5db2..bcbd21a4b7 100644
Binary files a/src/Apps/W1/EDocument/App/src/SampleInvoice/EDocSamplePurchInvoice3.docx and b/src/Apps/W1/EDocument/App/src/SampleInvoice/EDocSamplePurchInvoice3.docx differ
diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al
index 8bd9a0f5ba..a10d76086c 100644
--- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al
+++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al
@@ -4,6 +4,7 @@
// ------------------------------------------------------------------------------------------------
namespace Microsoft.eServices.EDocument;
+using Microsoft.eServices.EDocument.IO;
#if not CLEAN29
using Microsoft.eServices.EDocument.Processing.Import;
#endif
@@ -22,6 +23,7 @@ codeunit 6168 "E-Document Upgrade"
#if not CLEAN29
UpgradeProcessDraftEnum();
#endif
+ UpgradeDataExchV2Defs();
end;
local procedure UpgradeLogURLMaxLength()
@@ -41,11 +43,26 @@ codeunit 6168 "E-Document Upgrade"
UpgradeTag.SetUpgradeTag(GetUpgradeLogURLMaxLengthUpgradeTag());
end;
+ local procedure UpgradeDataExchV2Defs()
+ var
+ EDocumentInstall: Codeunit "E-Document Install";
+ UpgradeTag: Codeunit "Upgrade Tag";
+ begin
+ if UpgradeTag.HasUpgradeTag(GetUpgradeDataExchV2DefsTag()) then
+ exit;
+
+ EDocumentInstall.ImportInvoiceV2XML();
+ EDocumentInstall.ImportCreditMemoV2XML();
+
+ UpgradeTag.SetUpgradeTag(GetUpgradeDataExchV2DefsTag());
+ end;
+
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)]
local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])
begin
PerCompanyUpgradeTags.Add(GetUpgradeLogURLMaxLengthUpgradeTag());
PerCompanyUpgradeTags.Add(GetUpgradeProcessDraftEnumTag());
+ PerCompanyUpgradeTags.Add(GetUpgradeDataExchV2DefsTag());
end;
internal procedure GetUpgradeLogURLMaxLengthUpgradeTag(): Code[250]
@@ -75,4 +92,9 @@ codeunit 6168 "E-Document Upgrade"
exit('MS-EDoc-ProcessDraftEnum-20260407');
end;
+ internal procedure GetUpgradeDataExchV2DefsTag(): Code[250]
+ begin
+ exit('MS-EDoc-DataExchV2Defs-20260521');
+ end;
+
}
\ No newline at end of file
diff --git a/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-creditnote.xml b/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-creditnote.xml
new file mode 100644
index 0000000000..357def3f92
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-creditnote.xml
@@ -0,0 +1,133 @@
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ CN-5001
+ 2026-02-15
+ 381
+ XYZ
+ 1
+
+ 5
+
+
+
+ 103033
+ 2026-01-22
+
+
+
+
+ 1234567890128
+
+ CRONUS International
+
+
+ Main Street, 14
+ Birmingham
+ B27 4KT
+
+ GB
+
+
+
+ GB123456789
+
+ VAT
+
+
+
+ CRONUS International
+ 123456789
+
+
+ Jim Olive
+ JO@contoso.com
+
+
+
+
+
+ 789456278
+
+ 8712345000004
+
+
+ The Cannon Group PLC
+
+
+ 192 Market Square
+ Birmingham
+ B27 4KT
+
+ GB
+
+
+
+ GB789456278
+
+ VAT
+
+
+
+ The Cannon Group PLC
+ 789456278
+
+
+ Mr. Andy Teal
+
+
+
+
+ 30
+ 2026-03-15
+
+
+ 500
+
+ 2000
+ 500
+
+ S
+ 25
+
+ VAT
+
+
+
+
+
+ 2000
+ 2000
+ 2500
+ 0
+ 2500
+
+
+ 10000
+ 1
+ 2000
+
+ Bicycle - Return
+
+ 1000
+
+
+ S
+ 25
+
+ VAT
+
+
+
+
+ 2000.00
+ 1
+
+
+
diff --git a/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-invoice-attachment.xml b/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-invoice-attachment.xml
new file mode 100644
index 0000000000..2932a22371
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-invoice-attachment.xml
@@ -0,0 +1,132 @@
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ INV-ATT-001
+ 2026-03-01
+ 2026-04-01
+ 380
+ XYZ
+ 1
+
+
+ att-001
+ Invoice PDF copy
+
+ JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2Jq
+
+
+
+
+ att-002
+ Photo evidence
+
+ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
+
+
+
+
+ ext-001
+ External spec
+
+
+ https://example.com/spec.pdf
+
+
+
+
+
+ DR99999
+ 130
+
+
+
+ 1234567890128
+
+ Attachment Supplier Ltd.
+
+
+ Test Street 1
+ London
+ EC1A 1BB
+
+ GB
+
+
+
+ GB111222333
+
+ VAT
+
+
+
+ Attachment Supplier Ltd.
+
+
+
+
+
+ 8712345000004
+
+ Test Buyer Corp
+
+
+ Buyer Street 2
+ Birmingham
+ B27 4KT
+
+ GB
+
+
+
+ GB444555666
+
+ VAT
+
+
+
+ Test Buyer Corp
+
+
+
+
+ 125
+
+ 500
+ 125
+
+ S
+ 25
+
+ VAT
+
+
+
+
+
+ 500
+ 500
+ 625
+ 625
+
+
+ 1
+ 5
+ 500
+
+ Test Item
+
+ S
+ 25
+
+ VAT
+
+
+
+
+ 100.00
+
+
+
diff --git a/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-invoice.xml b/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-invoice.xml
new file mode 100644
index 0000000000..db325c80ca
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/.resources/data-exchange/data-exchange-invoice.xml
@@ -0,0 +1,210 @@
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+ urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
+ Snippet1
+ 2017-11-13
+ 2017-12-01
+ 380
+ EUR
+ 4025:123:4343
+ 0150abc
+
+
+ 9482348239847
+
+ 99887766
+
+
+ SupplierTradingName Ltd.
+
+
+ Main street 1
+ Postbox 123
+ London
+ GB 123 EW
+
+ GB
+
+
+
+ GB1232434
+
+ VAT
+
+
+
+ SupplierOfficialName Ltd
+ GB983294
+
+
+
+
+
+ FR23342
+
+ FR23342
+
+
+ BuyerTradingName AS
+
+
+ Hovedgatan 32
+ Po box 878
+ Stockholm
+ 456 34
+
+ SE
+
+
+
+ SE4598375937
+
+ VAT
+
+
+
+ Buyer Official Name
+ 39937423947
+
+
+ Lisa Johnson
+ 23434234
+ lj@buyer.se
+
+
+
+
+ 2017-11-01
+
+ 9483759475923478
+
+ Delivery street 2
+ Building 56
+ Stockholm
+ 21234
+
+ SE
+
+
+
+
+
+ Delivery party Name
+
+
+
+
+ 30
+ Snippet1
+
+ IBAN32423940
+ AccountName
+
+ BIC324098
+
+
+
+
+ Payment within 10 days, 2% discount
+
+
+ true
+ Insurance
+ 25
+
+ S
+ 25.0
+
+ VAT
+
+
+
+
+ 331.25
+
+ 1325
+ 331.25
+
+ S
+ 25.0
+
+ VAT
+
+
+
+
+
+ 1300
+ 1325
+ 1656.25
+ 25
+ 1656.25
+
+
+
+ 1
+ 7
+ 2800
+ Konteringsstreng
+
+ 123
+
+
+ Description of item
+ item name
+
+ 21382183120983
+
+
+ NO
+
+
+ 09348023
+
+
+ S
+ 25.0
+
+ VAT
+
+
+
+
+ 400
+
+
+
+ 2
+ -3
+ -1500
+
+ 123
+
+
+ Description 2
+ item name 2
+
+ 21382183120983
+
+
+ NO
+
+
+ 09348023
+
+
+ S
+ 25.0
+
+ VAT
+
+
+
+
+ 500
+
+
+
diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml
index d9b8a0adc7..7b1a00dba5 100644
--- a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml
+++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml
@@ -16,6 +16,9 @@
7300010000001
+
+ The Sellercompany Incorporated
+
Main street 2, Building 4
Big city
@@ -38,6 +41,9 @@
DK12345678
+
+ The Buyercompany
+
Anystreet 8
Back door
diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocDataExchTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocDataExchTests.Codeunit.al
new file mode 100644
index 0000000000..54c54a0439
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocDataExchTests.Codeunit.al
@@ -0,0 +1,548 @@
+// ------------------------------------------------------------------------------------------------
+// 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.eServices.EDocument.IO;
+using Microsoft.eServices.EDocument.IO.Peppol;
+using Microsoft.eServices.EDocument.Processing.Import;
+using Microsoft.eServices.EDocument.Processing.Import.Purchase;
+using Microsoft.Finance.Currency;
+using Microsoft.Finance.GeneralLedger.Setup;
+using Microsoft.Foundation.Attachment;
+using Microsoft.Purchases.Vendor;
+using Microsoft.Sales.Customer;
+using System.IO;
+using System.TestLibraries.Utilities;
+
+codeunit 139897 "E-Doc Data Exch Tests"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+
+ var
+ Customer: Record Customer;
+ Vendor: Record Vendor;
+ EDocumentService: Record "E-Document Service";
+ Assert: Codeunit Assert;
+ LibraryVariableStorage: Codeunit "Library - Variable Storage";
+ LibraryEDoc: Codeunit "Library - E-Document";
+ EDocImplState: Codeunit "E-Doc. Impl. State";
+ StructuredValidations: Codeunit "EDoc Structured Validations";
+ LibraryLowerPermission: Codeunit "Library - Lower Permissions";
+ IsInitialized: Boolean;
+ EDocumentStatusNotUpdatedErr: Label 'The status of the EDocument was not updated to the expected status after the step was executed.';
+
+ [Test]
+ procedure VerifyV2FieldMappingsImported()
+ var
+ DataExchFieldMapping: Record "Data Exch. Field Mapping";
+ begin
+ // [SCENARIO] V2 Data Exchange Definition field mappings have correct Target Table ID and Target Field ID
+ Initialize();
+
+ // [WHEN] Checking if column 8 (vendor name) mapping exists
+ DataExchFieldMapping.SetRange("Data Exch. Def Code", 'EDOCPEPINVPURCHDRAFT');
+ DataExchFieldMapping.SetRange("Column No.", 8);
+ DataExchFieldMapping.SetRange("Target Table ID", Database::"E-Document Purchase Header");
+ DataExchFieldMapping.SetRange("Target Field ID", 9); // Vendor Company Name
+
+ // [THEN] The mapping record exists
+ Assert.IsFalse(DataExchFieldMapping.IsEmpty(), 'Column 8 should map to Table 6100 Field 9 (Vendor Company Name)');
+ end;
+
+ [Test]
+ procedure InvoiceReadIntoDraft_HeaderFieldsMapped()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ begin
+ // [SCENARIO] Invoice XML processed through Data Exchange v2 handler populates staging header with vendor name, invoice no., document date, amounts
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-invoice.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] The staging header fields are populated from intermediate data
+ EDocumentPurchaseHeader.Get(EDocument."Entry No");
+ Assert.AreNotEqual('', EDocumentPurchaseHeader."Vendor Company Name", 'Vendor Company Name should be mapped from intermediate data.');
+ Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Sales Invoice No.", 'Sales Invoice No. should be mapped from Invoice ID.');
+ Assert.AreEqual(DMY2Date(13, 11, 2017), EDocumentPurchaseHeader."Document Date", 'Document Date should be mapped from IssueDate.');
+ Assert.AreEqual(DMY2Date(1, 12, 2017), EDocumentPurchaseHeader."Due Date", 'Due Date should be mapped from DueDate.');
+ Assert.AreEqual(1656.25, EDocumentPurchaseHeader.Total, 'Total should be mapped from TaxInclusiveAmount.');
+ Assert.AreEqual(1656.25, EDocumentPurchaseHeader."Amount Due", 'Amount Due should be mapped from TaxInclusiveAmount.');
+ Assert.AreEqual(1325, EDocumentPurchaseHeader."Sub Total", 'Sub Total should be mapped from TaxExclusiveAmount (Amount field 60).');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure InvoiceReadIntoDraft_LineFieldsMapped()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ LineIndex: Integer;
+ begin
+ // [SCENARIO] Invoice lines are created with Description, Quantity, Unit Price and sequential line numbers
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-invoice.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] Lines are created with correct field mappings
+ EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No");
+ Assert.AreEqual(3, EDocumentPurchaseLine.Count(), 'Expected 2 invoice lines + 1 charge line from the invoice XML.');
+
+ EDocumentPurchaseLine.FindSet();
+ repeat
+ LineIndex += 1;
+ case LineIndex of
+ 1:
+ begin
+ Assert.AreNotEqual('', EDocumentPurchaseLine.Description, 'First line Description should be mapped.');
+ Assert.AreEqual(7, EDocumentPurchaseLine.Quantity, 'First line Quantity should be 7.');
+ Assert.AreEqual(400, EDocumentPurchaseLine."Unit Price", 'First line Unit Price should be 400.');
+ Assert.AreNotEqual(0, EDocumentPurchaseLine."Line No.", 'First line should have a non-zero line number.');
+ end;
+ 2:
+ begin
+ Assert.AreEqual(-3, EDocumentPurchaseLine.Quantity, 'Second line Quantity should be -3.');
+ Assert.AreEqual(500, EDocumentPurchaseLine."Unit Price", 'Second line Unit Price should be 500.');
+ Assert.IsTrue(EDocumentPurchaseLine."Line No." > 0, 'Second line should have a sequential line number.');
+ end;
+ 3:
+ begin
+ Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Charge line Quantity should be 1.');
+ Assert.AreEqual(25, EDocumentPurchaseLine."Unit Price", 'Charge line Unit Price should be 25.');
+ Assert.AreEqual('Insurance', EDocumentPurchaseLine.Description, 'Charge line Description should be Insurance.');
+ end;
+ end;
+ until EDocumentPurchaseLine.Next() = 0;
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure InvoiceReadIntoDraft_ReturnsInvoiceDraftType()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] After parsing an Invoice, the Process Draft Impl. is set to "Purchase Invoice"
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-invoice.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] The process draft is set to Purchase Invoice
+ EDocument.Get(EDocument."Entry No");
+ Assert.AreEqual(
+ Enum::"E-Doc. Process Draft"::"Purchase Invoice",
+ EDocument."Process Draft Impl.",
+ 'The process draft implementation should be set to Purchase Invoice for invoices.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure CreditNoteReadIntoDraft_ReturnsCreditMemoDraftType()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] After parsing a CreditNote, the Process Draft Impl. is set to "Purchase Credit Memo"
+ Initialize();
+ SetupDataExchangeService();
+ SetupCreditMemoDataExchDef();
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-creditnote.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] The process draft is set to Purchase Credit Memo
+ EDocument.Get(EDocument."Entry No");
+ Assert.AreEqual(
+ Enum::"E-Doc. Process Draft"::"Purchase Credit Memo",
+ EDocument."Process Draft Impl.",
+ 'The process draft implementation should be set to Purchase Credit Memo for credit notes.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure InvoiceReadIntoDraft_TotalVATComputed()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ begin
+ // [SCENARIO] Total VAT is computed as Total - Sub Total - Total Discount
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-invoice.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] Total VAT = Total - Sub Total - Total Discount
+ EDocumentPurchaseHeader.Get(EDocument."Entry No");
+ // The Data Exchange definition does not directly map a Total VAT field.
+ // Total = 1656.25, Sub Total = 1300, Discount = 25 (charge, mapped to discount)
+ // VAT from XML TaxAmount = 331.25
+ // The handler maps Amount (field 60) to Sub Total, Amount Including VAT (field 61) to Total
+ // Invoice Discount Value (field 122) to Total Discount
+ // Total VAT is not directly mapped by the intermediate data -- it remains 0 unless computed elsewhere.
+ // Verify the amounts that ARE mapped are consistent
+ Assert.AreEqual(1656.25, EDocumentPurchaseHeader.Total, 'Total should match TaxInclusiveAmount.');
+ Assert.AreEqual(1325, EDocumentPurchaseHeader."Sub Total", 'Sub Total should match Amount (TaxExclusiveAmount, PH field 60).');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure InvoiceWithAttachment_AttachmentProcessed()
+ var
+ EDocument: Record "E-Document";
+ DocumentAttachment: Record "Document Attachment";
+ begin
+ // [SCENARIO] Attachment is decoded from base64 and stored when invoice contains embedded attachment
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-invoice-attachment.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] At least one attachment is created for the e-document
+ DocumentAttachment.SetRange("E-Document Entry No.", EDocument."Entry No");
+ Assert.IsTrue(DocumentAttachment.Count() > 0, 'Expected at least one attachment to be processed from the invoice with embedded attachment.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure InvoiceReadIntoDraft_CurrencyCodeLCYBlank()
+ var
+ EDocument: Record "E-Document";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ GLSetup: Record "General Ledger Setup";
+ begin
+ // [SCENARIO] When document currency matches LCY, Currency Code is blank on staging
+ Initialize();
+ SetupDataExchangeService();
+
+ // [GIVEN] LCY Code matches the document currency (EUR)
+ GLSetup.Get();
+ GLSetup."LCY Code" := 'EUR';
+ GLSetup.Modify();
+
+ CreateInboundEDocumentFromXML(EDocument, 'data-exchange/data-exchange-invoice.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] Currency Code is blank because document currency matches LCY
+ EDocumentPurchaseHeader.Get(EDocument."Entry No");
+ Assert.AreEqual('', EDocumentPurchaseHeader."Currency Code", 'Currency Code should be blank when document currency matches LCY.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchInvoice_FullDocument()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] Data Exchange v2 handler produces the same staging output as the PEPPOL handler for a full invoice
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-0.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then
+ // [THEN] Staging tables match the PEPPOL handler output
+ StructuredValidations.AssertFullPEPPOLDocumentExtracted(EDocument."Entry No")
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchInvoice_ReturnsInvoiceDraftType()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] After parsing an Invoice, the Process Draft Impl. is set to "Purchase Invoice"
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-0.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] The process draft is set to Purchase Invoice
+ EDocument.Get(EDocument."Entry No");
+ Assert.AreEqual(
+ Enum::"E-Doc. Process Draft"::"Purchase Invoice",
+ EDocument."Process Draft Impl.",
+ 'The process draft implementation should be set to Purchase Invoice for invoices.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchCreditNote_FullDocument()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] Data Exchange v2 handler produces the same staging output as the PEPPOL handler for a credit note
+ Initialize();
+ SetupDataExchangeService();
+ SetupCreditMemoDataExchDef();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-creditnote-0.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ // [THEN] Staging tables match the PEPPOL handler output
+ StructuredValidations.AssertFullPEPPOLCreditNoteExtracted(EDocument."Entry No");
+ EDocument.Get(EDocument."Entry No");
+ Assert.AreEqual(
+ Enum::"E-Doc. Process Draft"::"Purchase Credit Memo",
+ EDocument."Process Draft Impl.",
+ 'The process draft implementation should be set to Purchase Credit Memo for credit notes.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchInvoice_BaseExample()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] Basic PEPPOL invoice with 2 lines and a document-level charge is parsed correctly via Data Exchange
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-basic.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then
+ StructuredValidations.AssertPEPPOLBaseExampleExtracted(EDocument."Entry No")
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchInvoice_VatCategoryS()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] Invoice with multiple VAT rates and StandardItemIdentification priority via Data Exchange
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-vat-category-s.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then
+ StructuredValidations.AssertPEPPOLVatCategorySExtracted(EDocument."Entry No")
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchInvoice_VatCategoryZ()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] Invoice with zero-rated VAT (category Z), no DueDate, GBP currency via Data Exchange
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-vat-category-z.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then
+ StructuredValidations.AssertPEPPOLVatCategoryZExtracted(EDocument."Entry No")
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchInvoice_EmbeddedAttachments()
+ var
+ EDocument: Record "E-Document";
+ DocumentAttachment: Record "Document Attachment";
+ begin
+ // [SCENARIO] Embedded base64 attachments are extracted via Data Exchange
+ Initialize();
+ SetupDataExchangeService();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-attachment.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ StructuredValidations.AssertPEPPOLAttachmentHeaderExtracted(EDocument."Entry No");
+ DocumentAttachment.SetRange("E-Document Entry No.", EDocument."Entry No");
+ Assert.AreEqual(2, DocumentAttachment.Count(), 'Expected 2 embedded attachments (PDF + PNG). External URI and bare references should be skipped.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ [Test]
+ procedure DataExchCreditNote_CorrectionNoDueDate()
+ var
+ EDocument: Record "E-Document";
+ begin
+ // [SCENARIO] CreditNote without PaymentMeans/PaymentDueDate results in blank Due Date via Data Exchange
+ Initialize();
+ SetupDataExchangeService();
+ SetupCreditMemoDataExchDef();
+ CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-creditnote-no-duedate.xml');
+
+ // [WHEN] Processing the e-document to Read into Draft
+ if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin
+ StructuredValidations.AssertPEPPOLCreditNoteCorrectionExtracted(EDocument."Entry No");
+ EDocument.Get(EDocument."Entry No");
+ Assert.AreEqual(
+ Enum::"E-Doc. Process Draft"::"Purchase Credit Memo",
+ EDocument."Process Draft Impl.",
+ 'The process draft implementation should be set to Purchase Credit Memo for credit notes.');
+ end
+ else
+ Assert.Fail(EDocumentStatusNotUpdatedErr);
+ end;
+
+ local procedure Initialize()
+ var
+ TransformationRule: Record "Transformation Rule";
+ EDocument: Record "E-Document";
+ EDocDataStorage: Record "E-Doc. Data Storage";
+ EDocumentServiceStatus: Record "E-Document Service Status";
+ EDocumentPurchaseHeader: Record "E-Document Purchase Header";
+ EDocumentPurchaseLine: Record "E-Document Purchase Line";
+ EDocServiceDataExchDef: Record "E-Doc. Service Data Exch. Def.";
+ DocumentAttachment: Record "Document Attachment";
+ Currency: Record Currency;
+ begin
+ LibraryLowerPermission.SetOutsideO365Scope();
+ LibraryVariableStorage.Clear();
+ Clear(EDocImplState);
+ Clear(LibraryVariableStorage);
+
+ // Ensure PEPPOL Data Exchange Definitions exist (they may not in CI environments)
+ EnsurePEPPOLDataExchDefsExist();
+
+ if IsInitialized then
+ exit;
+
+ EDocument.DeleteAll();
+ EDocumentServiceStatus.DeleteAll();
+ EDocumentService.DeleteAll();
+ EDocDataStorage.DeleteAll();
+ EDocumentPurchaseHeader.DeleteAll();
+ EDocumentPurchaseLine.DeleteAll();
+ EDocServiceDataExchDef.DeleteAll();
+ DocumentAttachment.DeleteAll();
+
+ LibraryEDoc.SetupStandardVAT();
+ LibraryEDoc.SetupStandardSalesScenario(Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::"Mock");
+ LibraryEDoc.SetupStandardPurchaseScenario(Vendor, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::"Mock");
+ EDocumentService."Import Process" := "E-Document Import Process"::"Version 2.0";
+ EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::"Data Exchange Purchase";
+ EDocumentService.Modify();
+
+ // Set a currency that can be used across all localizations
+ Currency.Init();
+ Currency.Validate(Code, 'XYZ');
+ if Currency.Insert(true) then;
+
+ TransformationRule.DeleteAll();
+ TransformationRule.CreateDefaultTransformations();
+
+ IsInitialized := true;
+ end;
+
+ local procedure SetupDataExchangeService()
+ var
+ EDocServiceDataExchDef: Record "E-Doc. Service Data Exch. Def.";
+ begin
+ EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::"Data Exchange Purchase";
+ EDocumentService.Modify();
+
+ // Link the service to the shipped PEPPOL Invoice import Data Exchange Definition
+ EDocServiceDataExchDef.SetRange("E-Document Format Code", EDocumentService.Code);
+ EDocServiceDataExchDef.DeleteAll();
+
+ EDocServiceDataExchDef.Init();
+ EDocServiceDataExchDef."E-Document Format Code" := EDocumentService.Code;
+ EDocServiceDataExchDef."Document Type" := EDocServiceDataExchDef."Document Type"::"Purchase Invoice";
+ EDocServiceDataExchDef."Impt. Data Exchange Def. Code" := 'EDOCPEPINVPURCHDRAFT';
+ EDocServiceDataExchDef.Insert();
+ end;
+
+ local procedure SetupCreditMemoDataExchDef()
+ var
+ EDocServiceDataExchDef: Record "E-Doc. Service Data Exch. Def.";
+ begin
+ EDocServiceDataExchDef.Init();
+ EDocServiceDataExchDef."E-Document Format Code" := EDocumentService.Code;
+ EDocServiceDataExchDef."Document Type" := EDocServiceDataExchDef."Document Type"::"Purchase Credit Memo";
+ EDocServiceDataExchDef."Impt. Data Exchange Def. Code" := 'EDOCPEPCMPURCHDRAFT';
+ if not EDocServiceDataExchDef.Insert() then
+ EDocServiceDataExchDef.Modify();
+ end;
+
+ local procedure CreateInboundEDocumentFromXML(var EDocument: Record "E-Document"; FilePath: Text)
+ var
+ EDocLogRecord: Record "E-Document Log";
+ EDocumentLog: Codeunit "E-Document Log";
+ begin
+ LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService);
+
+ EDocumentLog.SetBlob('Test', Enum::"E-Doc. File Format"::XML, NavApp.GetResourceAsText(FilePath));
+ EDocumentLog.SetFields(EDocument, EDocumentService);
+ EDocLogRecord := EDocumentLog.InsertLog(Enum::"E-Document Service Status"::Imported, Enum::"Import E-Doc. Proc. Status"::Readable);
+
+ EDocument."Structured Data Entry No." := EDocLogRecord."E-Doc. Data Storage Entry No.";
+ EDocument.Modify();
+ end;
+
+ local procedure ProcessEDocumentToStep(var EDocument: Record "E-Document"; ProcessingStep: Enum "Import E-Document Steps"): Boolean
+ var
+ TempEDocImportParameters: Record "E-Doc. Import Parameters";
+ EDocLog: Record "E-Document Log";
+ EDocImport: Codeunit "E-Doc. Import";
+ EDocumentProcessing: Codeunit "E-Document Processing";
+ ErrorText: Text;
+ begin
+ EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::Readable);
+ TempEDocImportParameters."Step to Run" := ProcessingStep;
+ EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters);
+ EDocument.CalcFields("Import Processing Status");
+ if EDocument."Import Processing Status" <> Enum::"Import E-Doc. Proc. Status"::"Ready for draft" then begin
+ EDocLog.SetRange("E-Doc. Entry No", EDocument."Entry No");
+ if EDocLog.FindLast() then
+ ErrorText := Format(EDocLog.Status) + ' | ' + Format(EDocLog."Processing Status");
+ Assert.Fail('Processing failed (status: ' + Format(EDocument."Import Processing Status") + '). Log: ' + ErrorText +
+ '. ReadIntoDraft: ' + Format(EDocument."Read into Draft Impl.") + '. Service: ' + Format(EDocument.GetEDocumentService()."Read into Draft Impl."));
+ end;
+ exit(true);
+ end;
+
+ local procedure EnsurePEPPOLDataExchDefsExist()
+ var
+ DataExchDef: Record "Data Exch. Def";
+ EDocumentInstall: Codeunit "E-Document Install";
+ begin
+ if not DataExchDef.Get('EDOCPEPPOLINVIMP') then
+ EDocumentInstall.ImportInvoiceXML();
+ if not DataExchDef.Get('EDOCPEPPOLCRMEMOIMP') then
+ EDocumentInstall.ImportCreditMemoXML();
+ EDocumentInstall.ImportInvoiceV2XML();
+ EDocumentInstall.ImportCreditMemoV2XML();
+ end;
+}
diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMVerifyToolsTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMVerifyToolsTests.Codeunit.al
new file mode 100644
index 0000000000..2995a8e712
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMVerifyToolsTests.Codeunit.al
@@ -0,0 +1,397 @@
+// ------------------------------------------------------------------------------------------------
+// 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.Processing.Import;
+
+codeunit 135648 "EDoc MLLM Verify Tools Tests"
+{
+ Subtype = Test;
+
+ var
+ Assert: Codeunit Assert;
+
+ // VerifyLineMath tests
+
+ [Test]
+ procedure VerifyLineMath_Pass_NoDiscount()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] 40 × 5 × 1.0 = 200, line_extension_amount = 200 → pass
+ Result := EDocMLLMVerifyTools.VerifyLineMath(40, 5, 0, 200, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyLineMath to pass for 40 × 5 = 200');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Pass_WithDiscount()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] Swedish invoice case: 3.65 × 1083 × (1 - 36/100) ≈ 2529.89 → pass
+ // 3.65 × 1083 = 3952.95; 3952.95 × 0.64 = 2529.888 ≈ 2529.89
+ Result := EDocMLLMVerifyTools.VerifyLineMath(3.65, 1083, 36, 2529.89, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyLineMath to pass for Swedish invoice case');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Pass_WithinOnePct()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] 10 × 10 × 1.0 = 100, actual = 100.9 → within 1% tolerance → pass
+ Result := EDocMLLMVerifyTools.VerifyLineMath(10, 10, 0, 100.9, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyLineMath to pass when within 1% tolerance');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Fail_WrongPrice()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] Wrong unit price: 2.34 × 1083 × 0.80 ≈ 2027, but actual = 2529.89 → fail
+ Result := EDocMLLMVerifyTools.VerifyLineMath(2.34, 1083, 20, 2529.89, ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyLineMath to fail for wrong price');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ end;
+
+ [Test]
+ procedure VerifyLineMath_Pass_ZeroLineAmount()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] line_extension_amount = 0 → skip check → pass
+ Result := EDocMLLMVerifyTools.VerifyLineMath(0, 0, 0, 0, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyLineMath to pass when line_extension_amount = 0');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ // VerifyInvoiceTotals tests
+
+ [Test]
+ procedure VerifyInvoiceTotals_Pass()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ LineAmounts: List of [Decimal];
+ Result: Boolean;
+ begin
+ // [SCENARIO] [200, 30, 20] sums to 250, tax_exclusive_amount = 250 → pass
+ LineAmounts.Add(200);
+ LineAmounts.Add(30);
+ LineAmounts.Add(20);
+ Result := EDocMLLMVerifyTools.VerifyInvoiceTotals(LineAmounts, 250, 0, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyInvoiceTotals to pass when sum matches');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyInvoiceTotals_Fail_MissingLine()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ LineAmounts: List of [Decimal];
+ Result: Boolean;
+ begin
+ // [SCENARIO] [200] sums to 200, tax_exclusive_amount = 250 → fail (missing lines)
+ LineAmounts.Add(200);
+ Result := EDocMLLMVerifyTools.VerifyInvoiceTotals(LineAmounts, 250, 0, ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyInvoiceTotals to fail when sum does not match');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ end;
+
+ [Test]
+ procedure VerifyInvoiceTotals_Pass_WithHeaderDiscount()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Lines: List of [Decimal];
+ ErrorText: Text;
+ begin
+ // [200, 30, 20] = 250, minus 10 header discount = 240 = tax_exclusive_amount
+ Lines.Add(200); Lines.Add(30); Lines.Add(20);
+ Assert.IsTrue(VerifyTools.VerifyInvoiceTotals(Lines, 240, 10, ErrorText), ErrorText);
+ end;
+
+ // VerifyVAT tests
+
+ [Test]
+ procedure VerifyVAT_Pass()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] 250 × 15% = 37.5, tax_amount = 37.5 → pass
+ Result := EDocMLLMVerifyTools.VerifyVAT(250, 15, 37.5, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyVAT to pass for 250 × 15% = 37.5');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyVAT_Fail()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] 250 × 15% = 37.5, but tax_amount = 100 → fail
+ Result := EDocMLLMVerifyTools.VerifyVAT(250, 15, 100, ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyVAT to fail when tax_amount does not match');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ end;
+
+ [Test]
+ procedure VerifyVAT_Skip_ZeroTax()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] tax_amount = 0 → skip check → pass
+ Result := EDocMLLMVerifyTools.VerifyVAT(250, 15, 0, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyVAT to pass when tax_amount = 0 (skip)');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ // VerifyDates tests
+
+ [Test]
+ procedure VerifyDates_Pass_ValidDates()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] Valid issue and due dates where due >= issue → pass
+ Result := EDocMLLMVerifyTools.VerifyDates('2024-03-15', '2024-04-15', ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyDates to pass for valid dates');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyDates_Pass_NoDueDate()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] Valid issue date, no due date → pass
+ Result := EDocMLLMVerifyTools.VerifyDates('2024-03-15', '', ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyDates to pass when due_date is empty');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyDates_Fail_DueDateBeforeIssue()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] due_date 2024-03-15 is before issue_date 2024-04-15 → fail
+ Result := EDocMLLMVerifyTools.VerifyDates('2024-04-15', '2024-03-15', ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyDates to fail when due_date is before issue_date');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ end;
+
+ [Test]
+ procedure VerifyDates_Fail_InvalidDate()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] issue_date = 'not-a-date' cannot be parsed → fail
+ Result := EDocMLLMVerifyTools.VerifyDates('not-a-date', '', ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyDates to fail for unparseable issue_date');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ end;
+
+ [Test]
+ procedure VerifyDates_Fail_MissingIssueDate()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] issue_date = '' → fail with missing date error
+ Result := EDocMLLMVerifyTools.VerifyDates('', '', ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyDates to fail when issue_date is empty');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ end;
+
+ // VerifyRequiredFields tests
+
+ [Test]
+ procedure VerifyRequiredFields_Pass()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] All required fields present → pass
+ Result := EDocMLLMVerifyTools.VerifyRequiredFields('Contoso Ltd', 'INV-001', 2, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyRequiredFields to pass when all fields are present');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyRequiredFields_Fail_MissingVendor()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] VendorName = '' → fail, ErrorText contains 'vendor'
+ Result := EDocMLLMVerifyTools.VerifyRequiredFields('', 'INV-001', 2, ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyRequiredFields to fail when vendor name is missing');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ Assert.IsTrue(ErrorText.ToLower().Contains('vendor'), 'ErrorText should mention vendor');
+ end;
+
+ [Test]
+ procedure VerifyRequiredFields_Fail_NoLines()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Result: Boolean;
+ begin
+ // [SCENARIO] LineCount = 0 → fail, ErrorText contains 'line'
+ Result := EDocMLLMVerifyTools.VerifyRequiredFields('Contoso Ltd', 'INV-001', 0, ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyRequiredFields to fail when line count is 0');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ Assert.IsTrue(ErrorText.ToLower().Contains('line'), 'ErrorText should mention line');
+ end;
+
+ // VerifyRanges tests
+
+ [Test]
+ procedure VerifyRanges_Pass()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Quantities: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscountPcts: List of [Decimal];
+ Result: Boolean;
+ begin
+ // [SCENARIO] qty=5, price=40, vat=15, disc=0 → all in range → pass
+ Quantities.Add(5);
+ Prices.Add(40);
+ VATRates.Add(15);
+ DiscountPcts.Add(0);
+ Result := EDocMLLMVerifyTools.VerifyRanges(Quantities, Prices, VATRates, DiscountPcts, ErrorText);
+ Assert.IsTrue(Result, 'Expected VerifyRanges to pass for valid values');
+ Assert.AreEqual('', ErrorText, 'ErrorText should be empty on pass');
+ end;
+
+ [Test]
+ procedure VerifyRanges_Pass_NegativeQty()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ Qtys, Prices, VATRates, DiscPcts : List of [Decimal];
+ ErrorText: Text;
+ begin
+ // Negative qty is valid for credit/return lines
+ Qtys.Add(-1); Prices.Add(40); VATRates.Add(15); DiscPcts.Add(0);
+ Assert.IsTrue(VerifyTools.VerifyRanges(Qtys, Prices, VATRates, DiscPcts, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyRanges_Fail_DiscountOver100()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ Quantities: List of [Decimal];
+ Prices: List of [Decimal];
+ VATRates: List of [Decimal];
+ DiscountPcts: List of [Decimal];
+ Result: Boolean;
+ begin
+ // [SCENARIO] disc=150 → fail, ErrorText contains 'discount'
+ Quantities.Add(5);
+ Prices.Add(40);
+ VATRates.Add(15);
+ DiscountPcts.Add(150);
+ Result := EDocMLLMVerifyTools.VerifyRanges(Quantities, Prices, VATRates, DiscountPcts, ErrorText);
+ Assert.IsFalse(Result, 'Expected VerifyRanges to fail for discount > 100');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be non-empty on fail');
+ Assert.IsTrue(ErrorText.ToLower().Contains('discount'), 'ErrorText should mention discount');
+ end;
+
+ // VerifyPayable tests
+
+ [Test]
+ procedure VerifyPayable_Pass()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ // 250 + 37.5 = 287.5
+ Assert.IsTrue(VerifyTools.VerifyPayable(250, 37.5, 287.5, ErrorText), ErrorText);
+ end;
+
+ [Test]
+ procedure VerifyPayable_Fail()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsFalse(VerifyTools.VerifyPayable(250, 37.5, 300, ErrorText), 'Should fail');
+ Assert.AreNotEqual('', ErrorText, 'ErrorText should be set');
+ end;
+
+ [Test]
+ procedure VerifyPayable_Skip_ZeroPayable()
+ var
+ VerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ ErrorText: Text;
+ begin
+ Assert.IsTrue(VerifyTools.VerifyPayable(250, 37.5, 0, ErrorText), ErrorText);
+ end;
+
+ // IsWithinTolerance tests (testing internal method via public exposure)
+
+ [Test]
+ procedure IsWithinTolerance_Pass_ExactMatch()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ begin
+ // [SCENARIO] 100 vs 100 → within tolerance → true
+ Assert.IsTrue(EDocMLLMVerifyTools.IsWithinTolerance(100, 100), 'Expected IsWithinTolerance to return true for exact match');
+ end;
+
+ [Test]
+ procedure IsWithinTolerance_Pass_SmallDelta()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ begin
+ // [SCENARIO] 2529.888 vs 2529.89 → tiny delta, well within 1% → true
+ Assert.IsTrue(EDocMLLMVerifyTools.IsWithinTolerance(2529.888, 2529.89), 'Expected IsWithinTolerance to return true for small delta');
+ end;
+
+ [Test]
+ procedure IsWithinTolerance_Fail_LargeDelta()
+ var
+ EDocMLLMVerifyTools: Codeunit "E-Doc. MLLM Verify Tools";
+ begin
+ // [SCENARIO] 2027 vs 2529.89 → ~20% difference → false
+ Assert.IsFalse(EDocMLLMVerifyTools.IsWithinTolerance(2027, 2529.89), 'Expected IsWithinTolerance to return false for large delta');
+ end;
+}
diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al
index 2fef0f05fb..98a99e284b 100644
--- a/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al
@@ -165,7 +165,7 @@ codeunit 139894 "EDoc Structured Validations"
Assert.AreEqual(DMY2Date(15, 03, 2026), EDocumentPurchaseHeader."Due Date", 'The due date does not match the mock data.');
Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not match the mock data.');
Assert.AreEqual('5', EDocumentPurchaseHeader."Purchase Order No.", 'The order reference does not match the mock data.');
- Assert.AreEqual('103033', EDocumentPurchaseHeader."Vendor Invoice No.", 'The billing reference (vendor invoice no.) does not match the mock data.');
+ Assert.AreEqual('103033', EDocumentPurchaseHeader."Applies-to Ext. Invoice No.", 'The billing reference (vendor invoice no.) does not match the mock data.');
Assert.AreEqual('CRONUS International', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match the mock data.');
Assert.AreEqual('Main Street, 14', EDocumentPurchaseHeader."Vendor Address", 'The vendor street does not match the mock data.');
Assert.AreEqual('GB123456789', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match the mock data.');
@@ -439,7 +439,7 @@ codeunit 139894 "EDoc Structured Validations"
// CreditNote without PaymentMeans/PaymentDueDate: DueDate should be blank
Assert.AreEqual(0D, EDocumentPurchaseHeader."Due Date", 'Due Date should be blank when CreditNote has no PaymentMeans/PaymentDueDate.');
Assert.AreEqual(ExpectedCurrencyCode('EUR', GLSetup."LCY Code"), EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.');
- Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Vendor Invoice No.", 'The BillingReference (Vendor Invoice No.) does not match.');
+ Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Applies-to Ext. Invoice No.", 'The BillingReference (Vendor Invoice No.) does not match.');
Assert.AreEqual('SupplierTradingName Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.');
Assert.AreEqual('GB1232434', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match.');
Assert.AreEqual(1656.25, EDocumentPurchaseHeader.Total, 'The total does not match.');
diff --git a/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al
index 455373199d..18b950da4e 100644
--- a/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al
@@ -89,7 +89,6 @@ codeunit 7779 "Azure DI Impl." implements "AI Service Name"
end;
[TryFunction]
- [NonDebuggable]
local procedure SendRequest(Base64Data: Text; ModelType: Enum "ADI Model Type"; CallerModuleInfo: ModuleInfo; var Result: Text)
var
ALCopilotFunctions: DotNet ALCopilotFunctions;
@@ -147,4 +146,4 @@ codeunit 7779 "Azure DI Impl." implements "AI Service Name"
exit(AzureAiDocumentIntelligenceTxt);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al
index 40386b243f..4e9d049461 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al
@@ -16,19 +16,13 @@ codeunit 7767 "AOAI Authorization"
InherentPermissions = X;
var
- [NonDebuggable]
Endpoint: Text;
- [NonDebuggable]
Deployment: Text;
- [NonDebuggable]
ApiKey: SecretText;
- [NonDebuggable]
ManagedResourceDeployment: Text;
- [NonDebuggable]
AOAIAccountName: Text;
ResourceUtilization: Enum "AOAI Resource Utilization";
- [NonDebuggable]
procedure IsConfigured(CallerModule: ModuleInfo): Boolean
var
CurrentModule: ModuleInfo;
@@ -49,7 +43,6 @@ codeunit 7767 "AOAI Authorization"
end;
#if not CLEAN26
- [NonDebuggable]
procedure SetMicrosoftManagedAuthorization(NewEndpoint: Text; NewDeployment: Text; NewApiKey: SecretText; NewManagedResourceDeployment: Text)
begin
ClearVariables();
@@ -62,7 +55,6 @@ codeunit 7767 "AOAI Authorization"
end;
#endif
- [NonDebuggable]
procedure SetMicrosoftManagedAuthorization(NewManagedResourceDeployment: Text)
begin
ClearVariables();
@@ -71,7 +63,6 @@ codeunit 7767 "AOAI Authorization"
ManagedResourceDeployment := NewManagedResourceDeployment;
end;
- [NonDebuggable]
procedure SetSelfManagedAuthorization(NewEndpoint: Text; NewDeployment: Text; NewApiKey: SecretText)
begin
ClearVariables();
@@ -82,7 +73,6 @@ codeunit 7767 "AOAI Authorization"
ApiKey := NewApiKey;
end;
- [NonDebuggable]
procedure SetFirstPartyAuthorization(NewDeployment: Text)
begin
ClearVariables();
@@ -91,25 +81,21 @@ codeunit 7767 "AOAI Authorization"
ManagedResourceDeployment := NewDeployment;
end;
- [NonDebuggable]
procedure GetEndpoint(): SecretText
begin
exit(Endpoint);
end;
- [NonDebuggable]
procedure GetDeployment(): SecretText
begin
exit(Deployment);
end;
- [NonDebuggable]
procedure GetApiKey(): SecretText
begin
exit(ApiKey);
end;
- [NonDebuggable]
procedure GetManagedResourceDeployment(): SecretText
begin
exit(ManagedResourceDeployment);
@@ -129,4 +115,4 @@ codeunit 7767 "AOAI Authorization"
Clear(ManagedResourceDeployment);
Clear(ResourceUtilization);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al
index d8ef80a9f7..3138248018 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al
@@ -100,7 +100,6 @@ codeunit 7771 "Azure OpenAI"
/// Endpoint would look like: https://resource-name.openai.azure.com/
/// Deployment would look like: gpt-35-turbo-16k
///
- [NonDebuggable]
[Obsolete('Using Managed AI resources now requires different input parameters. Use the other overload for SetManagedResourceAuthorization instead.', '26.0')]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; Endpoint: Text; Deployment: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
begin
@@ -117,7 +116,6 @@ codeunit 7771 "Azure OpenAI"
/// Name of the Azure Open AI resource like "MyAzureOpenAIResource"
/// The API key to access the Azure Open AI resource. This is used only for verification of access, not for actual Azure Open AI calls.
/// The managed deployment to use for the model type.
- [NonDebuggable]
[Obsolete('Using Managed AI resources now requires only Model Type & Deployment Text as input parameters. Use the other overload for SetManagedResourceAuthorization instead. AOAIAccountName & ApiKey are ignored in the current version of SetManagedResourceAuthorization.', '28.0')]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
begin
@@ -131,7 +129,6 @@ codeunit 7771 "Azure OpenAI"
///
/// The model type to set authorization for.
/// The managed deployment to use for the model type.
- [NonDebuggable]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; ManagedResourceDeployment: Text)
begin
AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, ManagedResourceDeployment);
@@ -147,7 +144,6 @@ codeunit 7771 "Azure OpenAI"
/// Endpoint would look like: https://resource-name.openai.azure.com/
/// Deployment would look like: gpt-35-turbo-16k
///
- [NonDebuggable]
procedure SetAuthorization(ModelType: Enum "AOAI Model Type"; Endpoint: Text; Deployment: Text; ApiKey: SecretText)
begin
AzureOpenAIImpl.SetAuthorization(ModelType, Endpoint, Deployment, ApiKey);
@@ -159,7 +155,6 @@ codeunit 7771 "Azure OpenAI"
/// The model type to set authorization for.
/// The deployment name to use for the model type.
/// Deployment would look like: gpt-35-turbo-16k
- [NonDebuggable]
procedure SetAuthorization(ModelType: Enum "AOAI Model Type"; Deployment: Text)
begin
AzureOpenAIImpl.SetAuthorization(ModelType, Deployment);
@@ -173,7 +168,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated completion.
/// The completion authentication was not configured.
/// The completion generation failed with status code %1.
- [NonDebuggable]
procedure GenerateTextCompletion(Prompt: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): Text
var
CallerModuleInfo: ModuleInfo;
@@ -191,7 +185,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated completion.
/// The completion authentication was not configured.
/// The completion generation failed with status code %1.
- [NonDebuggable]
procedure GenerateTextCompletion(Prompt: SecretText; AOAICompletionParams: Codeunit "AOAI Text Completion Params"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): Text
var
CallerModuleInfo: ModuleInfo;
@@ -209,7 +202,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated completion.
/// The completion authentication was not configured.
/// The completion generation failed with status code %1.
- [NonDebuggable]
procedure GenerateTextCompletion(Metaprompt: SecretText; Prompt: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): Text
var
CallerModuleInfo: ModuleInfo;
@@ -228,7 +220,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated completion.
/// The completion authentication was not configured.
/// The completion generation failed with status code %1.
- [NonDebuggable]
procedure GenerateTextCompletion(Metaprompt: SecretText; Prompt: SecretText; AOAICompletionParams: Codeunit "AOAI Text Completion Params"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): Text
var
CallerModuleInfo: ModuleInfo;
@@ -246,7 +237,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated list of embeddings.
/// The embedding authentication was not configured.
/// The embedding generation failed with status code %1.
- [NonDebuggable]
procedure GenerateEmbeddings(Input: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): List of [Decimal]
var
CallerModuleInfo: ModuleInfo;
@@ -263,7 +253,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated chat completion.
/// The chat completion authentication was not configured.
/// The chat completion generation failed with status code %1.
- [NonDebuggable]
procedure GenerateChatCompletion(var AOAIChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response")
var
CallerModuleInfo: ModuleInfo;
@@ -281,7 +270,6 @@ codeunit 7771 "Azure OpenAI"
/// The generated chat completion.
/// The chat completion authentication was not configured.
/// The chat completion generation failed with status code %1.
- [NonDebuggable]
procedure GenerateChatCompletion(var AOAIChatMessages: Codeunit "AOAI Chat Messages"; AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; var AOAIOperationResponse: Codeunit "AOAI Operation Response")
var
CallerModuleInfo: ModuleInfo;
@@ -294,7 +282,6 @@ codeunit 7771 "Azure OpenAI"
/// Sets the copilot capability that the API is running for.
///
/// The copilot capability to set.
- [NonDebuggable]
procedure SetCopilotCapability(CopilotCapability: Enum "Copilot Capability")
var
CallerModuleInfo: ModuleInfo;
@@ -304,4 +291,4 @@ codeunit 7771 "Azure OpenAI"
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
index 71731dc20f..dcb4b7a831 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
@@ -83,7 +83,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
exit(IsEnabled(Capability, CallerModuleInfo) and IsAuthorizationConfigured(ModelType, CallerModuleInfo));
end;
- [NonDebuggable]
procedure SetAuthorization(ModelType: Enum "AOAI Model Type"; Deployment: Text)
begin
case ModelType of
@@ -98,7 +97,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
end;
end;
- [NonDebuggable]
procedure SetAuthorization(ModelType: Enum "AOAI Model Type"; Endpoint: Text; Deployment: Text; ApiKey: SecretText)
begin
case ModelType of
@@ -114,7 +112,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
end;
#if not CLEAN26
- [NonDebuggable]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; Endpoint: Text; Deployment: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
begin
case ModelType of
@@ -132,7 +129,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
#if not CLEAN28
#pragma warning disable AA0137
- [NonDebuggable]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
begin
SetManagedResourceAuthorization(ModelType, ManagedResourceDeployment);
@@ -140,7 +136,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
#pragma warning restore AA0137
#endif
- [NonDebuggable]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; ManagedResourceDeployment: Text)
begin
case ModelType of
@@ -155,7 +150,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
end;
end;
- [NonDebuggable]
procedure GenerateTextCompletion(Prompt: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo): Text
var
AOAICompletionParameters: Codeunit "AOAI Text Completion Params";
@@ -163,13 +157,11 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
exit(GenerateTextCompletion(GetTextMetaprompt(), Prompt, AOAICompletionParameters, AOAIOperationResponse, CallerModuleInfo));
end;
- [NonDebuggable]
procedure GenerateTextCompletion(Prompt: SecretText; AOAICompletionParameters: Codeunit "AOAI Text Completion Params"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo) Result: Text
begin
exit(GenerateTextCompletion(GetTextMetaprompt(), Prompt, AOAICompletionParameters, AOAIOperationResponse, CallerModuleInfo));
end;
- [NonDebuggable]
procedure GenerateTextCompletion(Metaprompt: SecretText; Prompt: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo): Text
var
AOAICompletionParameters: Codeunit "AOAI Text Completion Params";
@@ -177,7 +169,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
exit(GenerateTextCompletion(Metaprompt, Prompt, AOAICompletionParameters, AOAIOperationResponse, CallerModuleInfo));
end;
- [NonDebuggable]
procedure GenerateTextCompletion(Metaprompt: SecretText; Prompt: SecretText; AOAICompletionParameters: Codeunit "AOAI Text Completion Params"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo) Result: Text
var
CustomDimensions: Dictionary of [Text, Text];
@@ -194,7 +185,7 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo);
CheckTextCompletionMetaprompt(Metaprompt, CustomDimensions);
- UnwrappedPrompt := Metaprompt.Unwrap() + Prompt.Unwrap();
+ UnwrappedPrompt := UnwrapSecret(Metaprompt) + UnwrapSecret(Prompt);
UnwrappedPrompt := RemoveProhibitedCharacters(UnwrappedPrompt);
AOAICompletionParameters.AddCompletionsParametersToPayload(Payload);
@@ -212,7 +203,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
Result := AOAIOperationResponse.GetResult();
end;
- [NonDebuggable]
procedure GenerateEmbeddings(Input: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo): List of [Decimal]
var
CustomDimensions: Dictionary of [Text, Text];
@@ -225,7 +215,7 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
CopilotCapabilityImpl.CheckEnabled(CallerModuleInfo);
CheckAuthorizationEnabled(EmbeddingsAOAIAuthorization, CallerModuleInfo);
- Payload.Add('input', Input.Unwrap());
+ Payload.Add('input', UnwrapSecret(Input));
Payload.WriteTo(PayloadText);
CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo);
@@ -239,7 +229,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
exit(ProcessEmbeddingResponse(AOAIOperationResponse));
end;
- [NonDebuggable]
local procedure ProcessEmbeddingResponse(AOAIOperationResponse: Codeunit "AOAI Operation Response") Result: List of [Decimal]
var
Response: JsonObject;
@@ -255,7 +244,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
end;
end;
- [NonDebuggable]
procedure GenerateChatCompletion(var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo)
var
AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
@@ -263,7 +251,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
GenerateChatCompletion(ChatMessages, AOAIChatCompletionParams, AOAIOperationResponse, CallerModuleInfo);
end;
- [NonDebuggable]
procedure GenerateChatCompletion(var ChatMessages: Codeunit "AOAI Chat Messages"; AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo)
var
AOAIPolicyParams: Codeunit "AOAI Policy Params";
@@ -328,7 +315,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
GenerateChatCompletion(ChatMessages, AOAIChatCompletionParams, AOAIOperationResponse, CallerModuleInfo);
end;
- [NonDebuggable]
local procedure CheckJsonModeCompatibility(Payload: JsonObject)
var
ResponseFormatToken: JsonToken;
@@ -354,7 +340,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
Error(MessagesMustContainJsonWordWhenResponseFormatIsJsonErr);
end;
- [NonDebuggable]
[TryFunction]
local procedure ProcessChatCompletionResponse(var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo)
var
@@ -470,7 +455,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
end;
[TryFunction]
- [NonDebuggable]
local procedure SendRequest(ModelType: Enum "AOAI Model Type"; AOAIAuthorization: Codeunit "AOAI Authorization"; Payload: Text; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo; AzureOpenAIPolicy: Text)
var
CopilotNotifications: Codeunit "Copilot Notifications";
@@ -549,7 +533,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
Error(AuthenticationNotConfiguredErr);
end;
- [NonDebuggable]
procedure RemoveProhibitedCharacters(Prompt: Text) Result: Text
begin
Result := Prompt.Replace('<|end>', '');
@@ -563,7 +546,6 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
exit(Result);
end;
- [NonDebuggable]
internal procedure GetTextMetaprompt() Metaprompt: SecretText;
var
AzureKeyVault: Codeunit "Azure Key Vault";
@@ -583,12 +565,11 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
Metaprompt := KVSecret;
end;
- [NonDebuggable]
local procedure CheckTextCompletionMetaprompt(Metaprompt: SecretText; CustomDimensions: Dictionary of [Text, Text])
var
ModuleInfo: ModuleInfo;
begin
- if Metaprompt.Unwrap().Trim() = '' then begin
+ if UnwrapSecret(Metaprompt).Trim() = '' then begin
FeatureTelemetry.LogError('0000LO8', GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, EmptyMetapromptErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions);
NavApp.GetCurrentModuleInfo(ModuleInfo);
@@ -663,4 +644,10 @@ codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name"
exit(true);
end;
end;
-}
\ No newline at end of file
+
+ [NonDebuggable]
+ local procedure UnwrapSecret(Secret: SecretText): Text
+ begin
+ exit(Secret.Unwrap());
+ end;
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al
index 978a505362..009841f0a0 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al
@@ -146,7 +146,6 @@ codeunit 7762 "AOAI Chat Compl Params Impl"
FrequencyPenalty := NewFrequencyPenalty;
end;
- [NonDebuggable]
procedure AddChatCompletionsParametersToPayload(var Payload: JsonObject)
begin
if GetMaxTokens() > 0 then
@@ -181,4 +180,4 @@ codeunit 7762 "AOAI Chat Compl Params Impl"
DefaultPolicyParams.InitializeDefaults();
SetAOAIPolicyParams(DefaultPolicyParams);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al
index 31db11e7cd..9719d628a7 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al
@@ -149,9 +149,8 @@ codeunit 7761 "AOAI Chat Completion Params"
/// Adds the Chat Completion parameters to the payload.
///
/// JsonObject to add parameters to.
- [NonDebuggable]
internal procedure AddChatCompletionsParametersToPayload(var Payload: JsonObject)
begin
AOAIChatComplParamsImpl.AddChatCompletionsParametersToPayload(Payload);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al
index 8c9b3b1f21..f4d1e2b51b 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al
@@ -22,7 +22,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Sets the system message which is always at the top of the chat messages history provided to the model.
///
/// The primary system message.
- [NonDebuggable]
procedure SetPrimarySystemMessage(Message: SecretText)
begin
AOAIChatMessagesImpl.SetPrimarySystemMessage(Message);
@@ -32,7 +31,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Adds a system message to the chat messages history.
///
/// The message to add.
- [NonDebuggable]
procedure AddSystemMessage(NewMessage: Text)
begin
AOAIChatMessagesImpl.AddSystemMessage(NewMessage);
@@ -42,7 +40,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Adds a user message to the chat messages history.
///
/// The message to add.
- [NonDebuggable]
procedure AddUserMessage(NewMessage: Text)
begin
AOAIChatMessagesImpl.AddUserMessage(NewMessage);
@@ -53,7 +50,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// The message to add.
/// The name of the user.
- [NonDebuggable]
procedure AddUserMessage(NewMessage: Text; NewName: Text[2048])
begin
AOAIChatMessagesImpl.AddUserMessage(NewMessage, NewName);
@@ -63,7 +59,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Adds a user message with structured content parts to the chat messages history.
///
/// The user message builder containing content parts (e.g. text, file).
- [NonDebuggable]
procedure AddUserMessage(AOAIUserMessage: Codeunit "AOAI User Message")
begin
AOAIChatMessagesImpl.AddUserMessage(AOAIUserMessage);
@@ -74,7 +69,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// The user message builder containing content parts (e.g. text, file).
/// The name of the user.
- [NonDebuggable]
procedure AddUserMessage(AOAIUserMessage: Codeunit "AOAI User Message"; NewName: Text[2048])
begin
AOAIChatMessagesImpl.AddUserMessage(AOAIUserMessage, NewName);
@@ -84,7 +78,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Adds a assistant message to the chat messages history.
///
/// The message to add.
- [NonDebuggable]
procedure AddAssistantMessage(NewMessage: Text)
begin
AOAIChatMessagesImpl.AddAssistantMessage(NewMessage);
@@ -94,7 +87,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Adds a assistant message containing the tool calls returned from the model to the chat messages history.
///
/// The tool calls to add.
- [NonDebuggable]
internal procedure AddToolCalls(ToolCalls: JsonArray)
begin
AOAIChatMessagesImpl.AddToolCalls(ToolCalls);
@@ -106,7 +98,6 @@ codeunit 7763 "AOAI Chat Messages"
/// The id of the tool call.
/// The name of the called function.
/// The result of the tool call.
- [NonDebuggable]
procedure AddToolMessage(ToolCallId: Text; FunctionName: Text; FunctionResult: Text)
begin
AOAIChatMessagesImpl.AddToolMessage(ToolCallId, FunctionName, FunctionResult);
@@ -120,7 +111,6 @@ codeunit 7763 "AOAI Chat Messages"
/// The new role.
/// The new name.
/// Message id does not exist.
- [NonDebuggable]
procedure ModifyMessage(Id: Integer; NewMessage: Text; NewRole: Enum "AOAI Chat Roles"; NewName: Text[2048])
begin
AOAIChatMessagesImpl.ModifyMessage(Id, NewMessage, NewRole, NewName);
@@ -131,7 +121,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// Id of the message.
/// Message id does not exist.
- [NonDebuggable]
procedure DeleteMessage(Id: Integer)
begin
AOAIChatMessagesImpl.DeleteMessage(Id);
@@ -141,7 +130,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the history of chat messages.
///
/// List of chat messages.
- [NonDebuggable]
procedure GetHistory(): List of [Text]
begin
exit(AOAIChatMessagesImpl.GetHistory());
@@ -151,7 +139,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the history names of chat messages.
///
/// List of names of chat messages.
- [NonDebuggable]
procedure GetHistoryNames(): List of [Text[2048]]
begin
exit(AOAIChatMessagesImpl.GetHistoryNames());
@@ -161,7 +148,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the history roles of chat messages.
///
/// List of roles of chat messages.
- [NonDebuggable]
procedure GetHistoryRoles(): List of [Enum "AOAI Chat Roles"]
begin
exit(AOAIChatMessagesImpl.GetHistoryRoles());
@@ -171,7 +157,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the last chat message.
///
/// The last chat message.
- [NonDebuggable]
procedure GetLastMessage(): Text
begin
exit(AOAIChatMessagesImpl.GetLastMessage());
@@ -181,7 +166,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the last chat message role.
///
/// The last chat message role.
- [NonDebuggable]
procedure GetLastRole(): Enum "AOAI Chat Roles"
begin
exit(AOAIChatMessagesImpl.GetLastRole());
@@ -191,7 +175,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the last chat message name.
///
/// The last chat message name.
- [NonDebuggable]
procedure GetLastName(): Text[2048]
begin
exit(AOAIChatMessagesImpl.GetLastName());
@@ -201,7 +184,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets a copy of the last chat message tool calls array.
///
/// The last tool calls array.
- [NonDebuggable]
procedure GetLastToolCalls(): JsonArray
begin
exit(AOAIChatMessagesImpl.GetLastToolCalls());
@@ -212,7 +194,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// The new length.
/// History length must be greater than 0.
- [NonDebuggable]
procedure SetHistoryLength(NewLength: Integer)
begin
AOAIChatMessagesImpl.SetHistoryLength(NewLength);
@@ -225,7 +206,6 @@ codeunit 7763 "AOAI Chat Messages"
/// The number tokens used by all other messages.
/// History of messages in a JsonArray.
/// Use this after adding messages, to construct a json array of all messages.
- [NonDebuggable]
internal procedure AssembleHistory(var SystemMessageTokenCount: Integer; var MessagesTokenCount: Integer): JsonArray
begin
exit(AOAIChatMessagesImpl.PrepareHistory(SystemMessageTokenCount, MessagesTokenCount));
@@ -234,7 +214,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// Gets the number of tokens used by the primary system messages and all other messages.
///
- [NonDebuggable]
procedure GetHistoryTokenCount(): Integer
begin
exit(AOAIChatMessagesImpl.GetHistoryTokenCount());
@@ -312,7 +291,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// The Tool choice parameter.
/// See more details here: https://go.microsoft.com/fwlink/?linkid=2254538
- [NonDebuggable]
procedure SetToolChoice(ToolChoice: Text)
begin
AOAIToolsImpl.SetToolChoice(ToolChoice);
@@ -323,7 +301,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// The function name parameter.
/// See more details here: https://go.microsoft.com/fwlink/?linkid=2254538
- [NonDebuggable]
procedure SetFunctionAsToolChoice(FunctionName: Text)
begin
AOAIToolsImpl.SetFunctionAsToolChoice(FunctionName);
@@ -334,7 +311,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// The function codeunit.
/// See more details here: https://go.microsoft.com/fwlink/?linkid=2254538
- [NonDebuggable]
procedure SetFunctionAsToolChoice(Function: Interface "AOAI Function")
begin
AOAIToolsImpl.SetFunctionAsToolChoice(Function);
@@ -344,7 +320,6 @@ codeunit 7763 "AOAI Chat Messages"
/// Gets the Tool choice parameter.
///
/// The Tool choice parameter.
- [NonDebuggable]
procedure GetToolChoice(): Text
begin
exit(AOAIToolsImpl.GetToolChoice());
@@ -372,7 +347,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// Input text with XPIA Detection tags added.
/// Please make sure to configure the AOAI policy to support XPIA detection before using this method. Also, remember to add the 'Input text with XPIA detection tags added' to a message.
- [NonDebuggable]
procedure AddXPIADetectionTags(var Input: Text)
begin
AOAIChatMessagesImpl.AddXPIADetectionTags(Input);
@@ -383,7 +357,6 @@ codeunit 7763 "AOAI Chat Messages"
///
/// Tools in a JsonArray.
/// Use this after adding Tools, to construct a json array of all Tools.
- [NonDebuggable]
internal procedure AssembleTools(): JsonArray
begin
exit(AOAIToolsImpl.PrepareTools());
@@ -393,10 +366,9 @@ codeunit 7763 "AOAI Chat Messages"
/// Checks if the current chat messages history is compatible with the deployment model
///
/// True if compatible, false otherwise.
- [NonDebuggable]
internal procedure CheckCompatibilityWithModel(Deployment: SecretText)
begin
AOAIChatMessagesImpl.CheckCompatibilityWithModel(Deployment);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al
index afafb15579..63a03552d4 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al
@@ -21,17 +21,11 @@ codeunit 7764 "AOAI Chat Messages Impl"
Initialized: Boolean;
HistoryLength: Integer;
SystemMessage: SecretText;
- [NonDebuggable]
History: List of [Text];
- [NonDebuggable]
HistoryRoles: List of [Enum "AOAI Chat Roles"];
- [NonDebuggable]
HistoryNames: List of [Text[2048]];
- [NonDebuggable]
HistoryToolCallIds: List of [Text];
- [NonDebuggable]
HistoryToolCalls: List of [JsonArray];
- [NonDebuggable]
HistoryUserMessages: List of [Codeunit "AOAI User Message"];
IsSystemMessageSet: Boolean;
MessageIdDoesNotExistErr: Label 'Message id does not exist.';
@@ -46,63 +40,54 @@ codeunit 7764 "AOAI Chat Messages Impl"
IncompatibleModelErr: Label 'The current message history contains file content which is only compatible with the GPT-4.1 mini preview deployment.';
- [NonDebuggable]
procedure SetPrimarySystemMessage(NewPrimaryMessage: SecretText)
begin
SystemMessage := NewPrimaryMessage;
IsSystemMessageSet := true;
end;
- [NonDebuggable]
procedure AddSystemMessage(NewMessage: Text)
begin
Initialize();
AddMessage(NewMessage, '', Enum::"AOAI Chat Roles"::System);
end;
- [NonDebuggable]
procedure AddUserMessage(NewMessage: Text)
begin
Initialize();
AddMessage(NewMessage, '', Enum::"AOAI Chat Roles"::User);
end;
- [NonDebuggable]
procedure AddUserMessage(NewMessage: Text; NewName: Text[2048])
begin
Initialize();
AddMessage(NewMessage, NewName, Enum::"AOAI Chat Roles"::User);
end;
- [NonDebuggable]
procedure AddUserMessage(AOAIUserMessage: Codeunit "AOAI User Message")
begin
Initialize();
AddMessage(AOAIUserMessage, '', Enum::"AOAI Chat Roles"::User);
end;
- [NonDebuggable]
procedure AddUserMessage(AOAIUserMessage: Codeunit "AOAI User Message"; NewName: Text[2048])
begin
Initialize();
AddMessage(AOAIUserMessage, NewName, Enum::"AOAI Chat Roles"::User);
end;
- [NonDebuggable]
procedure AddAssistantMessage(NewMessage: Text)
begin
Initialize();
AddMessage(NewMessage, '', Enum::"AOAI Chat Roles"::Assistant);
end;
- [NonDebuggable]
procedure AddToolCalls(ToolCalls: JsonArray)
begin
Initialize();
AddMessage(ToolCalls);
end;
- [NonDebuggable]
procedure AddToolMessage(ToolCallId: Text; FunctionName: Text; FunctionResult: Text)
var
FunctionNameTruncated: Text[2048];
@@ -115,7 +100,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
end;
- [NonDebuggable]
procedure ModifyMessage(Id: Integer; NewMessage: Text; NewRole: Enum "AOAI Chat Roles"; NewName: Text[2048])
begin
if (Id < 1) or (Id > History.Count) then
@@ -126,7 +110,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryNames.Set(Id, NewName);
end;
- [NonDebuggable]
procedure DeleteMessage(Id: Integer)
begin
if (Id < 1) or (Id > History.Count) then
@@ -139,49 +122,41 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryUserMessages.RemoveAt(Id);
end;
- [NonDebuggable]
procedure GetHistory(): List of [Text]
begin
exit(History);
end;
- [NonDebuggable]
procedure GetHistoryNames(): List of [Text[2048]]
begin
exit(HistoryNames);
end;
- [NonDebuggable]
procedure GetHistoryRoles(): List of [Enum "AOAI Chat Roles"]
begin
exit(HistoryRoles);
end;
- [NonDebuggable]
procedure GetHistoryToolCallIds(): List of [Text]
begin
exit(HistoryToolCallIds);
end;
- [NonDebuggable]
procedure GetLastMessage() LastMessage: Text
begin
History.Get(History.Count, LastMessage);
end;
- [NonDebuggable]
procedure GetLastRole() LastRole: Enum "AOAI Chat Roles"
begin
HistoryRoles.Get(HistoryRoles.Count, LastRole);
end;
- [NonDebuggable]
procedure GetLastName() LastName: Text[2048]
begin
HistoryNames.Get(HistoryNames.Count, LastName);
end;
- [NonDebuggable]
procedure GetLastToolCalls() LastToolCalls: JsonArray
var
LastToolCallsRef: JsonArray;
@@ -190,13 +165,11 @@ codeunit 7764 "AOAI Chat Messages Impl"
LastToolCalls := LastToolCallsRef.Clone().AsArray(); // avoid modifications to the chat message
end;
- [NonDebuggable]
procedure GetLastToolCallId() LastToolCall: Text
begin
HistoryToolCallIds.Get(HistoryToolCallIds.Count, LastToolCall);
end;
- [NonDebuggable]
procedure SetHistoryLength(NewHistoryLength: Integer)
begin
if NewHistoryLength < 1 then
@@ -205,7 +178,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryLength := NewHistoryLength;
end;
- [NonDebuggable]
procedure GetHistoryTokenCount(): Integer
var
SystemMessageTokenCount: Integer;
@@ -215,7 +187,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
exit(SystemMessageTokenCount + MessagesTokenCount);
end;
- [NonDebuggable]
procedure PrepareHistory(var SystemMessageTokenCount: Integer; var MessagesTokenCount: Integer) HistoryResult: JsonArray
var
AOAIUserMessage: Codeunit "AOAI User Message";
@@ -238,9 +209,9 @@ codeunit 7764 "AOAI Chat Messages Impl"
Initialize();
CheckandAddMetaprompt(UsingMicrosoftMetaprompt);
- if SystemMessage.Unwrap() <> '' then begin
+ if UnwrapSecret(SystemMessage) <> '' then begin
MessageJsonObject.Add('role', Format(Enum::"AOAI Chat Roles"::System));
- MessageJsonObject.Add('content', SystemMessage.Unwrap());
+ MessageJsonObject.Add('content', UnwrapSecret(SystemMessage));
HistoryResult.Add(MessageJsonObject);
SystemMessageTokenCount := AOAIToken.GetGPT4TokenCount(SystemMessage);
@@ -321,7 +292,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
Initialized := true;
end;
- [NonDebuggable]
local procedure PrepareMessage(WrapMessage: Boolean; MessageVariant: Variant): Variant
var
AzureOpenAIImpl: Codeunit "Azure OpenAI Impl";
@@ -373,7 +343,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
Error(WrongTypeErr);
end;
- [NonDebuggable]
local procedure AddMessage(ToolCalls: JsonArray)
var
AOAIUserMessage: Codeunit "AOAI User Message";
@@ -386,7 +355,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryUserMessages.Add(AOAIUserMessage);
end;
- [NonDebuggable]
local procedure AddMessage(NewMessage: Text; NewName: Text[2048]; NewRole: Enum "AOAI Chat Roles")
var
AOAIUserMessage: Codeunit "AOAI User Message";
@@ -400,7 +368,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryUserMessages.Add(AOAIUserMessage);
end;
- [NonDebuggable]
local procedure AddMessage(NewMessage: Text; NewName: Text[2048]; NewToolCallId: Text; NewRole: Enum "AOAI Chat Roles")
var
AOAIUserMessage: Codeunit "AOAI User Message";
@@ -414,7 +381,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryUserMessages.Add(AOAIUserMessage);
end;
- [NonDebuggable]
local procedure AddMessage(AOAIUserMessage: Codeunit "AOAI User Message"; NewName: Text[2048]; NewRole: Enum "AOAI Chat Roles")
var
ToolCalls: JsonArray;
@@ -427,7 +393,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
HistoryUserMessages.Add(AOAIUserMessage);
end;
- [NonDebuggable]
local procedure GetChatMetaprompt(var UsingMicrosoftMetaprompt: Boolean) Metaprompt: SecretText;
var
AzureKeyVault: Codeunit "Azure Key Vault";
@@ -449,10 +414,9 @@ codeunit 7764 "AOAI Chat Messages Impl"
Metaprompt := KVSecret;
end;
- [NonDebuggable]
local procedure CheckandAddMetaprompt(var UsingMicrosoftMetaprompt: Boolean)
begin
- if SystemMessage.Unwrap().Trim() = '' then begin
+ if UnwrapSecret(SystemMessage).Trim() = '' then begin
if IsSystemMessageSet then
Telemetry.LogMessage('0000LO9', TelemetryMetapromptSetbutEmptyTxt, Verbosity::Normal, DataClassification::SystemMetadata)
else
@@ -461,7 +425,6 @@ codeunit 7764 "AOAI Chat Messages Impl"
end;
end;
- [NonDebuggable]
procedure CheckCompatibilityWithModel(Deployment: SecretText)
var
AOAIDeployments: Codeunit "AOAI Deployments";
@@ -472,9 +435,14 @@ codeunit 7764 "AOAI Chat Messages Impl"
for Counter := 1 to HistoryUserMessages.Count() do begin
HistoryUserMessages.Get(Counter, AOAIUserMessage);
if AOAIUserMessage.HasFilePart() then
- if Deployment.Unwrap() <> AOAIDeployments.GetGPT41MiniPreview() then
+ if UnwrapSecret(Deployment) <> AOAIDeployments.GetGPT41MiniPreview() then
Error(IncompatibleModelErr);
end;
end;
-}
\ No newline at end of file
+ [NonDebuggable]
+ local procedure UnwrapSecret(Secret: SecretText): Text
+ begin
+ exit(Secret.Unwrap());
+ end;
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al
index bf08eda527..58559307f0 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al
@@ -17,7 +17,6 @@ codeunit 7778 "AOAI Tools Impl"
FunctionNames: Dictionary of [Text, Integer];
Initialized: Boolean;
AddToolToPayload: Boolean;
- [NonDebuggable]
ToolChoice: Text;
ToolObjectInvalidErr: Label '%1 object does not contain %2 property.', Comment = '%1 is the object name and %2 is the property that is missing.';
ToolTypeErr: Label 'Tool type must be of function type.';
@@ -82,7 +81,6 @@ codeunit 7778 "AOAI Tools Impl"
Clear(FunctionNames);
end;
- [NonDebuggable]
procedure PrepareTools() ToolsResult: JsonArray
var
Counter: Integer;
@@ -116,7 +114,6 @@ codeunit 7778 "AOAI Tools Impl"
AddToolToPayload := AddToolsToPayload;
end;
- [NonDebuggable]
procedure SetToolChoice(NewToolChoice: Text)
begin
Initialize();
@@ -142,7 +139,6 @@ codeunit 7778 "AOAI Tools Impl"
ToolChoiceObject.WriteTo(ToolChoice);
end;
- [NonDebuggable]
procedure GetToolChoice(): Text
begin
exit(ToolChoice);
@@ -172,7 +168,6 @@ codeunit 7778 "AOAI Tools Impl"
Initialized := true;
end;
- [NonDebuggable]
local procedure ValidateTool(ToolObject: JsonObject): Boolean
var
AzureOpenAIImpl: Codeunit "Azure OpenAI Impl";
@@ -212,4 +207,4 @@ codeunit 7778 "AOAI Tools Impl"
end;
exit(true);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIUserMessageImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIUserMessageImpl.Codeunit.al
index 92cd084937..5d8f1851e3 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIUserMessageImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIUserMessageImpl.Codeunit.al
@@ -12,12 +12,10 @@ codeunit 7784 "AOAI User Message Impl"
var
CopilotCapabilityImpl: Codeunit "Copilot Capability Impl";
- [NonDebuggable]
ContentParts: JsonArray;
HasFileContent, HasTextContent : Boolean;
NotMicrosoftPublisherErr: Label 'This functionality is only available to Microsoft published apps.';
- [NonDebuggable]
procedure AddTextPart(TextContent: Text; CallerModuleInfo: ModuleInfo)
var
TextPartObject: JsonObject;
@@ -30,7 +28,6 @@ codeunit 7784 "AOAI User Message Impl"
HasTextContent := true;
end;
- [NonDebuggable]
procedure AddFilePart(FileData: Text; CallerModuleInfo: ModuleInfo)
var
FilePartObject: JsonObject;
@@ -45,7 +42,6 @@ codeunit 7784 "AOAI User Message Impl"
HasFileContent := true;
end;
- [NonDebuggable]
procedure GetContentParts(): JsonArray
begin
exit(ContentParts);
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParams.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParams.Codeunit.al
index 1dc5c4b30b..d4587bc74a 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParams.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParams.Codeunit.al
@@ -135,9 +135,8 @@ codeunit 7765 "AOAI Text Completion Params"
/// Add the completion parameters to the payload.
///
/// The JsonObject payload to add the completion parameters to.
- [NonDebuggable]
internal procedure AddCompletionsParametersToPayload(var Payload: JsonObject)
begin
AOAITextCompletionParamsImpl.AddCompletionsParametersToPayload(Payload);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParamsImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParamsImpl.Codeunit.al
index b3e5ba4aa6..aea173684b 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParamsImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/Text Completion/AOAITextCompletionParamsImpl.Codeunit.al
@@ -127,7 +127,6 @@ codeunit 7766 "AOAI TextCompletionParams Impl"
FrequencyPenalty := NewFrequencyPenalty;
end;
- [NonDebuggable]
procedure AddCompletionsParametersToPayload(var Payload: JsonObject)
begin
if GetMaxTokens() > 0 then
@@ -152,4 +151,4 @@ codeunit 7766 "AOAI TextCompletionParams Impl"
SetPresencePenalty(0);
SetFrequencyPenalty(0);
end;
-}
\ No newline at end of file
+}
diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al
index dacbc0e3e3..af30c1418c 100644
--- a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al
@@ -305,7 +305,6 @@ codeunit 7774 "Copilot Capability Impl"
exit(CheckPrivacyNoticeState(Silent, Capability));
end;
- [NonDebuggable]
local procedure IsTenantAllowedToUseAOAI(): Boolean
var
EnvironmentInformation: Codeunit "Environment Information";
@@ -560,4 +559,4 @@ codeunit 7774 "Copilot Capability Impl"
begin
exit(EnvironmentInformation.IsProduction());
end;
-}
\ No newline at end of file
+}