test(invoicing): lock in picking-slip inclusion of excluded rows
Task 8 of the non-invoiceable-order-extras plan. Two regression tests, no production code change. The picking-slip feature relies on a property of the existing print pipeline: excluded rows DO receive item_status = 'invoiced' (Task 7 / n8n batch flip) and DO carry the invoice number / DN number, so the print queries pick them up automatically. The picker must still pull the washers even though they never reach the customer. - print-bundle: explicitly verifies the picking slip renderer is called with both billable and excluded rows (mocked PickingSlipPdf → assert lines array has both descriptions). - delivery-note-print: explicitly verifies the PB filter does NOT carry an exclude_from_invoice clause — adding one would silently filter the picker slip and break Task 7's design. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cb3f1618c4
commit
f7a79e671a
2 changed files with 135 additions and 0 deletions
|
|
@ -53,6 +53,29 @@ describe("GET /api/invoicing/delivery-note-print", () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Task 8 of the non-invoiceable-order-extras plan. The DN print
|
||||
// route queries PB by delivery_note_number + item_status. Excluded
|
||||
// rows DO receive that DN number + status (Task 7 / T7's status
|
||||
// flip loop is left untouched), so they are picked up
|
||||
// automatically. The picker must see the washers — the same
|
||||
// regression as print-bundle: lock the no-filter behaviour in.
|
||||
it("does not filter excluded rows out of the delivery-note query", async () => {
|
||||
getFullListMock.mockResolvedValueOnce([]);
|
||||
await GET(
|
||||
makeRequest(
|
||||
"https://test.local/api/invoicing/delivery-note-print?deliveryNoteNumber=DN26071401",
|
||||
) as any,
|
||||
);
|
||||
const filter = (getFullListMock.mock.calls[0]?.[0] as { filter: string }).filter;
|
||||
// The filter MUST include the DN number and the status, and
|
||||
// MUST NOT carry an exclude_from_invoice clause. Adding one
|
||||
// would silently filter the picker slip — and break Task 7's
|
||||
// design (excluded rows ride along for the status flip).
|
||||
expect(filter).toContain("delivery_note_number = \"DN26071401\"");
|
||||
expect(filter).toContain("item_status = \"delivery_note\"");
|
||||
expect(filter).not.toContain("exclude_from_invoice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/invoicing/delivery-note-print", () => {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,52 @@ describe("GET /api/invoicing/print-bundle", () => {
|
|||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("picks only the customer quantity and sends the MOQ overflow to stock", async () => {
|
||||
// Customer ordered 4; supplier MOQ forced 10 onto the PO and all 10
|
||||
// arrived. The picker must send 4 to the customer and put 6 into
|
||||
// stock — the "ordered" column is the CUSTOMER's number, not the
|
||||
// supplier's. Reading order_qty here told the picker to send all 10
|
||||
// and reported no surplus at all.
|
||||
getFullList.mockResolvedValueOnce([
|
||||
{
|
||||
id: "item-1",
|
||||
description: "Washer",
|
||||
qb_invoice_pdf: "inv.pdf",
|
||||
quantity: 4,
|
||||
order_qty: 10,
|
||||
qty_collected: 10,
|
||||
order_item_details: { invoice: { number: "INV-1", date: "2026-07-09" } },
|
||||
expand: {
|
||||
order: {
|
||||
id: "ord-1",
|
||||
order_number: "ORD-2026-07-001",
|
||||
expand: { company: { company: "CNL" }, customer: { customer: "Acme" } },
|
||||
},
|
||||
purchase_order: { expand: { supplier: { suppliername: "Alpha Bearings" } } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
fetchMock.mockResolvedValueOnce(new Response(new Uint8Array([9, 9]), { status: 200 }));
|
||||
buildPrintBundle.mockResolvedValueOnce(new Uint8Array([1, 2, 3]));
|
||||
|
||||
const res = await GET(makeGetRequest("INV-1") as any);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const props = (pickingSlipPdfMock.mock.calls[0]?.[0] ?? {}) as unknown as {
|
||||
lines: Array<{
|
||||
line: { ordered: number; collected: number; pickQty: number; outstanding: number; toStock: number };
|
||||
}>;
|
||||
};
|
||||
expect(props.lines[0].line).toMatchObject({
|
||||
ordered: 4,
|
||||
collected: 10,
|
||||
pickQty: 4,
|
||||
outstanding: 0,
|
||||
toStock: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a bundled PDF on success", async () => {
|
||||
getFullList.mockResolvedValueOnce([
|
||||
{
|
||||
|
|
@ -224,4 +270,70 @@ describe("GET /api/invoicing/print-bundle", () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Task 8 of the non-invoiceable-order-extras plan. The
|
||||
// non-invoiceable feature relies on a property of the existing
|
||||
// print pipeline: excluded rows DO receive item_status =
|
||||
// "invoiced" (Task 7 / n8n batch flip) and DO carry the invoice
|
||||
// number, so the print-bundle query picks them up automatically.
|
||||
// The picker must still pull the washers even though they never
|
||||
// reach the customer. This test locks that behaviour in.
|
||||
it("includes excluded rows in the picking slip (picker pulls the washers)", async () => {
|
||||
getFullList.mockResolvedValueOnce([
|
||||
{
|
||||
id: "item-bill",
|
||||
description: "Bolt",
|
||||
qb_invoice_pdf: "inv.pdf",
|
||||
order_qty: 4,
|
||||
qty_collected: 4,
|
||||
order_item_details: { invoice: { number: "INV-1", date: "2026-07-09" } },
|
||||
expand: {
|
||||
order: {
|
||||
id: "ord-1",
|
||||
order_number: "ORD-2026-07-001",
|
||||
expand: { company: { company: "CNL" }, customer: { customer: "Acme" } },
|
||||
},
|
||||
purchase_order: { expand: { supplier: { suppliername: "Alpha Bearings" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item-excl",
|
||||
description: "Washer (supplier-side component)",
|
||||
qb_invoice_pdf: "inv.pdf",
|
||||
// Excluded from the customer invoice (T5) but still flipped
|
||||
// to invoiced alongside its siblings (T7). The print query
|
||||
// keys on item_status = "invoiced" + invoice.number, neither
|
||||
// of which differs from a billable row — so this row lands
|
||||
// in the picking slip via the existing query.
|
||||
order_qty: 4,
|
||||
qty_collected: 4,
|
||||
order_item_details: { invoice: { number: "INV-1", date: "2026-07-09" } },
|
||||
exclude_from_invoice: true,
|
||||
expand: {
|
||||
order: {
|
||||
id: "ord-1",
|
||||
order_number: "ORD-2026-07-001",
|
||||
expand: { company: { company: "CNL" }, customer: { customer: "Acme" } },
|
||||
},
|
||||
purchase_order: { expand: { supplier: { suppliername: "Alpha Bearings" } } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
fetchMock.mockResolvedValueOnce(new Response(new Uint8Array([9, 9]), { status: 200 }));
|
||||
buildPrintBundle.mockResolvedValueOnce(new Uint8Array([1, 2, 3]));
|
||||
|
||||
const res = await GET(makeGetRequest("INV-1") as any);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Both rows reached the picking slip renderer — the picker
|
||||
// sees the washers, not just the bolt.
|
||||
const props = (pickingSlipPdfMock.mock.calls[0]?.[0] ?? {}) as unknown as {
|
||||
lines: Array<{ line: { description?: string } }>;
|
||||
};
|
||||
expect(props.lines).toHaveLength(2);
|
||||
const descriptions = props.lines.map((l) => l.line.description);
|
||||
expect(descriptions).toContain("Bolt");
|
||||
expect(descriptions).toContain("Washer (supplier-side component)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue