I have recently modified the default report 207 Sales Invoice and created a custom report. One request I had, was to display Shipment information on this invoice for each shipment that the current invoice covers. Particularly I needed to show the following values:
The standard report only shows the posted shipment date and the quantity and that only for one shipment (I believe its the last one). It does not show the "Shipment No.".
When making the required changes I had several problems and it was difficult to find any information about the findPostedShipmentDate()
function, so I have decided to post my solution here.
The question would be:
In the function "Sales Invoice Line"::OnAfterGetRecord()
the function findPostedShipmentDate()
is called. This function performs several checks to find the posting date and if necessary calls the function GenerateBufferFromValueEntry()
which fills the table "SalesShipmentBuffer" which temporary stores all the shipment information for the current "Sales Invoice Line" record:
CASE "Sales Invoice Line".Type OF
"Sales Invoice Line".Type::Item:
GenerateBufferFromValueEntry("Sales Invoice Line");
...
Afterwards it performs several checks on the buffer table and deletes the entries again (more on this later).
In the GenerateBufferFromValueEntry
the following happens. The actual shipment information is stored in the ItemLedgerEntry
(32) table, in order to find the correct rows, the ValueEntry
(5802) table is used, which contains the right key (ValueEntry."Item Ledger Entry No."
).
Additionally to the existing range filters I needed to add one line to limit the range to the Item No.
and I had to remove the range filter for the Entry No.
. I also modified the function AddBufferEntry
to additionally take the ItemLedgerEntry."Document No."
which contains the Shipment No.
from the delivery note. Here is the complete code:
TotalQuantity := SalesInvoiceLine2."Quantity (Base)";
ValueEntry.SETCURRENTKEY("Document No.");
ValueEntry.SETRANGE("Document No.",SalesInvoiceLine2."Document No.");
ValueEntry.SETRANGE("Posting Date","Sales Invoice Header"."Posting Date");
ValueEntry.SETRANGE(ValueEntry."Item No.", "Sales Invoice Line"."No."); //Added
ValueEntry.SETRANGE("Item Charge No.",'');
//ValueEntry.SETFILTER("Entry No.",'%1..',FirstValueEntryNo); //Removed
IF ValueEntry.FIND('-') THEN BEGIN
REPEAT
IF ItemLedgerEntry.GET(ValueEntry."Item Ledger Entry No.") THEN BEGIN
IF SalesInvoiceLine2."Qty. per Unit of Measure" <> 0 THEN
Quantity := ValueEntry."Invoiced Quantity" /
SalesInvoiceLine2."Qty. per Unit of Measure"
ELSE
Quantity := ValueEntry."Invoiced Quantity";
AddBufferEntry(
SalesInvoiceLine2,
-Quantity,
ItemLedgerEntry."Posting Date",
ItemLedgerEntry."Document No."); //Added
TotalQuantity := TotalQuantity + ValueEntry."Invoiced Quantity";
END;
FirstValueEntryNo := ValueEntry."Entry No." + 1;
UNTIL (ValueEntry.NEXT = 0) OR (TotalQuantity = 0);
END;
In the FindPostedShipmentDate
function I also deleted the first couple of lines to prevent the function from exiting before the SalesShipmentBuffer
had been filled:
IF "Sales Invoice Line"."Shipment No." <> '' THEN
IF SalesShipmentHeader.GET("Sales Invoice Line"."Shipment No.") THEN
EXIT(SalesShipmentHeader."Posting Date");
IF "Sales Invoice Header"."Order No."='' THEN
EXIT("Sales Invoice Header"."Posting Date");
In the last part of FindPostedShipmentDate
the records of SalesShipmentBuffer
are deleted again, which might seem a bit strange at first (it did to me). The reason is that even after the DELETE
and DELETEALL
functions are called, the record variable retains the values that were stored in the record before calling these functions. So the result is that the SalesShipmentBuffer
tables is cleared, such that its empty for the next run of the findPostedShipmentDate()
function, but the values can still be used in the report. The other confusing part is that actually does a very simple thing which is made to look very complicated. If the buffer contains only one line it deletes the line, otherwise if it contains more than one line it first calculates the sum of all quantity fields and then deletes all lines.
Anyway, I only had to make one change, which was removing the following line since the Document No.
field now contains the Shipment No. and not the Invoice No.
SalesShipmentBuffer.SETRANGE("Document No.","Sales Invoice Line"."Document No.");
Here is the remaining code:
SalesShipmentBuffer.SETRANGE("Line No." ,"Sales Invoice Line"."Line No.");
SalesShipmentBuffer.SETRANGE("No." ,"Sales Invoice Line"."No.");
IF SalesShipmentBuffer.FIND('-') THEN BEGIN
SalesShipmentBuffer2 := SalesShipmentBuffer;
IF SalesShipmentBuffer.NEXT = 0 THEN BEGIN
SalesShipmentBuffer.GET(
SalesShipmentBuffer2."Document No.",
SalesShipmentBuffer2."Line No.",
SalesShipmentBuffer2."Entry No."
);
SalesShipmentBuffer.DELETE;
EXIT(SalesShipmentBuffer2."Posting Date");
END ;
SalesShipmentBuffer.CALCSUMS(Quantity);
IF SalesShipmentBuffer.Quantity <> "Sales Invoice Line".Quantity THEN BEGIN
SalesShipmentBuffer.DELETEALL;
EXIT("Sales Invoice Header"."Posting Date");
END;
END ELSE
EXIT("Sales Invoice Header"."Posting Date");
That's it. Now I just had to modify the DataItem source entries to show the necessary fields on the report. The changes I had to make are actually not so many, but the code is not documented at all and I could not find much information when I googled for this problem or the functions involved. So I hope my post is helpful, in case someone has a similar problem than I had.
On a side not, I do not understand why the above code segment is written in such a wired why, I belive (though I have not verified this), that it could be simplified into the following code, which is much more readable. Please leave comments if you feel that my analysis is wrong.
SalesShipmentBuffer.SETRANGE("Line No." ,"Sales Invoice Line"."Line No.");
SalesShipmentBuffer.SETRANGE("No." ,"Sales Invoice Line"."No.");
SalesShipmentBuffer.FINDFIRST;
IF SalesShipmentBuffer.COUNT > 0 THEN BEGIN
IF SalesShipmentBuffer.COUNT = 1 THEN BEGIN
SalesShipmentBuffer.DELETE;
EXIT(SalesShipmentBuffer2."Posting Date");
END ELSE BEGIN
SalesShipmentBuffer.CALCSUMS(Quantity);
SalesShipmentBuffer.DELETEALL;
EXIT("Sales Invoice Header"."Posting Date");
END;
END ELSE
EXIT("Sales Invoice Header"."Posting Date");