diff --git a/Apps/W1/EnforcedDigitalVouchers/app/Permissions/DigitalVoucherObjects.PermissionSet.al b/Apps/W1/EnforcedDigitalVouchers/app/Permissions/DigitalVoucherObjects.PermissionSet.al index b9ed6c23e5..c23da3955c 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/Permissions/DigitalVoucherObjects.PermissionSet.al +++ b/Apps/W1/EnforcedDigitalVouchers/app/Permissions/DigitalVoucherObjects.PermissionSet.al @@ -12,15 +12,16 @@ permissionset 5583 "Digital Voucher - Objects" Permissions = table "Digital Voucher Entry Setup" = X, table "Digital Voucher Setup" = X, table "Voucher Entry Source Code" = X, - page "Digital Voucher Entry Setup" = X, - page "Digital Voucher Setup" = X, - page "Digital Voucher Guide" = X, - page "Voucher Entry Source Codes" = X, - codeunit "Voucher Attach Or Note Check" = X, + codeunit "Digital Voucher Entry" = X, + codeunit "Digital Voucher Feature" = X, + codeunit "Digital Voucher Impl." = X, codeunit "Voucher Attachment Check" = X, + codeunit "Voucher Attach Or Note Check" = X, + codeunit "Voucher E-Document Check" = X, codeunit "Voucher No Check" = X, codeunit "Voucher Unknown Check" = X, - codeunit "Digital Voucher Impl." = X, - codeunit "Digital Voucher Feature" = X, - codeunit "Digital Voucher Entry" = X; + page "Digital Voucher Entry Setup" = X, + page "Digital Voucher Guide" = X, + page "Digital Voucher Setup" = X, + page "Voucher Entry Source Codes" = X; } diff --git a/Apps/W1/EnforcedDigitalVouchers/app/app.json b/Apps/W1/EnforcedDigitalVouchers/app/app.json index 838ba947f3..afccec4c73 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/app.json +++ b/Apps/W1/EnforcedDigitalVouchers/app/app.json @@ -11,7 +11,14 @@ "url": "https://go.microsoft.com/fwlink/?LinkId=724011", "logo": "ExtensionLogo.png", "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2204541", - "dependencies": [], + "dependencies": [ + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", + "name": "E-Document Core", + "publisher": "Microsoft", + "version": "28.0.0.0" + } + ], "screenshots": [], "platform": "28.0.0.0", "internalsVisibleTo": [ @@ -20,6 +27,11 @@ "name": "Enforced Digital Vouchers Test Library", "publisher": "Microsoft" }, + { + "id": "f0eb8756-ea72-4ef8-b0de-686d2a44b259", + "name": "Enforced Digital Vouchers Tests", + "publisher": "Microsoft" + }, { "id": "bb837764-d7cc-4b7b-898a-3ea5a1fab62f", "name": "Enforced Digital Vouchers (DK)", @@ -42,4 +54,4 @@ }, "application": "29.0.0.0", "target": "Cloud" -} +} \ No newline at end of file diff --git a/Apps/W1/EnforcedDigitalVouchers/app/src/EntrySetup/DigitalVoucherEntrySetup.Table.al b/Apps/W1/EnforcedDigitalVouchers/app/src/EntrySetup/DigitalVoucherEntrySetup.Table.al index 270499c647..9a530b3fd6 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/src/EntrySetup/DigitalVoucherEntrySetup.Table.al +++ b/Apps/W1/EnforcedDigitalVouchers/app/src/EntrySetup/DigitalVoucherEntrySetup.Table.al @@ -21,9 +21,26 @@ table 5579 "Digital Voucher Entry Setup" } field(2; "Check Type"; Enum "Digital Voucher Check Type") { + trigger OnValidate() + var + CheckTypeEDocErr: Label '%1 %2 is available only for %3 %4 or %5.', Comment = '%1 - Check type field caption, %2 - Check type value, %3 - Entry type field caption, %4 - Entry type value, %5 - Entry type value'; + begin + if "Check Type" = "Check Type"::"E-Document" then begin + if not ("Entry Type" in ["Entry Type"::"Sales Document", "Entry Type"::"Purchase Document"]) then + Error(CheckTypeEDocErr, FieldCaption("Check Type"), "Check Type"::"E-Document", FieldCaption("Entry Type"), "Entry Type"::"Sales Document", "Entry Type"::"Purchase Document"); + "Generate Automatically" := true; + end; + end; } field(3; "Generate Automatically"; Boolean) { + trigger OnValidate() + var + GenerateAutoMustBeEnabledErr: Label 'Generate Automatically must be enabled when %1 is %2.', Comment = '%1 - Check type field caption, %2 - Check type value'; + begin + if ("Check Type" = "Check Type"::"E-Document") and (not "Generate Automatically") then + Error(GenerateAutoMustBeEnabledErr, FieldCaption("Check Type"), "Check Type"::"E-Document"); + end; } field(4; "Skip If Manually Added"; Boolean) { @@ -47,4 +64,4 @@ table 5579 "Digital Voucher Entry Setup" Clustered = true; } } -} +} \ No newline at end of file diff --git a/Apps/W1/EnforcedDigitalVouchers/app/src/Extensions/DigVoucherIncDocAttach.TableExt.al b/Apps/W1/EnforcedDigitalVouchers/app/src/Extensions/DigVoucherIncDocAttach.TableExt.al index a86976dc86..aef35ff27e 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/src/Extensions/DigVoucherIncDocAttach.TableExt.al +++ b/Apps/W1/EnforcedDigitalVouchers/app/src/Extensions/DigVoucherIncDocAttach.TableExt.al @@ -12,5 +12,9 @@ tableextension 5582 "Dig. Voucher Inc. Doc. Attach." extends "Incoming Document { DataClassification = SystemMetadata; } + field(5583; "Is E-Document"; Boolean) + { + DataClassification = SystemMetadata; + } } } \ No newline at end of file diff --git a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherCheckType.Enum.al b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherCheckType.Enum.al index e522f49d46..528ffcba8c 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherCheckType.Enum.al +++ b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherCheckType.Enum.al @@ -26,4 +26,9 @@ enum 5580 "Digital Voucher Check Type" implements "Digital Voucher Check" Caption = 'Attachment or Note'; Implementation = "Digital Voucher Check" = "Voucher Attach Or Note Check"; } + value(3; "E-Document") + { + Caption = 'E-Document'; + Implementation = "Digital Voucher Check" = "Voucher E-Document Check"; + } } diff --git a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherImpl.Codeunit.al b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherImpl.Codeunit.al index 0868541b1e..e07710c656 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherImpl.Codeunit.al +++ b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/DigitalVoucherImpl.Codeunit.al @@ -24,6 +24,7 @@ using Microsoft.Service.History; using Microsoft.Service.Posting; using System.Email; using System.Environment.Configuration; +using System.IO; using System.Media; using System.Reflection; using System.Utilities; @@ -38,13 +39,13 @@ codeunit 5579 "Digital Voucher Impl." Tabledata "Purch. Cr. Memo Hdr." = m; var - DigitalVoucherFeature: Codeunit "Digital Voucher Feature"; DigitalVoucherEntry: Codeunit "Digital Voucher Entry"; - AssistedSetupTxt: Label 'Set up a digital voucher feature'; + DigitalVoucherFeature: Codeunit "Digital Voucher Feature"; AssistedSetupDescriptionTxt: Label 'In some countries authorities require to make sure that for every single general ledger register ther is a digital vouchers assigned.'; AssistedSetupHelpTxt: Label 'https://learn.microsoft.com/en-us/dynamics365/business-central/across-how-setup-digital-vouchers', Locked = true; - CannotRemoveReferenceRecordFromIncDocErr: Label 'Cannot remove the reference record from the incoming document because it is used for the enforced digital voucher functionality'; + AssistedSetupTxt: Label 'Set up a digital voucher feature'; CannotChangeIncomDocWithEnforcedDigitalVoucherErr: Label 'Cannot change incoming document with the enforced digital voucher functionality'; + CannotRemoveReferenceRecordFromIncDocErr: Label 'Cannot remove the reference record from the incoming document because it is used for the enforced digital voucher functionality'; DigitalVoucherFileTxt: Label 'DigitalVoucher_%1_%2_%3.pdf', Comment = '%1 = doc type; %2 = posting date; %3 = doc no.'; procedure HandleDigitalVoucherForDocument(var ErrorMessageMgt: Codeunit "Error Message Management"; EntryType: Enum "Digital Voucher Entry Type"; Record: Variant) @@ -103,13 +104,13 @@ codeunit 5579 "Digital Voucher Impl." [CommitBehavior(CommitBehavior::Ignore)] procedure GenerateDigitalVoucherForDocument(RecRef: RecordRef) var - SalesInvHeader: Record "Sales Invoice Header"; - SalesCrMemoHeader: Record "Sales Cr.Memo Header"; - ServInvHeader: Record "Service Invoice Header"; - ServCrMemoHeader: Record "Service Cr.Memo Header"; - PurchInvHeader: Record "Purch. Inv. Header"; PurchCrMemoHeader: Record "Purch. Cr. Memo Hdr."; + PurchInvHeader: Record "Purch. Inv. Header"; ReportSelections: Record "Report Selections"; + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + SalesInvHeader: Record "Sales Invoice Header"; + ServCrMemoHeader: Record "Service Cr.Memo Header"; + ServInvHeader: Record "Service Invoice Header"; begin case RecRef.Number of Database::"Sales Invoice Header": @@ -246,7 +247,7 @@ codeunit 5579 "Digital Voucher Impl." exit; DataTypeManagement.GetRecordRef(RelatedRecord, RecRef); if DigitalVoucherFeature.IsDigitalVoucherEnabledForTableNumber(RecRef.Number) then - error(CannotChangeIncomDocWithEnforcedDigitalVoucherErr); + Error(CannotChangeIncomDocWithEnforcedDigitalVoucherErr); end; procedure GetDigitalVoucherEntrySetup(var DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; EntryType: Enum "Digital Voucher Entry Type"): Boolean @@ -266,6 +267,132 @@ codeunit 5579 "Digital Voucher Impl." exit(not IncomingDocumentAttachment.IsEmpty()); end; + local procedure AttachIncomingEDocument(EDocument: Record "E-Document"; SourceDocumentHeader: RecordRef; var TempBlob: Codeunit "Temp Blob") + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + IncomingDocumentAttachment: Record "Incoming Document Attachment"; + EDocumentHelper: Codeunit "E-Document Processing"; + ImportAttachmentIncDoc: Codeunit "Import Attachment - Inc. Doc."; + FileNameTok: Label 'E-Document_%1.xml', Locked = true; + RecordLinkTxt: Text; + begin + if not DigitalVoucherFeature.IsFeatureEnabled() then + exit; + + GetDigitalVoucherEntrySetup(DigitalVoucherEntrySetup, "Digital Voucher Entry Type"::"Sales Document"); + if DigitalVoucherEntrySetup."Check Type" <> DigitalVoucherEntrySetup."Check Type"::"E-Document" then + exit; + + if not (EDocument."Document Type" in [ + EDocument."Document Type"::"Sales Invoice", + EDocument."Document Type"::"Sales Credit Memo", + EDocument."Document Type"::"Sales Order", + EDocument."Document Type"::"Sales Quote", + EDocument."Document Type"::"Sales Return Order"]) then + exit; + + RecordLinkTxt := EDocumentHelper.GetRecordLinkText(EDocument); + IncomingDocumentAttachment.SetRange("Document No.", EDocument."Document No."); + IncomingDocumentAttachment.SetRange("Posting Date", EDocument."Posting Date"); + IncomingDocumentAttachment.SetContentFromBlob(TempBlob); + if not ImportAttachmentIncDoc.ImportAttachment(IncomingDocumentAttachment, StrSubstNo(FileNameTok, RecordLinkTxt), TempBlob) then + exit; + + IncomingDocumentAttachment."Is E-Document" := true; + IncomingDocumentAttachment.Modify(false); + end; + + local procedure AttachOutgoingEDocument(var EDocument: Record "E-Document"; PostedRecord: Variant) + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + VoucherEDocumentCheck: Codeunit "Voucher E-Document Check"; + RecRef: RecordRef; + DocNo: Code[20]; + PostingDate: Date; + DocType: Text; + begin + if not DigitalVoucherFeature.IsFeatureEnabled() then + exit; + + GetDigitalVoucherEntrySetup(DigitalVoucherEntrySetup, "Digital Voucher Entry Type"::"Purchase Document"); + if DigitalVoucherEntrySetup."Check Type" <> DigitalVoucherEntrySetup."Check Type"::"E-Document" then + exit; + + if not (EDocument."Document Type" in [ + EDocument."Document Type"::"Purchase Invoice", + EDocument."Document Type"::"Purchase Credit Memo", + EDocument."Document Type"::"Purchase Order", + EDocument."Document Type"::"Purchase Quote", + EDocument."Document Type"::"Purchase Return Order"]) then + exit; + + RecRef.GetTable(PostedRecord); + DigitalVoucherEntry.GetDocNoAndPostingDateFromRecRef(DocType, DocNo, PostingDate, RecRef); + AttachPurchaseEDocument(EDocument, DocNo, PostingDate); + end; + + local procedure AttachPurchaseEDocument(EDocument: Record "E-Document"; DocumentNo: Code[20]; PostingDate: Date) + var + EDocDataStorage: Record "E-Doc. Data Storage"; + IncomingDocumentAttachment: Record "Incoming Document Attachment"; + ImportAttachmentIncDoc: Codeunit "Import Attachment - Inc. Doc."; + TempBlob: Codeunit "Temp Blob"; + EDocumentFileNameLbl: Label 'E-Document_%1.%2', Comment = '%1 = E-Document Entry No., %2 = File Format', Locked = true; + FileName: Text[250]; + begin + if EDocument."Unstructured Data Entry No." = 0 then + exit; + + if not EDocDataStorage.Get(EDocument."Unstructured Data Entry No.") then + exit; + + TempBlob := EDocDataStorage.GetTempBlob(); + if not TempBlob.HasValue() then + exit; + + if EDocument."File Name" <> '' then + FileName := CopyStr(EDocument."File Name", 1, MaxStrLen(FileName)) + else + FileName := StrSubstNo(EDocumentFileNameLbl, EDocument."Entry No", EDocDataStorage."File Format"); + + IncomingDocumentAttachment.SetRange("Document No.", DocumentNo); + IncomingDocumentAttachment.SetRange("Posting Date", PostingDate); + IncomingDocumentAttachment.SetContentFromBlob(TempBlob); + if not ImportAttachmentIncDoc.ImportAttachment(IncomingDocumentAttachment, FileName, TempBlob) then + exit; + + IncomingDocumentAttachment."Is E-Document" := true; + IncomingDocumentAttachment.Modify(false); + + if EDocDataStorage."File Format" = EDocDataStorage."File Format"::PDF then begin + FileName := StrSubstNo(EDocumentFileNameLbl, EDocument."Entry No", EDocDataStorage."File Format"::XML); + ExtractXMLFromPDF(TempBlob, FileName, IncomingDocumentAttachment); + end; + end; + + local procedure ExtractXMLFromPDF(var TempBlob: Codeunit System.Utilities."Temp Blob"; FileName: Text[250]; var IncomingDocumentAttachment: Record "Incoming Document Attachment") + var + ImportAttachmentIncDoc: Codeunit "Import Attachment - Inc. Doc."; + PDFDocument: Codeunit "PDF Document"; + ExtractedXmlBlob: Codeunit "Temp Blob"; + PdfInStream: InStream; + begin + TempBlob.CreateInStream(PdfInStream); + if not PDFDocument.GetDocumentAttachmentStream(PdfInStream, ExtractedXmlBlob) then + exit; + + if not ExtractedXmlBlob.HasValue() then + exit; + + IncomingDocumentAttachment.Default := false; + IncomingDocumentAttachment."Main Attachment" := false; + if not ImportAttachmentIncDoc.ImportAttachment(IncomingDocumentAttachment, FileName, ExtractedXmlBlob) then + exit; + + IncomingDocumentAttachment."Is E-Document" := true; + IncomingDocumentAttachment.Modify(false); + end; + local procedure FilterIncomingDocumentRecordFromRecordRef(var IncomingDocumentAttachment: Record "Incoming Document Attachment"; var IncomingDocument: Record "Incoming Document"; MainRecordRef: RecordRef): Boolean begin Clear(IncomingDocument); @@ -334,8 +461,8 @@ codeunit 5579 "Digital Voucher Impl." local procedure IsPaymentReconciliationJournal(DigitalVoucherEntryType: Enum "Digital Voucher Entry Type"; RecRef: RecordRef): Boolean var - SourceCodeSetup: Record "Source Code Setup"; GenJournalLine: Record "Gen. Journal Line"; + SourceCodeSetup: Record "Source Code Setup"; FieldRef: FieldRef; SourceCodeValue: Text; begin @@ -351,8 +478,8 @@ codeunit 5579 "Digital Voucher Impl." local procedure IsGenJnlLineWithIncDocAttachedToAdjLine(DigitalVoucherEntryType: Enum "Digital Voucher Entry Type"; RecRef: RecordRef): Boolean var - GenJournalLine: Record "Gen. Journal Line"; AdjacentGenJournalLine: Record "Gen. Journal Line"; + GenJournalLine: Record "Gen. Journal Line"; IncomingDocument: Record "Incoming Document"; begin if DigitalVoucherEntryType <> DigitalVoucherEntryType::"General Journal" then @@ -538,8 +665,8 @@ codeunit 5579 "Digital Voucher Impl." [EventSubscriber(ObjectType::Codeunit, Codeunit::"Service-Post", 'OnAfterPostServiceDoc', '', true, true)] local procedure CheckServiceVoucherOnAfterPostServiceDoc(ServInvoiceNo: Code[20]; ServCrMemoNo: Code[20]; PassedInvoice: Boolean) var - ServInvHeader: Record "Service Invoice Header"; ServCrMemoHeader: Record "Service Cr.Memo Header"; + ServInvHeader: Record "Service Invoice Header"; RecVar: Variant; begin if not DigitalVoucherFeature.IsFeatureEnabled() then @@ -639,8 +766,8 @@ codeunit 5579 "Digital Voucher Impl." [EventSubscriber(ObjectType::Table, Database::"Incoming Document", 'OnBeforeCanReplaceMainAttachment', '', false, false)] local procedure CheckVoucherOnBeforeCanReplaceMainAttachment(var CanReplaceMainAttachment: Boolean; IncomingDocument: Record "Incoming Document"; var IsHandled: Boolean) var - RecRef: RecordRef; RelatedRecordID: RecordId; + RecRef: RecordRef; begin RelatedRecordID := IncomingDocument."Related Record ID"; if RelatedRecordID.TableNo = 0 then @@ -653,8 +780,8 @@ codeunit 5579 "Digital Voucher Impl." [EventSubscriber(ObjectType::Table, Database::"Incoming Document", 'OnBeforeRemoveReferencedRecords', '', false, false)] local procedure CheckVoucherOnBeforeRemoveReferencedRecords(IncomingDocument: Record "Incoming Document"; var IsHandled: Boolean) var - RecRef: RecordRef; RelatedRecordID: RecordId; + RecRef: RecordRef; begin if not DigitalVoucherFeature.IsFeatureEnabled() then exit; @@ -677,6 +804,7 @@ codeunit 5579 "Digital Voucher Impl." local procedure ExcludeDigitalVouchersOnAttachIncomingDocumentsOnAfterSetFilter(var IncomingDocumentAttachment: Record "Incoming Document Attachment") begin IncomingDocumentAttachment.SetRange("Is Digital Voucher", false); + IncomingDocumentAttachment.SetRange("Is E-Document", false); end; [EventSubscriber(ObjectType::Table, Database::"Digital Voucher Setup", 'OnBeforeDeleteEvent', '', false, false)] @@ -699,6 +827,21 @@ codeunit 5579 "Digital Voucher Impl." CopyDigitalVoucherToCorrectiveDocument("Digital Voucher Entry Type"::"Sales Document", SalesInvoiceHeader, SalesHeader."No.", SalesHeader."Posting Date"); end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Export", OnExportEDocumentAfterCreateEDocument, '', false, false)] + local procedure EDocExportOnExportEDocumentAfterCreateEDocument(EDocument: Record "E-Document"; SourceDocumentHeaderMapped: RecordRef; SourceDocumentLineMapped: RecordRef; var TempBlob: Codeunit "Temp Blob"; Success: Boolean) + begin + if not Success then + exit; + + AttachIncomingEDocument(EDocument, SourceDocumentHeaderMapped, TempBlob); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Document Subscribers", OnAfterUpdateToPostedPurchaseEDocument, '', false, false)] + local procedure EDocumentSubscribers_OnAfterUpdateToPostedPurchaseEDocument(var EDocument: Record "E-Document"; PostedRecord: Variant; DocumentType: Enum "E-Document Type") + begin + AttachOutgoingEDocument(EDocument, PostedRecord); + end; + [IntegrationEvent(false, false)] local procedure OnGenerateDigitalVoucherForDocumentOnCaseElse(RecRef: RecordRef) begin diff --git a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherAttachmentCheck.Codeunit.al b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherAttachmentCheck.Codeunit.al index 575070c823..bdc0daa3e1 100644 --- a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherAttachmentCheck.Codeunit.al +++ b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherAttachmentCheck.Codeunit.al @@ -22,7 +22,7 @@ codeunit 5580 "Voucher Attachment Check" implements "Digital Voucher Check" if (DigitalVoucherEntryType in [DigitalVoucherEntryType::"General Journal", DigitalVoucherEntryType::"Purchase Journal", DigitalVoucherEntryType::"Sales Journal"]) or (RecRef.Number() = Database::"Service Header") then - error(NotPossibleToPostWithoutVoucherErr); + Error(NotPossibleToPostWithoutVoucherErr); ErrorMessageMgt.LogSimpleErrorMessage(NotPossibleToPostWithoutVoucherErr); end; diff --git a/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherEDocumentCheck.Codeunit.al b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherEDocumentCheck.Codeunit.al new file mode 100644 index 0000000000..613d7e55d5 --- /dev/null +++ b/Apps/W1/EnforcedDigitalVouchers/app/src/Implementation/VoucherEDocumentCheck.Codeunit.al @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument; + +using System.Utilities; + +codeunit 5588 "Voucher E-Document Check" implements "Digital Voucher Check" +{ + Access = Internal; + + /// + /// Validates that an E-Document is attached to the document before posting. + /// Only applies to Purchase Documents when the Digital Voucher feature is enabled and Check Type is set to E-Document. + /// + /// Error message management for logging validation errors. + /// The type of digital voucher entry being validated. + /// Record reference to the document being validated. + internal procedure CheckVoucherIsAttachedToDocument(var ErrorMessageMgt: Codeunit "Error Message Management"; DigitalVoucherEntryType: Enum "Digital Voucher Entry Type"; RecRef: RecordRef) + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + EDocument: Record "E-Document"; + DigitalVoucherFeature: Codeunit "Digital Voucher Feature"; + DigitalVoucherImpl: Codeunit "Digital Voucher Impl."; + NotPossibleToPostWithoutEDocumentErr: Label 'Not possible to post without linking an E-Document.'; + begin + if not DigitalVoucherFeature.IsFeatureEnabled() then + exit; + + if DigitalVoucherEntryType <> DigitalVoucherEntryType::"Purchase Document" then + exit; + + DigitalVoucherImpl.GetDigitalVoucherEntrySetup(DigitalVoucherEntrySetup, DigitalVoucherEntryType); + if DigitalVoucherEntrySetup."Check Type" <> DigitalVoucherEntrySetup."Check Type"::"E-Document" then + exit; + + EDocument.SetRange("Document Record ID", RecRef.RecordId()); + if EDocument.IsEmpty() then begin + ErrorMessageMgt.LogSimpleErrorMessage(NotPossibleToPostWithoutEDocumentErr); + exit; + end; + end; + + /// + /// Generates a digital voucher for a posted document by delegating to the Attachment check type implementation. + /// This procedure retrieves the digital voucher entry setup and invokes the attachment-based voucher generation. + /// + /// The type of digital voucher entry for the posted document. + /// Record reference to the posted document. + internal procedure GenerateDigitalVoucherForPostedDocument(DigitalVoucherEntryType: Enum "Digital Voucher Entry Type"; RecRef: RecordRef) + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + DigitalVoucherImpl: Codeunit "Digital Voucher Impl."; + DigitalVoucherCheck: Interface "Digital Voucher Check"; + begin + DigitalVoucherImpl.GetDigitalVoucherEntrySetup(DigitalVoucherEntrySetup, DigitalVoucherEntryType); + DigitalVoucherCheck := DigitalVoucherEntrySetup."Check Type"::Attachment; + DigitalVoucherCheck.GenerateDigitalVoucherForPostedDocument(DigitalVoucherEntrySetup."Entry Type", RecRef); + end; +} \ No newline at end of file diff --git a/Apps/W1/EnforcedDigitalVouchers/digital-voucher.code-workspace b/Apps/W1/EnforcedDigitalVouchers/digital-voucher.code-workspace new file mode 100644 index 0000000000..c3cf87c90e --- /dev/null +++ b/Apps/W1/EnforcedDigitalVouchers/digital-voucher.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "app" + }, + { + "path": "test" + }, + { + "path": "test library" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/Apps/W1/EnforcedDigitalVouchers/test/app.json b/Apps/W1/EnforcedDigitalVouchers/test/app.json index 2d8ee2105f..74f3e406e0 100644 --- a/Apps/W1/EnforcedDigitalVouchers/test/app.json +++ b/Apps/W1/EnforcedDigitalVouchers/test/app.json @@ -19,6 +19,12 @@ "publisher": "Microsoft", "version": "29.0.0.0" }, + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", + "name": "E-Document Core", + "publisher": "Microsoft", + "version": "28.0.0.0" + }, { "id": "928f7b70-0dbd-431a-beb5-f45c4adbd361", "name": "Enforced Digital Vouchers Test Library", @@ -48,6 +54,10 @@ { "from": 139515, "to": 139516 + }, + { + "from": 139649, + "to": 139649 } ], "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2141039", @@ -60,4 +70,4 @@ "features": [ "TranslationFile" ] -} +} \ No newline at end of file diff --git a/Apps/W1/EnforcedDigitalVouchers/test/src/EDocAttachmentTests.Codeunit.al b/Apps/W1/EnforcedDigitalVouchers/test/src/EDocAttachmentTests.Codeunit.al new file mode 100644 index 0000000000..c717cc1929 --- /dev/null +++ b/Apps/W1/EnforcedDigitalVouchers/test/src/EDocAttachmentTests.Codeunit.al @@ -0,0 +1,425 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Test.EServices.EDocument; + +using Microsoft.eServices.EDocument; +using Microsoft.Foundation.Address; +using Microsoft.Purchases.Document; +using Microsoft.Purchases.History; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; +using Microsoft.Sales.History; +using Microsoft.Tests.EServices.EDocument; + +codeunit 139649 "E-Doc. Attachment Tests" +{ + Subtype = Test; + TestPermissions = Disabled; + + var + Assert: Codeunit Assert; + LibraryERM: Codeunit "Library - ERM"; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryPurchase: Codeunit "Library - Purchase"; + LibraryRandom: Codeunit "Library - Random"; + LibrarySales: Codeunit "Library - Sales"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + IsInitialized: Boolean; + DialogErrorCodeTok: Label 'Dialog', Locked = true; + NotPossibleToPostWithoutEDocumentErr: Label 'Not possible to post without linking an E-Document.', Locked = true; + EDocumentTok: Label 'E-Document', Locked = true; + + [Test] + procedure GenerateAutomaticallyEnabledWhenCheckTypeSetToEDocument() + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Generate Automatically is auto-enabled when Check Type is set to E-Document + Initialize(); + + // [GIVEN] Setup record for Purchase Document with Generate Automatically = false + DigitalVoucherEntrySetup."Entry Type" := DigitalVoucherEntrySetup."Entry Type"::"Purchase Document"; + DigitalVoucherEntrySetup."Generate Automatically" := false; + + // [WHEN] Validate Check Type = E-Document + DigitalVoucherEntrySetup.Validate("Check Type", DigitalVoucherEntrySetup."Check Type"::"E-Document"); + + // [THEN] Generate Automatically is automatically set to true + DigitalVoucherEntrySetup.TestField("Generate Automatically", true); + end; + + [Test] + procedure GenerateAutomaticallyCannotBeDisabledForEDocument() + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + GenerateAutomaticallyErrorTok: Label 'Generate Automatically must be enabled', Locked = true; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Generate Automatically cannot be disabled when Check Type = E-Document + Initialize(); + + // [GIVEN] Entry Type = Purchase Document, Check Type = E-Document + DigitalVoucherEntrySetup."Entry Type" := DigitalVoucherEntrySetup."Entry Type"::"Purchase Document"; + DigitalVoucherEntrySetup.Validate("Check Type", DigitalVoucherEntrySetup."Check Type"::"E-Document"); + + // [WHEN] Try to disable Generate Automatically + asserterror DigitalVoucherEntrySetup.Validate("Generate Automatically", false); + + // [THEN] Error: Generate Automatically must be enabled when Check Type is E-Document + Assert.ExpectedError(GenerateAutomaticallyErrorTok); + end; + + [Test] + procedure EDocumentCheckTypeNotAllowedForGeneralJournal() + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] E-Document check type is not allowed for General Journal + Initialize(); + + // [GIVEN] Entry Type = General Journal + DigitalVoucherEntrySetup."Entry Type" := DigitalVoucherEntrySetup."Entry Type"::"General Journal"; + + // [WHEN] Validate Check Type = E-Document + asserterror DigitalVoucherEntrySetup.Validate("Check Type", DigitalVoucherEntrySetup."Check Type"::"E-Document"); + + // [THEN] Error mentioning E-Document requires Sales Document or Purchase Document + Assert.ExpectedError(EDocumentTok); + end; + + [Test] + procedure EDocumentCheckTypeNotAllowedForSalesJournal() + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] E-Document check type is not allowed for Sales Journal + Initialize(); + + // [GIVEN] Entry Type = Sales Journal + DigitalVoucherEntrySetup."Entry Type" := DigitalVoucherEntrySetup."Entry Type"::"Sales Journal"; + + // [WHEN] Validate Check Type = E-Document + asserterror DigitalVoucherEntrySetup.Validate("Check Type", DigitalVoucherEntrySetup."Check Type"::"E-Document"); + + // [THEN] Error mentioning E-Document requires Sales Document or Purchase Document + Assert.ExpectedError(EDocumentTok); + end; + + [Test] + procedure EDocumentCheckTypeNotAllowedForPurchaseJournal() + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] E-Document check type is not allowed for Purchase Journal + Initialize(); + + // [GIVEN] Entry Type = Purchase Journal + DigitalVoucherEntrySetup."Entry Type" := DigitalVoucherEntrySetup."Entry Type"::"Purchase Journal"; + + // [WHEN] Validate Check Type = E-Document + asserterror DigitalVoucherEntrySetup.Validate("Check Type", DigitalVoucherEntrySetup."Check Type"::"E-Document"); + + // [THEN] Error mentioning E-Document requires Sales Document or Purchase Document + Assert.ExpectedError(EDocumentTok); + end; + + [Test] + procedure PurchaseInvoiceCannotPostWithoutEDocument() + var + DummyEDocument: Record "E-Document"; + PurchaseHeader: Record "Purchase Header"; + DigVouchersDisableEnforce: Codeunit "Dig. Vouchers Disable Enforce"; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Purchase invoice posting fails when E-Document check is enabled but no e-document linked + Initialize(); + BindSubscription(DigVouchersDisableEnforce); + EnableDigitalVoucherFeature(); + + // [GIVEN] Digital voucher entry setup for purchase document is "E-Document" + InitSetupEDocument("Digital Voucher Entry Type"::"Purchase Document"); + + // [GIVEN] Purchase invoice WITHOUT linked e-document + CreatePurchaseDocument(PurchaseHeader, "Purchase Document Type"::Invoice, DummyEDocument); + + // [WHEN] Post purchase invoice + asserterror LibraryPurchase.PostPurchaseDocument(PurchaseHeader, true, true); + + // [THEN] Error: Not possible to post without linking an E-Document + Assert.ExpectedErrorCode(DialogErrorCodeTok); + Assert.ExpectedError(NotPossibleToPostWithoutEDocumentErr); + + UnbindSubscription(DigVouchersDisableEnforce); + end; + + [Test] + procedure PurchaseInvoiceEDocumentAttachedAfterPosting() + var + EDocument: Record "E-Document"; + PurchInvHeader: Record "Purch. Inv. Header"; + PurchaseHeader: Record "Purchase Header"; + DigVouchersDisableEnforce: Codeunit "Dig. Vouchers Disable Enforce"; + DocNo: Code[20]; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Purchase invoice with e-document creates incoming attachment with Is E-Document = true + Initialize(); + BindSubscription(DigVouchersDisableEnforce); + EnableDigitalVoucherFeature(); + + // [GIVEN] Digital voucher entry setup for purchase document is "E-Document" + InitSetupEDocument("Digital Voucher Entry Type"::"Purchase Document"); + + // [GIVEN] Purchase invoice with linked e-document + CreatePurchaseDocumentWithEDocument(PurchaseHeader, EDocument, "Purchase Document Type"::Invoice); + + // [WHEN] Post purchase invoice (e-document attachment happens automatically) + DocNo := LibraryPurchase.PostPurchaseDocument(PurchaseHeader, true, true); + + // [THEN] Incoming attachment exists with "Is E-Document" = true + PurchInvHeader.Get(DocNo); + AssertEDocumentIncomingAttachmentExists(PurchInvHeader."Posting Date", PurchInvHeader."No."); + + UnbindSubscription(DigVouchersDisableEnforce); + end; + + [Test] + procedure PurchaseCreditMemoEDocumentAttachedAfterPosting() + var + EDocument: Record "E-Document"; + PurchCrMemoHdr: Record "Purch. Cr. Memo Hdr."; + PurchaseHeader: Record "Purchase Header"; + DigVouchersDisableEnforce: Codeunit "Dig. Vouchers Disable Enforce"; + DocNo: Code[20]; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Purchase credit memo with e-document creates incoming attachment with Is E-Document = true + Initialize(); + BindSubscription(DigVouchersDisableEnforce); + EnableDigitalVoucherFeature(); + + // [GIVEN] Digital voucher entry setup for purchase document is "E-Document" + InitSetupEDocument("Digital Voucher Entry Type"::"Purchase Document"); + + // [GIVEN] Purchase credit memo with linked e-document + CreatePurchaseDocumentWithEDocument(PurchaseHeader, EDocument, "Purchase Document Type"::"Credit Memo"); + + // [WHEN] Post purchase credit memo (e-document attachment happens automatically) + DocNo := LibraryPurchase.PostPurchaseDocument(PurchaseHeader, true, true); + + // [THEN] Incoming attachment exists with "Is E-Document" = true + PurchCrMemoHdr.Get(DocNo); + AssertEDocumentIncomingAttachmentExists(PurchCrMemoHdr."Posting Date", PurchCrMemoHdr."No."); + + UnbindSubscription(DigVouchersDisableEnforce); + end; + + [Test] + procedure SalesInvoiceEDocumentAttachedAfterExport() + var + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + DigVouchersDisableEnforce: Codeunit "Dig. Vouchers Disable Enforce"; + DocNo: Code[20]; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Sales invoice e-document export creates incoming attachment with Is E-Document = true + Initialize(); + BindSubscription(DigVouchersDisableEnforce); + EnableDigitalVoucherFeature(); + + // [GIVEN] Digital voucher entry setup for sales document is "E-Document" + InitSetupEDocument("Digital Voucher Entry Type"::"Sales Document"); + + // [WHEN] Posted sales invoice + DocNo := CreateSalesDocumentWithEDocRequirements(SalesHeader, "Sales Document Type"::Invoice); + SalesInvoiceHeader.Get(DocNo); + + // [THEN] Incoming attachment exists with "Is E-Document" = true + AssertEDocumentIncomingAttachmentExists(SalesInvoiceHeader."Posting Date", SalesInvoiceHeader."No."); + + UnbindSubscription(DigVouchersDisableEnforce); + end; + + [Test] + procedure SalesCreditMemoEDocumentAttachedAfterExport() + var + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + SalesHeader: Record "Sales Header"; + DigVouchersDisableEnforce: Codeunit "Dig. Vouchers Disable Enforce"; + DocNo: Code[20]; + begin + // [FEATURE] [Digital Voucher] [E-Document] + // [SCENARIO] Sales credit memo e-document export creates incoming attachment with Is E-Document = true + Initialize(); + BindSubscription(DigVouchersDisableEnforce); + EnableDigitalVoucherFeature(); + + // [GIVEN] Digital voucher entry setup for sales document is "E-Document" + InitSetupEDocument("Digital Voucher Entry Type"::"Sales Document"); + + // [WHEN] Posted sales credit memo + DocNo := CreateSalesDocumentWithEDocRequirements(SalesHeader, "Sales Document Type"::"Credit Memo"); + SalesCrMemoHeader.Get(DocNo); + + // [THEN] Incoming attachment exists with "Is E-Document" = true + AssertEDocumentIncomingAttachmentExists(SalesCrMemoHeader."Posting Date", SalesCrMemoHeader."No."); + + UnbindSubscription(DigVouchersDisableEnforce); + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"E-Doc. Attachment Tests"); + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"E-Doc. Attachment Tests"); + + IsInitialized := true; + Commit(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"E-Doc. Attachment Tests"); + end; + + local procedure EnableDigitalVoucherFeature() + var + DigitalVoucherSetup: Record "Digital Voucher Setup"; + begin + if DigitalVoucherSetup.Get() then + DigitalVoucherSetup.Delete(false); + DigitalVoucherSetup.Enabled := true; + DigitalVoucherSetup.Insert(false); + end; + + local procedure InitSetupEDocument(EntryType: Enum "Digital Voucher Entry Type") + var + DigitalVoucherEntrySetup: Record "Digital Voucher Entry Setup"; + begin + DigitalVoucherEntrySetup.SetRange("Entry Type", EntryType); + DigitalVoucherEntrySetup.DeleteAll(false); + DigitalVoucherEntrySetup."Entry Type" := EntryType; + DigitalVoucherEntrySetup.Validate("Check Type", DigitalVoucherEntrySetup."Check Type"::"E-Document"); + DigitalVoucherEntrySetup.Insert(false); + end; + + local procedure CreatePurchaseDocumentWithEDocument(var PurchaseHeader: Record "Purchase Header"; var EDocument: Record "E-Document"; DocumentType: Enum "Purchase Document Type") + var + EDocumentService: Record "E-Document Service"; + EDocDataStorage: Record "E-Doc. Data Storage"; + EDocLogHelper: Codeunit "E-Document Log Helper"; + OutStream: OutStream; + EDocType: Enum "E-Document Type"; + XmlRootTag: Text; + FileName: Text; + begin + case DocumentType of + "Purchase Document Type"::Invoice: + begin + EDocType := EDocType::"Purchase Invoice"; + XmlRootTag := 'Invoice'; + FileName := 'test-invoice.xml'; + end; + "Purchase Document Type"::"Credit Memo": + begin + EDocType := EDocType::"Purchase Credit Memo"; + XmlRootTag := 'CreditNote'; + FileName := 'test-credit-memo.xml'; + end; + end; + + CreateService(EDocumentService); + + EDocDataStorage.Init(); + EDocDataStorage."File Format" := EDocDataStorage."File Format"::XML; + EDocDataStorage."Data Storage".CreateOutStream(OutStream); + OutStream.WriteText(StrSubstNo('<%1>%2', XmlRootTag, PurchaseHeader."No.")); + EDocDataStorage.Insert(true); + + EDocument.Init(); + EDocument.Service := EDocumentService.Code; + EDocument.Direction := EDocument.Direction::Incoming; + EDocument."Document Type" := EDocType; + EDocument."Unstructured Data Entry No." := EDocDataStorage."Entry No."; + EDocument."File Name" := FileName; + EDocument.Insert(true); + EDocLogHelper.InsertLog(EDocument, EDocumentService, Enum::"E-Document Service Status"::Created); + CreatePurchaseDocument(PurchaseHeader, DocumentType, EDocument); + end; + + local procedure AssertEDocumentIncomingAttachmentExists(PostingDate: Date; DocNo: Code[20]) + var + IncomingDocument: Record "Incoming Document"; + IncomingDocumentAttachment: Record "Incoming Document Attachment"; + NoIncomingDocumentTxt: Label 'No incoming document found for %1 on %2', Comment = '%1 = Document No., %2 = Posting Date'; + begin + Assert.IsTrue( + IncomingDocument.FindByDocumentNoAndPostingDate(IncomingDocument, DocNo, Format(PostingDate)), + StrSubstNo(NoIncomingDocumentTxt, DocNo, PostingDate)); + + IncomingDocumentAttachment.SetRange("Incoming Document Entry No.", IncomingDocument."Entry No."); + IncomingDocumentAttachment.SetRange("Is E-Document", true); + + Assert.RecordIsNotEmpty(IncomingDocumentAttachment); + end; + + local procedure CreateSalesDocumentWithEDocRequirements(var SalesHeader: Record "Sales Header"; DocumentType: Enum "Sales Document Type"): Code[20] + var + Customer: Record Customer; + PostCode: Record "Post Code"; + begin + LibrarySales.CreateCustomer(Customer); + LibrarySales.CreateCustomerAddress(Customer); + Customer.Validate(GLN, '1234567890128'); + Customer.Modify(false); + + case DocumentType of + "Sales Document Type"::Invoice: + LibrarySales.CreateSalesInvoiceForCustomerNo(SalesHeader, Customer."No."); + "Sales Document Type"::"Credit Memo": + LibrarySales.CreateSalesCreditMemoForCustomerNo(SalesHeader, Customer."No."); + end; + + SalesHeader.Validate("Bill-to Address", LibraryRandom.RandText(MaxStrLen(SalesHeader."Bill-to Address"))); + LibraryERM.CreatePostCode(PostCode); + SalesHeader.Validate("Bill-to Post Code", PostCode.Code); + SalesHeader.Validate("Your Reference", LibraryRandom.RandText(MaxStrLen(SalesHeader."Your Reference"))); + + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure CreateService(var EDocService: Record "E-Document Service") + var + LibraryUtility: Codeunit "Library - Utility"; + begin + EDocService.Init(); + EDocService.Code := LibraryUtility.GenerateRandomCode20(EDocService.FieldNo(Code), Database::"E-Document Service"); + EDocService.Insert(false); + end; + + local procedure CreatePurchaseDocument(var PurchaseHeader: Record "Purchase Header"; DocumentType: Enum "Purchase Document Type"; var EDocument: Record "E-Document") + var + PurchaseLine: Record "Purchase Line"; + begin + LibraryPurchase.CreatePurchHeader(PurchaseHeader, DocumentType, LibraryPurchase.CreateVendorNo()); + LibraryPurchase.CreatePurchaseLine( + PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, + LibraryInventory.CreateItemNo(), LibraryRandom.RandInt(10)); + PurchaseLine.Validate("Direct Unit Cost", LibraryRandom.RandDec(100, 2)); + PurchaseLine.Modify(true); + PurchaseHeader.CalcFields(Amount, "Amount Including VAT"); + PurchaseHeader.Validate("Doc. Amount Incl. VAT", PurchaseHeader."Amount Including VAT"); + PurchaseHeader.Validate("E-Document Link", EDocument.SystemId); + PurchaseHeader.Modify(false); + if EDocument."Entry No" <> 0 then begin + EDocument."Document No." := PurchaseHeader."No."; + EDocument."Posting Date" := PurchaseHeader."Posting Date"; + EDocument."Document Record ID" := PurchaseHeader.RecordId(); + EDocument.Modify(false); + end; + end; +}