Codeunit 1620 PEPPOL Validation, source in 29
Source29
src/Layers/W1/BaseApp/Sales/Peppol/PEPPOLValidation.Codeunit.al493 lines, Copyright (c) Microsoft Corporation. MIT
#if not CLEAN29
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Sales.Peppol;
using Microsoft.Finance.AllocationAccount;
using Microsoft.Finance.Currency;
using Microsoft.Finance.GeneralLedger.Setup;
using Microsoft.Finance.VAT.Setup;
using Microsoft.Foundation.Address;
using Microsoft.Foundation.Company;
using Microsoft.Inventory.Location;
using Microsoft.Sales.Customer;
using Microsoft.Sales.Document;
using Microsoft.Sales.History;
using System.Utilities;
/// <summary>
/// Validates sales documents against PEPPOL requirements before electronic invoice export.
/// </summary>
codeunit 1620 "PEPPOL Validation"
{
TableNo = "Sales Header";
ObsoleteState = Pending;
ObsoleteReason = 'Replaced by PEPPOL App';
ObsoleteTag = '29.0';
trigger OnRun()
var
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeOnRun(Rec, IsHandled);
if IsHandled then
exit;
CheckSalesDocument(Rec);
CheckSalesDocumentLines(Rec);
end;
var
ConfirmManagement: Codeunit "Confirm Management";
OutsideScopeVATBreakdowns: Dictionary of [Text, Text];
#pragma warning disable AA0470
WrongLengthErr: Label 'should be %1 characters long';
EmptyUnitOfMeasureErr: Label 'You must specify a valid International Standard Code for the Unit of Measure for %1.', Comment = 'Parameter 1 - document type (Quote,Order,Invoice,Credit Memo,Blanket Order,Return Order), 2 - document number';
#pragma warning restore AA0470
MissingDescriptionErr: Label 'Description field is empty. \Field must be filled if you want to send the posted document as an electronic document.', Comment = 'Parameter 1 - document type (), 2 - document number';
#pragma warning disable AA0470
MissingCustGLNOrVATRegNoErr: Label 'You must specify either GLN or VAT Registration No. for Customer %1.';
#pragma warning restore AA0470
MissingCompInfGLNOrVATRegNoErr: Label 'You must specify either GLN or VAT Registration No. in %1.', Comment = '%1=Company Information';
NegativeUnitPriceErr: Label 'The unit price is negative in %1. It cannot be negative if you want to send the posted document as an electronic document. \\Do you want to continue?', Comment = '%1 - record ID';
VatMustBeZeroForCategoryErr: Label 'VAT % must be 0 for tax category code %1', Comment = '%1 - Tax Category code';
OnlyOneOCategoryVatPostingSetupErr: Label 'There can be only one tax subtotal present on invoice used with "Not subject to VAT" (O) tax category.';
VATGreaterThanZeroErr: Label 'Line should have greater VAT than 0% for tax category %1', Comment = '%1 - Tax Category code';
/// <summary>
/// Validates a sales document header against PEPPOL requirements including company information, customer data, and required fields.
/// </summary>
/// <param name="SalesHeader">Specifies the sales document header to validate.</param>
procedure CheckSalesDocument(SalesHeader: Record "Sales Header")
var
CompanyInfo: Record "Company Information";
GLSetup: Record "General Ledger Setup";
ResponsibilityCenter: Record "Responsibility Center";
Customer: Record Customer;
IsHandled: Boolean;
begin
CompanyInfo.Get();
GLSetup.Get();
IsHandled := false;
OnBeforeCheckSalesDocument(SalesHeader, CompanyInfo, IsHandled);
if IsHandled then
exit;
CheckCurrencyCode(SalesHeader."Currency Code");
if SalesHeader."Responsibility Center" <> '' then begin
ResponsibilityCenter.Get(SalesHeader."Responsibility Center");
ResponsibilityCenter.TestField(Name);
ResponsibilityCenter.TestField(Address);
ResponsibilityCenter.TestField(City);
ResponsibilityCenter.TestField("Post Code");
ResponsibilityCenter.TestField("Country/Region Code");
end else begin
CompanyInfo.TestField(Name);
CompanyInfo.TestField(Address);
CompanyInfo.TestField(City);
CompanyInfo.TestField("Post Code");
end;
CompanyInfo.TestField("Country/Region Code");
CheckCountryRegionCode(CompanyInfo."Country/Region Code");
IsHandled := false;
OnCheckSalesDocumentOnBeforeCheckCompanyVATRegNo(SalesHeader, CompanyInfo, IsHandled);
if not IsHandled then
if CompanyInfo.GLN + CompanyInfo."VAT Registration No." = '' then
Error(MissingCompInfGLNOrVATRegNoErr, CompanyInfo.TableCaption());
SalesHeader.TestField("Bill-to Name");
SalesHeader.TestField("Bill-to Address");
SalesHeader.TestField("Bill-to City");
SalesHeader.TestField("Bill-to Post Code");
SalesHeader.TestField("Bill-to Country/Region Code");
CheckCountryRegionCode(SalesHeader."Bill-to Country/Region Code");
IsHandled := false;
OnCheckSalesDocumentOnBeforeCheckCustomerVATRegNo(SalesHeader, Customer, IsHandled);
if not IsHandled then
if (SalesHeader."Document Type" in [SalesHeader."Document Type"::Invoice, SalesHeader."Document Type"::Order, SalesHeader."Document Type"::"Credit Memo"]) and
Customer.Get(SalesHeader."Bill-to Customer No.")
then
if (Customer.GLN + Customer."VAT Registration No.") = '' then
Error(MissingCustGLNOrVATRegNoErr, Customer."No.");
if SalesHeader."Document Type" = SalesHeader."Document Type"::"Credit Memo" then
if SalesHeader."Applies-to Doc. Type" = SalesHeader."Applies-to Doc. Type"::Invoice then
SalesHeader.TestField("Applies-to Doc. No.");
if SalesHeader."Document Type" in [SalesHeader."Document Type"::Invoice, SalesHeader."Document Type"::Order] then
SalesHeader.TestField("Shipment Date");
IsHandled := false;
OnCheckSalesDocumentOnBeforeCheckYourReference(SalesHeader, IsHandled);
if not IsHandled then
SalesHeader.TestField("Your Reference");
CheckShipToAddress(SalesHeader);
SalesHeader.TestField("Due Date");
if CompanyInfo.IBAN = '' then
CompanyInfo.TestField("Bank Account No.");
CompanyInfo.TestField("Bank Branch No.");
CompanyInfo.TestField("SWIFT Code");
OnAfterCheckSalesDocument(SalesHeader, CompanyInfo);
end;
/// <summary>
/// Validates all lines of a sales document against PEPPOL requirements.
/// </summary>
/// <param name="SalesHeader">Specifies the sales document header whose lines are to be validated.</param>
procedure CheckSalesDocumentLines(SalesHeader: Record "Sales Header")
var
SalesLine: Record "Sales Line";
begin
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
if SalesLine.FindSet() then
repeat
CheckSalesDocumentLine(SalesLine)
until SalesLine.Next() = 0;
end;
/// <summary>
/// Validates a single sales document line against PEPPOL requirements including unit of measure codes, descriptions, and VAT settings.
/// </summary>
/// <param name="SalesLine">Specifies the sales document line to validate.</param>
procedure CheckSalesDocumentLine(SalesLine: Record "Sales Line")
var
GeneralLedgerSetup: Record "General Ledger Setup";
PEPPOLMgt: Codeunit "PEPPOL Management";
unitCode: Text;
unitCodeListID: Text;
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeCheckSalesDocumentLine(SalesLine, IsHandled);
if IsHandled then
exit;
// Allocation account lines are placeholder lines that are expanded into their underlying G/L
// distribution lines during posting and are never themselves exported in the electronic
// document. Validate the G/L accounts they will be expanded into instead.
if SalesLine.Type = SalesLine.Type::"Allocation Account" then begin
this.CheckAllocationAccountExpandedLines(SalesLine);
exit;
end;
PEPPOLMgt.GetLineUnitCodeInfo(SalesLine, unitCode, unitCodeListID);
if (SalesLine.Type <> SalesLine.Type::" ") and (SalesLine."No." <> '') and (unitCode = '') then
Error(EmptyUnitOfMeasureErr, SalesLine."Unit of Measure Code");
IsHandled := false;
OnCheckSalesDocumentLineOnBeforeCheckEmptyDescription(SalesLine, IsHandled);
if not IsHandled then
if CheckSalesLineTypeAndDescription(SalesLine) then
Error(MissingDescriptionErr);
if (SalesLine.Type <> SalesLine.Type::" ") and (SalesLine."No." <> '') then begin
// Not a description line
if GeneralLedgerSetup.UseVat() then
SalesLine.TestField("VAT Prod. Posting Group");
this.CheckTaxCategory(SalesLine);
if (SalesLine.Type = SalesLine.Type::Item) and (SalesLine."Unit Price" < 0) then
if not ConfirmManagement.GetResponseOrDefault(StrSubstNo(NegativeUnitPriceErr, SalesLine.RecordId), false) then
Error('');
end;
end;
local procedure CheckAllocationAccountExpandedLines(SalesLine: Record "Sales Line")
var
AllocationAccount: Record "Allocation Account";
AllocationLine: Record "Allocation Line";
ExpandedSalesLine: Record "Sales Line";
AllocationAccountMgt: Codeunit "Allocation Account Mgt.";
begin
if not AllocationAccount.Get(SalesLine."No.") then
exit;
AllocationAccountMgt.GenerateAllocationLines(
AllocationAccount, AllocationLine, SalesLine.Amount,
SalesLine.GetSalesHeader()."Posting Date", SalesLine."Dimension Set ID", SalesLine."Currency Code");
if AllocationLine.FindSet() then
repeat
if AllocationLine."Destination Account Type" = AllocationLine."Destination Account Type"::"G/L Account" then begin
ExpandedSalesLine := SalesLine;
ExpandedSalesLine.Type := ExpandedSalesLine.Type::"G/L Account";
ExpandedSalesLine."No." := AllocationLine."Destination Account Number";
this.CheckSalesDocumentLine(ExpandedSalesLine);
end;
until AllocationLine.Next() = 0;
end;
/// <summary>
/// Validates a posted sales invoice and all its lines against PEPPOL requirements.
/// </summary>
/// <param name="SalesInvoiceHeader">Specifies the posted sales invoice header to validate.</param>
procedure CheckSalesInvoice(SalesInvoiceHeader: Record "Sales Invoice Header")
var
SalesInvoiceLine: Record "Sales Invoice Line";
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
begin
SalesHeader.TransferFields(SalesInvoiceHeader);
SalesHeader."Document Type" := SalesHeader."Document Type"::Invoice;
CheckSalesDocument(SalesHeader);
SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No.");
if SalesInvoiceLine.FindSet() then
repeat
SalesLine.TransferFields(SalesInvoiceLine);
SalesLine."Document Type" := SalesLine."Document Type"::Invoice;
CheckSalesDocumentLine(SalesLine);
until SalesInvoiceLine.Next() = 0;
end;
/// <summary>
/// Validates a posted sales credit memo and all its lines against PEPPOL requirements.
/// </summary>
/// <param name="SalesCrMemoHeader">Specifies the posted sales credit memo header to validate.</param>
procedure CheckSalesCreditMemo(SalesCrMemoHeader: Record "Sales Cr.Memo Header")
var
SalesCrMemoLine: Record "Sales Cr.Memo Line";
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
begin
SalesHeader.TransferFields(SalesCrMemoHeader);
SalesHeader."Document Type" := SalesHeader."Document Type"::"Credit Memo";
CheckSalesDocument(SalesHeader);
SalesCrMemoLine.SetRange("Document No.", SalesCrMemoHeader."No.");
if SalesCrMemoLine.FindSet() then
repeat
SalesLine.TransferFields(SalesCrMemoLine);
SalesLine."Document Type" := SalesLine."Document Type"::"Credit Memo";
CheckSalesDocumentLine(SalesLine);
until SalesCrMemoLine.Next() = 0;
end;
local procedure CheckCurrencyCode(CurrencyCode: Code[10])
var
GLSetup: Record "General Ledger Setup";
Currency: Record Currency;
MaxCurrencyCodeLength: Integer;
begin
MaxCurrencyCodeLength := 3;
if CurrencyCode = '' then begin
GLSetup.Get();
GLSetup.TestField("LCY Code");
CurrencyCode := GLSetup."LCY Code";
end;
if not Currency.Get(CurrencyCode) then begin
if StrLen(CurrencyCode) <> MaxCurrencyCodeLength then
GLSetup.FieldError("LCY Code", StrSubstNo(WrongLengthErr, MaxCurrencyCodeLength));
exit; // Valid
end;
if StrLen(Currency.Code) <> MaxCurrencyCodeLength then
Currency.FieldError(Code, StrSubstNo(WrongLengthErr, MaxCurrencyCodeLength));
end;
local procedure CheckCountryRegionCode(CountryRegionCode: Code[10])
var
CountryRegion: Record "Country/Region";
CompanyInfo: Record "Company Information";
MaxCountryCodeLength: Integer;
begin
MaxCountryCodeLength := 2;
if CountryRegionCode = '' then begin
CompanyInfo.Get();
CompanyInfo.TestField("Country/Region Code");
CountryRegionCode := CompanyInfo."Country/Region Code";
end;
CountryRegion.Get(CountryRegionCode);
CountryRegion.TestField("ISO Code");
if StrLen(CountryRegion."ISO Code") <> MaxCountryCodeLength then
CountryRegion.FieldError("ISO Code", StrSubstNo(WrongLengthErr, MaxCountryCodeLength));
end;
local procedure CheckShipToAddress(SalesHeader: Record "Sales Header")
var
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeCheckShipToAddress(SalesHeader, IsHandled);
if IsHandled then
exit;
SalesHeader.TestField("Ship-to Address");
SalesHeader.TestField("Ship-to City");
SalesHeader.TestField("Ship-to Post Code");
SalesHeader.TestField("Ship-to Country/Region Code");
CheckCountryRegionCode(SalesHeader."Ship-to Country/Region Code");
end;
local procedure CheckTaxCategory(SalesLine: Record "Sales Line")
var
VATPostingSetup: Record "VAT Posting Setup";
PEPPOLManagement: Codeunit "PEPPOL Management";
begin
VATPostingSetup.Get(SalesLine."VAT Bus. Posting Group", SalesLine."VAT Prod. Posting Group");
VATPostingSetup.TestField("Tax Category");
case true of
PEPPOLManagement.IsStandardVATCategory(VATPostingSetup."Tax Category"):
this.EnsurePositiveRate(SalesLine."VAT %", VATPostingSetup."Tax Category");
PEPPOLManagement.IsOutsideScopeVATCategory(VATPostingSetup."Tax Category"):
begin
this.EnsureZeroRate(SalesLine."VAT %", VATPostingSetup."Tax Category");
this.EnsureSingleOutsideScopeVATBreakdown(SalesLine);
end;
PEPPOLManagement.IsZeroVatCategory(VATPostingSetup."Tax Category"):
this.EnsureZeroRate(SalesLine."VAT %", VATPostingSetup."Tax Category");
end
end;
local procedure EnsureZeroRate(VatPercent: Decimal; TaxCategoryCode: Code[10])
begin
if VatPercent > 0 then
Error(VatMustBeZeroForCategoryErr, TaxCategoryCode);
end;
local procedure EnsurePositiveRate(VatPercent: Decimal; TaxCategoryCode: Code[10])
begin
if VatPercent = 0 then
Error(VATGreaterThanZeroErr, TaxCategoryCode);
end;
local procedure EnsureSingleOutsideScopeVATBreakdown(SalesLine: Record "Sales Line")
var
BreakdownKey: Text;
begin
// Check if separate VAT amount line won't be created to ensure that only one VAT breakdown line is created in PEPPOL document
BreakdownKey := Format(SalesLine."VAT Calculation Type") + '|' + SalesLine."Tax Group Code";
if OutsideScopeVATBreakdowns.Count() > 0 then begin
if not OutsideScopeVATBreakdowns.ContainsKey(BreakdownKey) then
Error(OnlyOneOCategoryVatPostingSetupErr);
end else
OutsideScopeVATBreakdowns.Add(BreakdownKey, Format(SalesLine."VAT %"));
end;
/// <summary>
/// Checks if a sales line has a type set but is missing a description.
/// </summary>
/// <param name="SalesLine">Specifies the sales line to check.</param>
/// <returns>Returns true if the line has a type but no description, false otherwise.</returns>
procedure CheckSalesLineTypeAndDescription(SalesLine: Record "Sales Line"): Boolean
begin
if (SalesLine.Type <> SalesLine.Type::" ") and (SalesLine.Description = '') then
exit(true);
end;
/// <summary>
/// Raised after validating a sales document header against PEPPOL requirements.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record that was validated.</param>
/// <param name="CompanyInfo">Specifies the company information record used for validation.</param>
[IntegrationEvent(false, false)]
local procedure OnAfterCheckSalesDocument(SalesHeader: Record "Sales Header"; CompanyInfo: Record "Company Information")
begin
end;
/// <summary>
/// Raised before validating the ship-to address of a sales document.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record to validate.</param>
/// <param name="IsHandled">Set to true to skip the default ship-to address validation.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeCheckShipToAddress(SalesHeader: Record "Sales Header"; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before running the PEPPOL validation codeunit.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record to validate.</param>
/// <param name="IsHandled">Set to true to skip the default validation logic.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeOnRun(SalesHeader: Record "Sales Header"; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before checking if a sales line has an empty description.
/// </summary>
/// <param name="SalesLine">Specifies the sales line record being validated.</param>
/// <param name="IsHandled">Set to true to skip the empty description check.</param>
[IntegrationEvent(false, false)]
local procedure OnCheckSalesDocumentLineOnBeforeCheckEmptyDescription(SalesLine: Record "Sales Line"; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before validating a sales document header against PEPPOL requirements.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record to validate.</param>
/// <param name="CompanyInformation">Specifies the company information record used for validation.</param>
/// <param name="IsHandled">Set to true to skip the default sales document validation.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeCheckSalesDocument(var SalesHeader: Record "Sales Header"; var CompanyInformation: Record "Company Information"; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before validating a sales document line against PEPPOL requirements.
/// </summary>
/// <param name="SalesLine">Specifies the sales line record to validate.</param>
/// <param name="IsHandled">Set to true to skip the default sales line validation.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeCheckSalesDocumentLine(var SalesLine: Record "Sales Line"; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before checking the company VAT registration number during sales document validation.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record being validated.</param>
/// <param name="CompanyInformation">Specifies the company information record.</param>
/// <param name="IsHandled">Set to true to skip the company VAT registration number check.</param>
[IntegrationEvent(false, false)]
local procedure OnCheckSalesDocumentOnBeforeCheckCompanyVATRegNo(SalesHeader: Record "Sales Header"; CompanyInformation: Record "Company Information"; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before checking the customer VAT registration number during sales document validation.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record being validated.</param>
/// <param name="Customer">Specifies the customer record.</param>
/// <param name="IsHandled">Set to true to skip the customer VAT registration number check.</param>
[IntegrationEvent(false, false)]
local procedure OnCheckSalesDocumentOnBeforeCheckCustomerVATRegNo(SalesHeader: Record "Sales Header"; Customer: Record Customer; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before checking the Your Reference field during sales document validation.
/// </summary>
/// <param name="SalesHeader">Specifies the sales header record being validated.</param>
/// <param name="IsHandled">Set to true to skip the Your Reference field check.</param>
[IntegrationEvent(false, false)]
local procedure OnCheckSalesDocumentOnBeforeCheckYourReference(SalesHeader: Record "Sales Header"; var IsHandled: Boolean)
begin
end;
}
#endif