Codeunit 1302 Customer Mgt., source in 29
Source29
src/Layers/W1/BaseApp/Sales/Customer/CustomerMgt.Codeunit.al770 lines, Copyright (c) Microsoft Corporation. MIT
// ------------------------------------------------------------------------------------------------
// 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.Customer;
using Microsoft.CRM.Interaction;
using Microsoft.Finance.GeneralLedger.Journal;
using Microsoft.Foundation.Period;
using Microsoft.Inventory.Costing;
using Microsoft.Sales.Document;
using Microsoft.Sales.History;
using Microsoft.Sales.Receivables;
/// <summary>
/// Provides customer management functions including statistics calculations and sales analysis.
/// </summary>
codeunit 1302 "Customer Mgt."
{
trigger OnRun()
begin
end;
var
FiscalYearTotals: Boolean;
#if not CLEAN27
/// <summary>
/// Calculates the average number of days it takes for a customer to pay their invoices.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate the average days to pay for.</param>
/// <returns>The average number of days to pay as a decimal value.</returns>
[Obsolete('Use procedure CalculatePaymentStats(CustomerNo: Code[20]; var Stats: Dictionary of [Text, Text]) instead.', '27.0')]
procedure AvgDaysToPay(CustNo: Code[20]) AverageDaysToPay: Decimal
var
Stats: Dictionary of [Text, Text];
begin
CalculatePaymentStats(CustNo, Stats);
exit(GetAvgDaysToPay(stats));
end;
local procedure GetAvgDaysToPay(var Stats: Dictionary of [Text, Text]) AverageDaysToPay: Decimal
var
CustomerCardCalculations: Codeunit "Customer Card Calculations";
AverageDaysToPayText: Text;
begin
if Stats.Get(CustomerCardCalculations.GetAvgDaysToPayLabel(), AverageDaysToPayText) then
if Evaluate(AverageDaysToPay, AverageDaysToPayText, 0) then
exit(AverageDaysToPay);
end;
#endif
/// <summary>
/// Calculates payment statistics for a customer including average days to pay, late payment counts, and overdue invoices.
/// </summary>
/// <param name="CustomerNo">Specifies the customer number to calculate payment statistics for.</param>
/// <param name="Stats">Returns a dictionary containing the calculated payment statistics.</param>
procedure CalculatePaymentStats(CustomerNo: Code[20]; var Stats: Dictionary of [Text, Text])
var
CustLedgerEntry: Record "Cust. Ledger Entry";
CustLedgerEntry2: Record "Cust. Ledger Entry";
CustomerCardCalculations: Codeunit "Customer Card Calculations";
TotalDaysToPay: Decimal;
TotalNoOfInv: Integer;
PaidLateCount: Decimal;
begin
TotalDaysToPay := 0;
TotalNoOfInv := 0;
PaidLateCount := 0;
CustLedgerEntry.SetCurrentKey("Customer No.", "Posting Date");
SetFilterForPostedDocs(CustLedgerEntry, CustomerNo, CustLedgerEntry."Document Type"::Invoice);
CustLedgerEntry.SetRange(Open, false);
if CustLedgerEntry.FindSet() then
repeat
case true of
CustLedgerEntry."Closed at Date" > CustLedgerEntry."Posting Date":
begin
TotalDaysToPay += CustLedgerEntry."Closed at Date" - CustLedgerEntry."Posting Date";
TotalNoOfInv += 1;
if CustLedgerEntry."Closed at Date" > CustLedgerEntry."Due Date" then
PaidLateCount += 1;
end;
CustLedgerEntry."Closed by Entry No." <> 0:
if CustLedgerEntry2.Get(CustLedgerEntry."Closed by Entry No.") then begin
TotalDaysToPay += CustLedgerEntry2."Posting Date" - CustLedgerEntry."Posting Date";
TotalNoOfInv += 1;
if CustLedgerEntry2."Posting Date" > CustLedgerEntry."Due Date" then
PaidLateCount += 1;
end;
else begin
CustLedgerEntry2.SetCurrentKey("Closed by Entry No.");
CustLedgerEntry2.SetRange("Closed by Entry No.", CustLedgerEntry."Entry No.");
if CustLedgerEntry2.FindFirst() then begin
TotalDaysToPay += CustLedgerEntry2."Posting Date" - CustLedgerEntry."Posting Date";
TotalNoOfInv += 1;
if CustLedgerEntry2."Posting Date" > CustLedgerEntry."Due Date" then
PaidLateCount += 1;
end
end;
end;
until CustLedgerEntry.Next() = 0;
if TotalNoOfInv <> 0 then begin
Stats.Add(CustomerCardCalculations.GetAvgDaysToPayLabel(), (TotalDaysToPay / TotalNoOfInv).ToText());
Stats.Add(CustomerCardCalculations.GetPaidLateCountLabel(), PaidLateCount.ToText());
Stats.Add(CustomerCardCalculations.GetPaidOnTimeCountLabel(), (TotalNoOfInv - PaidLateCount).ToText());
Stats.Add(CustomerCardCalculations.GetPercentPaidLateLabel(), Round((PaidLateCount / TotalNoOfInv) * 100).ToText());
end;
CustLedgerEntry.SetRange(Open, true);
CustLedgerEntry.SetRange("Due Date", 0D, WorkDate() - 1);
Stats.Add(CustomerCardCalculations.GetOverdueCountLabel(), CustLedgerEntry.Count().ToText());
CalcLastPaymentInfo(CustomerNo, Stats);
CustLedgerEntry.Reset();
CustLedgerEntry.SetCurrentKey("Posting Date");
CustLedgerEntry.SetRange("Customer No.", CustomerNo);
CustLedgerEntry.SetFilter("Sales (LCY)", '>%1', 0);
if CustLedgerEntry.FindLast() then
Stats.Add(CustomerCardCalculations.GetDaysSinceLastSaleLabel(), (WorkDate() - CustLedgerEntry."Posting Date").ToText());
end;
/// <summary>
/// Retrieves the last payment information for a customer including date, amount, and whether it was paid on time.
/// </summary>
/// <param name="CustomerNo">Specifies the customer number to retrieve payment information for.</param>
/// <param name="Stats">Returns a dictionary containing the last payment date, amount, and on-time status.</param>
procedure CalcLastPaymentInfo(CustomerNo: Code[20]; var Stats: Dictionary of [Text, Text])
var
CustLedgerEntry: Record "Cust. Ledger Entry";
CustLedgerEntry2: Record "Cust. Ledger Entry";
CustomerCardCalculations: Codeunit "Customer Card Calculations";
begin
CustLedgerEntry.SetCurrentKey("Document Type", "Customer No.", "Posting Date", "Currency Code");
CustLedgerEntry.SetRange("Customer No.", CustomerNo);
CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Payment);
CustLedgerEntry.SetRange(Reversed, false);
if CustLedgerEntry.FindLast() then begin
CustLedgerEntry.CalcFields("Amount (LCY)");
Stats.Add(CustomerCardCalculations.GetLastPaymentDateLabel(), CustLedgerEntry."Posting Date".ToText());
Stats.Add(CustomerCardCalculations.GetLastPaymentAmountLabel(), (-CustLedgerEntry."Amount (LCY)").ToText());
CustLedgerEntry2.SetRange("Closed by Entry No.", CustLedgerEntry."Entry No.");
if CustLedgerEntry2.FindLast() then
Stats.Add(CustomerCardCalculations.GetLastPaymentOnTimeLabel(), (CustLedgerEntry2."Closed at Date" < CustLedgerEntry2."Due Date").ToText())
end;
end;
/// <summary>
/// Calculates the number of distinct items that have been sold to a customer.
/// </summary>
/// <param name="CustomerNo">Specifies the customer number to count distinct items for.</param>
/// <param name="Stats">Returns a dictionary containing the count of distinct items sold.</param>
procedure CalcNumberOfDistinctItemsSold(CustomerNo: Code[20]; var Stats: Dictionary of [Text, Text])
var
CustomerCardCalculations: Codeunit "Customer Card Calculations";
DistinctItemsSoldQuery: Query "Distinct Items Sold";
DistinctItemCount: Integer;
begin
DistinctItemCount := 0;
DistinctItemsSoldQuery.SetRange(CustomerNoFilter, CustomerNo);
if DistinctItemsSoldQuery.Open() then
while DistinctItemsSoldQuery.Read() do
DistinctItemCount += 1;
Stats.Add(CustomerCardCalculations.GetDistinctItemsSoldLabel(), DistinctItemCount.ToText())
end;
/// <summary>
/// Calculates interaction statistics for a customer including total count, last interaction date, and most frequent type.
/// </summary>
/// <param name="CustomerNo">Specifies the customer number to calculate interaction statistics for.</param>
/// <param name="Stats">Returns a dictionary containing the calculated interaction statistics.</param>
procedure CalculateInteractionStats(CustomerNo: Code[20]; var Stats: Dictionary of [Text, Text])
var
InteractionLogEntry: Record "Interaction Log Entry";
InteractionGroup: Record "Interaction Group";
CustomerCardCalculations: Codeunit "Customer Card Calculations";
CustomerInteractionStats: Query "Customer Interaction Stats.";
EntryCount, MaxEntryNo, MaxGroupCount : integer;
MaxGroupDescription: Text[100];
begin
MaxEntryNo := 0;
MaxGroupCount := 0;
CustomerInteractionStats.SetRange(CustomerNo, CustomerNo);
CustomerInteractionStats.SetFilter(InteractionDate, GetCurrentYearFilter());
CustomerInteractionStats.Open();
while CustomerInteractionStats.Read() do begin
EntryCount += CustomerInteractionStats.InteractionCount;
if MaxGroupCount < CustomerInteractionStats.InteractionCount then begin
MaxGroupCount := CustomerInteractionStats.InteractionCount;
MaxGroupDescription := CustomerInteractionStats.Description;
end;
if MaxEntryNo < CustomerInteractionStats.MaxEntryNo then
MaxEntryNo := CustomerInteractionStats.MaxEntryNo;
end;
Stats.Add(CustomerCardCalculations.GetInteractionCountLabel(), EntryCount.ToText());
if InteractionLogEntry.Get(MaxEntryNo) then begin
Stats.Add(CustomerCardCalculations.GetLastInteractionDateLabel(), InteractionLogEntry.Date.ToText());
if InteractionGroup.Get(InteractionLogEntry."Interaction Group Code") then
Stats.Add(CustomerCardCalculations.GetLastInteractionTypeLabel(), InteractionGroup.Description);
Stats.Add(CustomerCardCalculations.GetMostFrequentInteractionTypeLabel(), MaxGroupDescription);
end;
end;
/// <summary>
/// Calculates customer statistics including sales, profit, and cost adjustments for the current fiscal year.
/// </summary>
/// <param name="Customer">Specifies the customer record to calculate statistics for.</param>
/// <param name="AdjmtCostLCY">Returns the adjusted cost in local currency.</param>
/// <param name="AdjCustProfit">Returns the adjusted customer profit.</param>
/// <param name="AdjProfitPct">Returns the adjusted profit percentage.</param>
/// <param name="CustInvDiscAmountLCY">Returns the customer invoice discount amount in local currency.</param>
/// <param name="CustPaymentsLCY">Returns the customer payments in local currency.</param>
/// <param name="CustSalesLCY">Returns the customer sales in local currency.</param>
/// <param name="CustProfit">Returns the customer profit.</param>
procedure CalculateStatistic(Customer: Record Customer; var AdjmtCostLCY: Decimal; var AdjCustProfit: Decimal; var AdjProfitPct: Decimal; var CustInvDiscAmountLCY: Decimal; var CustPaymentsLCY: Decimal; var CustSalesLCY: Decimal; var CustProfit: Decimal)
begin
Customer.SetFilter("Date Filter", GetCurrentYearFilter());
Customer.CalcFields("Sales (LCY)", "Profit (LCY)", "Inv. Discounts (LCY)", "Payments (LCY)");
CalculateStatisticsWithCurrentCustomerValues(Customer, AdjmtCostLCY, AdjCustProfit, AdjProfitPct, CustInvDiscAmountLCY, CustPaymentsLCY, CustSalesLCY, CustProfit)
end;
/// <summary>
/// Calculates customer statistics using current customer field values without applying date filters.
/// </summary>
/// <param name="Customer">Specifies the customer record with pre-calculated field values.</param>
/// <param name="AdjmtCostLCY">Returns the adjusted cost in local currency.</param>
/// <param name="AdjCustProfit">Returns the adjusted customer profit.</param>
/// <param name="AdjProfitPct">Returns the adjusted profit percentage.</param>
/// <param name="CustInvDiscAmountLCY">Returns the customer invoice discount amount in local currency.</param>
/// <param name="CustPaymentsLCY">Returns the customer payments in local currency.</param>
/// <param name="CustSalesLCY">Returns the customer sales in local currency.</param>
/// <param name="CustProfit">Returns the customer profit.</param>
procedure CalculateStatisticsWithCurrentCustomerValues(var Customer: Record Customer; var AdjmtCostLCY: Decimal; var AdjCustProfit: Decimal; var AdjProfitPct: Decimal; var CustInvDiscAmountLCY: Decimal; var CustPaymentsLCY: Decimal; var CustSalesLCY: Decimal; var CustProfit: Decimal)
var
CostCalcuMgt: Codeunit "Cost Calculation Management";
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeCalculateStatisticsWithCurrentCustomerValues(Customer, AdjmtCostLCY, AdjCustProfit, AdjProfitPct, CustInvDiscAmountLCY, CustPaymentsLCY, CustSalesLCY, CustProfit, IsHandled);
if IsHandled then
exit;
// Costs (LCY):
CustSalesLCY := Customer."Sales (LCY)";
CustProfit := Customer."Profit (LCY)" + CostCalcuMgt.NonInvtblCostAmt(Customer);
AdjmtCostLCY := CustSalesLCY - CustProfit + CostCalcuMgt.CalcCustActualCostLCY(Customer);
AdjCustProfit := CustProfit + AdjmtCostLCY;
// Profit %
if Customer."Sales (LCY)" <> 0 then
AdjProfitPct := Round(100 * AdjCustProfit / Customer."Sales (LCY)", 0.1)
else
AdjProfitPct := 0;
CustInvDiscAmountLCY := Customer."Inv. Discounts (LCY)";
CustPaymentsLCY := Customer."Payments (LCY)";
end;
/// <summary>
/// Calculates the total sales amount on posted invoices for a customer within the current year.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate posted invoice amounts for.</param>
/// <param name="RecCount">Returns the count of posted invoices.</param>
/// <returns>The total sales amount in local currency on posted invoices.</returns>
procedure CalcAmountsOnPostedInvoices(CustNo: Code[20]; var RecCount: Integer): Decimal
var
CustLedgEntry: Record "Cust. Ledger Entry";
begin
exit(CalcAmountsOnPostedDocs(CustNo, RecCount, CustLedgEntry."Document Type"::Invoice));
end;
/// <summary>
/// Calculates the total sales amount on posted credit memos for a customer within the current year.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate posted credit memo amounts for.</param>
/// <param name="RecCount">Returns the count of posted credit memos.</param>
/// <returns>The total sales amount in local currency on posted credit memos.</returns>
procedure CalcAmountsOnPostedCrMemos(CustNo: Code[20]; var RecCount: Integer): Decimal
var
CustLedgEntry: Record "Cust. Ledger Entry";
begin
exit(CalcAmountsOnPostedDocs(CustNo, RecCount, CustLedgEntry."Document Type"::"Credit Memo"));
end;
/// <summary>
/// Calculates the total outstanding amount on sales orders for a customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate sales order amounts for.</param>
/// <param name="RecCount">Returns the count of sales orders.</param>
/// <returns>The total outstanding amount in local currency on sales orders.</returns>
procedure CalcAmountsOnOrders(CustNo: Code[20]; var RecCount: Integer): Decimal
var
SalesHeader: Record "Sales Header";
begin
exit(CalculateAmountsOnUnpostedDocs(CustNo, RecCount, SalesHeader."Document Type"::Order));
end;
/// <summary>
/// Calculates the total outstanding amount on sales quotes for a customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate sales quote amounts for.</param>
/// <param name="RecCount">Returns the count of sales quotes.</param>
/// <returns>The total outstanding amount in local currency on sales quotes.</returns>
procedure CalcAmountsOnQuotes(CustNo: Code[20]; var RecCount: Integer): Decimal
var
SalesHeader: Record "Sales Header";
begin
exit(CalculateAmountsOnUnpostedDocs(CustNo, RecCount, SalesHeader."Document Type"::Quote));
end;
local procedure CalcAmountsOnPostedDocs(CustNo: Code[20]; var RecCount: Integer; DocType: Enum "Gen. Journal Document Type"): Decimal
var
[SecurityFiltering(SecurityFilter::Filtered)]
CustLedgerEntry: Record "Cust. Ledger Entry";
begin
SetFilterForPostedDocs(CustLedgerEntry, CustNo, DocType);
RecCount := CustLedgerEntry.Count();
CustLedgerEntry.CalcSums("Sales (LCY)");
exit(CustLedgerEntry."Sales (LCY)");
end;
/// <summary>
/// Calculates the total outstanding amount including VAT on unposted invoices and credit memos for a customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate amounts for.</param>
/// <returns>The total outstanding amount including VAT on unposted documents.</returns>
procedure CalculateAmountsWithVATOnUnpostedDocuments(CustNo: Code[20]): Decimal
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesLine: Record "Sales Line";
Result: Decimal;
begin
if CustNo = '' then
exit;
SetFilterForUnpostedLines(SalesLine, CustNo, SalesLine."Document Type"::Invoice, true);
SalesLine.CalcSums("Outstanding Amount (LCY)");
Result := SalesLine."Outstanding Amount (LCY)";
SetFilterForUnpostedLines(SalesLine, CustNo, SalesLine."Document Type"::"Credit Memo", true);
SalesLine.CalcSums("Outstanding Amount (LCY)");
Result -= SalesLine."Outstanding Amount (LCY)";
exit(Result);
end;
/// <summary>
/// Calculates the total outstanding amount on unposted sales invoices for a customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate unposted invoice amounts for.</param>
/// <param name="RecCount">Returns the count of unposted invoices.</param>
/// <returns>The total outstanding amount in local currency on unposted invoices.</returns>
procedure CalculateAmountsOnUnpostedInvoices(CustNo: Code[20]; var RecCount: Integer): Decimal
var
SalesLine: Record "Sales Line";
begin
exit(CalculateAmountsOnUnpostedDocs(CustNo, RecCount, SalesLine."Document Type"::Invoice));
end;
/// <summary>
/// Calculates the total outstanding amount on unposted sales credit memos for a customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate unposted credit memo amounts for.</param>
/// <param name="RecCount">Returns the count of unposted credit memos.</param>
/// <returns>The total outstanding amount in local currency on unposted credit memos.</returns>
procedure CalculateAmountsOnUnpostedCrMemos(CustNo: Code[20]; var RecCount: Integer): Decimal
var
SalesLine: Record "Sales Line";
begin
exit(CalculateAmountsOnUnpostedDocs(CustNo, RecCount, SalesLine."Document Type"::"Credit Memo"));
end;
local procedure CalculateAmountsOnUnpostedDocs(CustNo: Code[20]; var RecCount: Integer; DocumentType: Enum "Sales Document Type") Result: Decimal
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesHeader: Record "Sales Header";
SalesOutstdAmountOnVAT: Query "Sales Outstd. Amount On VAT";
Factor: Integer;
begin
RecCount := 0;
Result := 0;
if CustNo = '' then
exit;
SalesHeader.SetRange("Document Type", DocumentType);
SalesHeader.SetRange("Bill-to Customer No.", CustNo);
RecCount := SalesHeader.Count();
case DocumentType of
"Sales Document Type"::Invoice,
"Sales Document Type"::Order,
"Sales Document Type"::Quote:
Factor := 1;
"Sales Document Type"::"Credit Memo":
Factor := -1;
end;
SalesOutstdAmountOnVAT.SetRange(Document_Type, DocumentType);
SalesOutstdAmountOnVAT.SetRange(Bill_to_Customer_No_, CustNo);
SalesOutstdAmountOnVAT.Open();
while SalesOutstdAmountOnVAT.Read() do
Result += Factor * SalesOutstdAmountOnVAT.Sum_Outstanding_Amount__LCY_ * 100 / (100 + SalesOutstdAmountOnVAT.VAT__);
SalesOutstdAmountOnVAT.Close();
exit(Round(Result));
end;
/// <summary>
/// Opens the Posted Sales Invoices page filtered for the specified customer within the current year.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter posted invoices for.</param>
procedure DrillDownOnPostedInvoices(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesInvoiceHeader: Record "Sales Invoice Header";
begin
SalesInvoiceHeader.SetRange("Bill-to Customer No.", CustNo);
SalesInvoiceHeader.SetFilter("Posting Date", GetCurrentYearFilter());
PAGE.Run(PAGE::"Posted Sales Invoices", SalesInvoiceHeader);
end;
/// <summary>
/// Opens the Posted Sales Credit Memos page filtered for the specified customer within the current year.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter posted credit memos for.</param>
procedure DrillDownOnPostedCrMemo(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesCrMemoHeader: Record "Sales Cr.Memo Header";
begin
SalesCrMemoHeader.SetRange("Bill-to Customer No.", CustNo);
SalesCrMemoHeader.SetFilter("Posting Date", GetCurrentYearFilter());
PAGE.Run(PAGE::"Posted Sales Credit Memos", SalesCrMemoHeader);
end;
/// <summary>
/// Opens the Sales Order List page filtered for the specified customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter sales orders for.</param>
procedure DrillDownOnOrders(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesHeader: Record "Sales Header";
begin
SalesHeader.SetRange("Bill-to Customer No.", CustNo);
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
PAGE.Run(PAGE::"Sales Order List", SalesHeader);
end;
/// <summary>
/// Opens the Sales Quotes page filtered for the specified customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter sales quotes for.</param>
procedure DrillDownOnQuotes(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesHeader: Record "Sales Header";
begin
SalesHeader.SetRange("Bill-to Customer No.", CustNo);
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Quote);
PAGE.Run(PAGE::"Sales Quotes", SalesHeader);
end;
/// <summary>
/// Opens the Sales List page showing unposted invoices and credit memos for the specified customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter expected money owed documents for.</param>
procedure DrillDownMoneyOwedExpected(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesHeader: Record "Sales Header";
begin
SetFilterForUnpostedDocs(SalesHeader, CustNo, -1);
PAGE.Run(PAGE::"Sales List", SalesHeader)
end;
/// <summary>
/// Opens the Sales Invoice List page filtered for unposted invoices of the specified customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter unposted invoices for.</param>
procedure DrillDownOnUnpostedInvoices(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesHeader: Record "Sales Header";
begin
SetFilterForUnpostedDocs(SalesHeader, CustNo, SalesHeader."Document Type"::Invoice.AsInteger());
PAGE.Run(PAGE::"Sales Invoice List", SalesHeader)
end;
/// <summary>
/// Opens the Sales Credit Memos page filtered for unposted credit memos of the specified customer.
/// </summary>
/// <param name="CustNo">Specifies the customer number to filter unposted credit memos for.</param>
procedure DrillDownOnUnpostedCrMemos(CustNo: Code[20])
var
[SecurityFiltering(SecurityFilter::Filtered)]
SalesHeader: Record "Sales Header";
begin
SetFilterForUnpostedDocs(SalesHeader, CustNo, SalesHeader."Document Type"::"Credit Memo".AsInteger());
PAGE.Run(PAGE::"Sales Credit Memos", SalesHeader)
end;
local procedure SetFilterForUnpostedDocs(var SalesHeader: Record "Sales Header"; CustNo: Code[20]; DocumentType: Integer)
begin
SalesHeader.SetRange("Bill-to Customer No.", CustNo);
SalesHeader.SetFilter("Posting Date", GetCurrentYearFilter());
if DocumentType = -1 then
SalesHeader.SetFilter("Document Type", '%1|%2', SalesHeader."Document Type"::Invoice, SalesHeader."Document Type"::"Credit Memo")
else
SalesHeader.SetRange("Document Type", DocumentType);
end;
local procedure SetFilterForUnpostedLines(var SalesLine: Record "Sales Line"; CustNo: Code[20]; DocumentType: Enum "Sales Document Type"; Posted: Boolean)
begin
SalesLine.SetRange("Bill-to Customer No.", CustNo);
if Posted then
SalesLine.SetFilter("Posting Date", GetCurrentYearFilter());
SalesLine.SetRange("Document Type", DocumentType);
OnAfterSetFilterForUnpostedLines(SalesLine);
end;
local procedure SetFilterForPostedDocs(var CustLedgEntry: Record "Cust. Ledger Entry"; CustNo: Code[20]; DocumentType: Enum "Gen. Journal Document Type")
begin
CustLedgEntry.SetRange("Customer No.", CustNo);
CustLedgEntry.SetFilter("Posting Date", GetCurrentYearFilter());
CustLedgEntry.SetRange("Document Type", DocumentType);
OnAfterSetFilterForPostedDocs(CustLedgEntry);
end;
/// <summary>
/// Retrieves the date filter for the current fiscal year or accounting period.
/// </summary>
/// <returns>A text filter representing the current year date range.</returns>
procedure GetCurrentYearFilter(): Text[30]
var
DateFilterCalc: Codeunit "DateFilter-Calc";
CustDateFilter: Text[30];
CustDateName: Text[30];
SkipSetFilter: Boolean;
begin
SkipSettingFilter(SkipSetFilter);
if SkipSetFilter then
exit(CustDateFilter);
if FiscalYearTotals then
DateFilterCalc.CreateAccountingPeriodFilter(CustDateFilter, CustDateName, WorkDate(), 0)
else
DateFilterCalc.CreateFiscalYearFilter(CustDateFilter, CustDateName, WorkDate(), 0);
exit(CustDateFilter);
end;
/// <summary>
/// Calculates the total sales for a customer including posted and outstanding invoices and credit memos.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate total sales for.</param>
/// <returns>The total sales amount in local currency.</returns>
procedure GetTotalSales(CustNo: Code[20]) Result: Decimal
var
Totals: Decimal;
AmountOnPostedInvoices: Decimal;
AmountOnPostedCrMemos: Decimal;
AmountOnOutstandingInvoices: Decimal;
AmountOnOutstandingCrMemos: Decimal;
NoPostedInvoices: Integer;
NoPostedCrMemos: Integer;
NoOutstandingInvoices: Integer;
NoOutstandingCrMemos: Integer;
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeGetTotalSales(CustNo, Result, IsHandled);
if IsHandled then
exit(Result);
AmountOnPostedInvoices := CalcAmountsOnPostedInvoices(CustNo, NoPostedInvoices);
AmountOnPostedCrMemos := CalcAmountsOnPostedCrMemos(CustNo, NoPostedCrMemos);
AmountOnOutstandingInvoices := CalculateAmountsOnUnpostedInvoices(CustNo, NoOutstandingInvoices);
AmountOnOutstandingCrMemos := CalculateAmountsOnUnpostedCrMemos(CustNo, NoOutstandingCrMemos);
Totals := AmountOnPostedInvoices + AmountOnPostedCrMemos + AmountOnOutstandingInvoices + AmountOnOutstandingCrMemos;
exit(Totals)
end;
/// <summary>
/// Calculates the year-to-date sales for a customer using fiscal year totals.
/// </summary>
/// <param name="CustNo">Specifies the customer number to calculate year-to-date sales for.</param>
/// <returns>The year-to-date sales amount in local currency.</returns>
procedure GetYTDSales(CustNo: Code[20]): Decimal
var
Totals: Decimal;
begin
FiscalYearTotals := true;
Totals := GetTotalSales(CustNo);
FiscalYearTotals := false;
exit(Totals);
end;
/// <summary>
/// Searches for duplicate external document numbers in sales documents and posted documents.
/// </summary>
/// <param name="OriginalSalesHeader">Specifies the sales header to search for matching external document numbers.</param>
/// <returns>True if a duplicate external document number is found, otherwise false.</returns>
procedure SearchForExternalDocNo(var OriginalSalesHeader: Record "Sales Header"): Boolean
var
SalesHeader: Record "Sales Header";
SalesInvoiceHeader: Record "Sales Invoice Header";
SalesCrMemoHeader: Record "Sales Cr.Memo Header";
IsHandled: Boolean;
ResultFound: Boolean;
begin
OnBeforeSearchForExternalDocNo(OriginalSalesHeader, ResultFound, IsHandled);
if IsHandled then
exit(ResultFound);
SalesHeader.SetLoadFields("Document Type", "No.", "Sell-to Customer No.", "External Document No.");
SalesHeader.SetRange("Document Type", OriginalSalesHeader."Document Type");
SalesHeader.SetFilter("No.", '<>%1', OriginalSalesHeader."No.");
SalesHeader.SetRange("Sell-to Customer No.", OriginalSalesHeader."Sell-to Customer No.");
SalesHeader.SetRange("External Document No.", OriginalSalesHeader."External Document No.");
if not SalesHeader.IsEmpty() then
exit(true);
case OriginalSalesHeader."Document Type" of
OriginalSalesHeader."Document Type"::Invoice, OriginalSalesHeader."Document Type"::Order:
begin
SalesInvoiceHeader.SetLoadFields("Sell-to Customer No.", "External Document No.");
SalesInvoiceHeader.SetRange("Sell-to Customer No.", OriginalSalesHeader."Sell-to Customer No.");
SalesInvoiceHeader.SetRange("External Document No.", OriginalSalesHeader."External Document No.");
exit(not SalesInvoiceHeader.IsEmpty());
end;
OriginalSalesHeader."Document Type"::"Credit Memo":
begin
SalesCrMemoHeader.SetLoadFields("Sell-to Customer No.", "External Document No.");
SalesCrMemoHeader.SetRange("Sell-to Customer No.", OriginalSalesHeader."Sell-to Customer No.");
SalesCrMemoHeader.SetRange("External Document No.", OriginalSalesHeader."External Document No.");
exit(not SalesCrMemoHeader.IsEmpty());
end;
end;
end;
/// <summary>
/// Calculates the ship-to and bill-to options based on the sales header address information.
/// </summary>
/// <param name="ShipToOptions">Returns the calculated ship-to option based on address comparison.</param>
/// <param name="BillToOptions">Returns the calculated bill-to option based on customer and address comparison.</param>
/// <param name="SalesHeader">Specifies the sales header to evaluate for ship-to and bill-to options.</param>
procedure CalculateShipBillToOptions(var ShipToOptions: Enum "Sales Ship-to Options"; var BillToOptions: Enum "Sales Bill-to Options"; var SalesHeader: Record "Sales Header")
var
ShipToNameEqualsSellToName: Boolean;
begin
ShipToNameEqualsSellToName :=
(SalesHeader."Ship-to Name" = SalesHeader."Sell-to Customer Name") and (SalesHeader."Ship-to Name 2" = SalesHeader."Sell-to Customer Name 2");
case true of
(SalesHeader."Ship-to Code" = '') and ShipToNameEqualsSellToName and SalesHeader.ShipToAddressEqualsSellToAddress():
ShipToOptions := ShipToOptions::"Default (Sell-to Address)";
(SalesHeader."Ship-to Code" = '') and (not ShipToNameEqualsSellToName or not SalesHeader.ShipToAddressEqualsSellToAddress()):
ShipToOptions := ShipToOptions::"Custom Address";
SalesHeader."Ship-to Code" <> '':
ShipToOptions := ShipToOptions::"Alternate Shipping Address";
end;
case true of
(SalesHeader."Bill-to Customer No." = SalesHeader."Sell-to Customer No.") and SalesHeader.BillToAddressEqualsSellToAddress():
BillToOptions := BillToOptions::"Default (Customer)";
(SalesHeader."Bill-to Customer No." = SalesHeader."Sell-to Customer No.") and (not SalesHeader.BillToAddressEqualsSellToAddress()):
BillToOptions := BillToOptions::"Custom Address";
SalesHeader."Bill-to Customer No." <> SalesHeader."Sell-to Customer No.":
BillToOptions := BillToOptions::"Another Customer";
end;
OnAfterCalculateShipBillToOptions(ShipToOptions, BillToOptions, SalesHeader);
end;
/// <summary>
/// Raised after setting filters for posted customer ledger entries.
/// </summary>
/// <param name="CustLedgEntry">The customer ledger entry record with filters applied.</param>
[IntegrationEvent(false, false)]
local procedure OnAfterSetFilterForPostedDocs(var CustLedgEntry: Record "Cust. Ledger Entry")
begin
end;
/// <summary>
/// Raised after setting filters for unposted sales lines.
/// </summary>
/// <param name="SalesLine">The sales line record with filters applied.</param>
[IntegrationEvent(false, false)]
local procedure OnAfterSetFilterForUnpostedLines(var SalesLine: Record "Sales Line")
begin
end;
/// <summary>
/// Raised before calculating the total sales for a customer.
/// </summary>
/// <param name="CustNo">The customer number.</param>
/// <param name="Result">Set to the result to override the calculation.</param>
/// <param name="IsHandled">Set to true to skip the default calculation.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetTotalSales(CustNo: Code[20]; var Result: Decimal; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised to allow skipping the date filter setting for current year calculations.
/// </summary>
/// <param name="SkipSetFilter">Set to true to skip the date filter.</param>
[IntegrationEvent(false, false)]
local procedure SkipSettingFilter(var SkipSetFilter: Boolean)
begin
end;
/// <summary>
/// Raised after calculating the ship-to and bill-to options for a sales header.
/// </summary>
/// <param name="ShipToOptions">The calculated ship-to option that can be modified.</param>
/// <param name="BillToOptions">The calculated bill-to option that can be modified.</param>
/// <param name="SalesHeader">The sales header used for the calculation.</param>
[IntegrationEvent(false, false)]
local procedure OnAfterCalculateShipBillToOptions(var ShipToOptions: Enum "Sales Ship-to Options"; var BillToOptions: Enum "Sales Bill-to Options"; SalesHeader: Record "Sales Header")
begin
end;
/// <summary>
/// Raised before searching for duplicate external document numbers.
/// </summary>
/// <param name="OriginalSalesHeader">The sales header to check for duplicates.</param>
/// <param name="ResultFound">Set to true if a duplicate was found.</param>
/// <param name="IsHandled">Set to true to skip the default search.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeSearchForExternalDocNo(var OriginalSalesHeader: Record "Sales Header"; var ResultFound: Boolean; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Raised before calculating customer statistics using current customer field values.
/// </summary>
/// <param name="Customer">The customer record.</param>
/// <param name="AdjmtCostLCY">The adjusted cost in local currency to override.</param>
/// <param name="AdjCustProfit">The adjusted customer profit to override.</param>
/// <param name="AdjProfitPct">The adjusted profit percentage to override.</param>
/// <param name="CustInvDiscAmountLCY">The invoice discount amount to override.</param>
/// <param name="CustPaymentsLCY">The customer payments to override.</param>
/// <param name="CustSalesLCY">The customer sales to override.</param>
/// <param name="CustProfit">The customer profit to override.</param>
/// <param name="IsHandled">Set to true to skip the default calculation.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeCalculateStatisticsWithCurrentCustomerValues(var Customer: Record Customer; var AdjmtCostLCY: Decimal; var AdjCustProfit: Decimal; var AdjProfitPct: Decimal; var CustInvDiscAmountLCY: Decimal; var CustPaymentsLCY: Decimal; var CustSalesLCY: Decimal; var CustProfit: Decimal; var IsHandled: Boolean)
begin
end;
}