fix(delivery-notes): stop flagging duplicates once a copy is zeroed

The "Possible duplicate receipts" warning grouped posted receipts by
document number regardless of what they book. After the office corrects
the duplicate copy of a slip to 0 (receipts are delivery evidence and
cannot be deleted), the pair kept showing.

Receipts whose lines all total 0 are now left out of the grouping: a copy
that books nothing cannot double-count. Receipts with no lines are still
flagged, and pairs where both copies book quantity still show.

Clears the four Raumix DQ0010116/0472/0051/0092 groups on MEGCPO9165.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-11-09 10:31:30 +02:00
parent 648578c060
commit dda421effa
3 changed files with 29 additions and 4 deletions

View file

@ -107,7 +107,7 @@ export default async function DeliveryNotesPage({
}));
const posForAssign: PoChoice[] = buildPoChoices(pos, sites);
const possibleDuplicates = possibleDuplicateReceiptGroups(dns, pos);
const possibleDuplicates = possibleDuplicateReceiptGroups(dns, pos, itemsByDn);
// KPIs describe the full site-scoped set of delivery notes, not the rows
// the operator has filtered in the table.

View file

@ -37,6 +37,18 @@ describe("possible duplicate posted receipts", () => {
]);
});
it("stops flagging a pair once the duplicate copy has been corrected to zero", () => {
const rows = [receipt("kept", "s", "DQ0010116"), receipt("zeroed", "s", "DQ0010116")];
const booked = new Map([["kept", { count: 1, total: 30.26 }], ["zeroed", { count: 1, total: 0 }]]);
expect(possibleDuplicateReceiptGroups(rows, [], booked)).toEqual([]);
});
it("still flags receipts that have no lines, and pairs where both copies still book quantity", () => {
const rows = [receipt("a", "s", "DQ1"), receipt("b", "s", "DQ1"), receipt("c", "s", "DQ2"), receipt("d", "s", "DQ2")];
const booked = new Map([["a", { count: 1, total: 30 }], ["b", { count: 1, total: 30 }]]);
expect(possibleDuplicateReceiptGroups(rows, [], booked).map((g) => g.receipts.map((r) => r.id))).toEqual([["a", "b"], ["c", "d"]]);
});
it("also flags an invoice explicitly referencing an already posted delivery", () => {
const invoice = receipt("invoice", "s", "INV1");
invoice.raw_ocr_data.documentReferences.push("DN1");

View file

@ -14,14 +14,27 @@ export type PossibleDuplicateReceiptGroup = { key: string; receipts: DuplicateRe
const object = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
const text = (value: unknown): string => typeof value === "string" ? value : "";
/** Informational only: callers pass site-scoped receipts; values stay untouched. */
/**
* Informational only: callers pass site-scoped receipts; values stay untouched.
*
* `bookedQty` (receipt id → total qty_received over its lines), when given,
* leaves out receipts whose lines have all been corrected to zero: a copy
* that books nothing cannot double-count, so once the office has zeroed the
* duplicate copy the pair stops being flagged. Receipts with no lines at all
* are kept, because nothing shows they were reviewed.
*/
export function possibleDuplicateReceiptGroups(
receipts: readonly ReceiptReviewRecord[],
pos: readonly ReceiptPoLookup[] = [],
bookedQty?: ReadonlyMap<string, { count: number; total: number }>,
): PossibleDuplicateReceiptGroup[] {
const poById = new Map(pos.map((po) => [po.id, po]));
const posted: ReceiptReviewRecord[] = receipts.filter((receipt) => Boolean(text(receipt.posted_at)) || receipt.is_verified === true ||
["auto-verified", "manual-verified"].includes(text(receipt.processing_status))).map((receipt) => {
const zeroed = (receipt: ReceiptReviewRecord) => {
const booked = bookedQty?.get(receipt.id);
return Boolean(booked && booked.count > 0 && booked.total === 0);
};
const posted: ReceiptReviewRecord[] = receipts.filter((receipt) => !zeroed(receipt) && (Boolean(text(receipt.posted_at)) || receipt.is_verified === true ||
["auto-verified", "manual-verified"].includes(text(receipt.processing_status)))).map((receipt) => {
const expanded = object(receipt.expand);
const expandedPo = object(expanded.purchase_order);
const po = poById.get(text(receipt.purchase_order));