fix(dn-pipeline): a PO line with no unit no longer blocks delivery

61 of 148 purchase_order_items have an empty unit. Blocking every
delivery against them rejects 41% of lines over a gap on the order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-17 20:57:44 +02:00
parent e66f29b124
commit 703cdedc42
4 changed files with 54 additions and 3 deletions

View file

@ -69,6 +69,20 @@ describe("buildDeliveryNoteItemRows", () => {
expect(rows[0].condition_notes).toContain("unknown_unit");
});
it("passes the slip quantity through when the PO line has no unit", () => {
const { rows } = buildDeliveryNoteItemRows({
items: [{ description: "G6 Sub Base", qty: 12, unit: "ton", matched_po_item_id: "aaaaaaaaaaaaaaa" }],
poItems: [{ id: "aaaaaaaaaaaaaaa", unit: "" }],
confidence: 1,
});
expect(rows[0].qty_received).toBe(12);
expect(rows[0].qty_raw).toBe(12);
expect(rows[0].unit_raw).toBe("ton");
expect(rows[0].unit_converted).toBe(false);
expect(rows[0].purchase_order_item).toBe("aaaaaaaaaaaaaaa");
expect(rows[0].condition_notes).toContain("po unit missing");
});
it("keeps an unmatched line at zero with no link", () => {
const { rows } = buildDeliveryNoteItemRows({
items: [{ description: "Mystery", qty: 4, unit: "kg" }],

View file

@ -92,6 +92,13 @@ export function buildDeliveryNoteItemRows(
// local fetch). Keep the link for the audit trail but do not
// invent a quantity we cannot express in the right unit.
failure = "po_item_unavailable";
} else if (!normaliseUnit(poItem.unit ?? null)) {
// No target unit to convert into. Keep the link and the slip's
// own number — the alternative is discarding a real delivery
// because the purchase order is missing a field.
qtyReceived = qty;
converted = false;
failure = "po unit missing";
} else {
const conv = convertQty(qty, normaliseUnit(unitRaw), normaliseUnit(poItem.unit));
if (conv.ok) {

View file

@ -171,6 +171,19 @@ describe("runNoteChecks", () => {
expect(r.passed).toBe(false);
});
it("skips units_convertible when the PO line has no unit", () => {
const ctx = { ...CTX, poItems: [{ id: "bxqbo8rs04pjqwc", description: "G6 Sub Base", unit: "" }] };
const r = byId(runNoteChecks(note(), ctx), "units_convertible");
expect(r.passed).toBeNull();
expect(r.reason).toContain("no unit");
});
it("still fails when the SLIP line has no unit", () => {
const n = note({ items: [{ description: "G6 Sub Base", qty: 5, unit: null, unit_price: null }] });
const r = byId(runNoteChecks(n, CTX), "units_convertible");
expect(r.passed).toBe(false);
});
it("fails when the note number is already in delivery_notes", () => {
const ctx = { ...CTX, existingNoteNumbers: new Set(["DQ0010107"]) };
const r = byId(runNoteChecks(note(), ctx), "not_duplicate");

View file

@ -147,15 +147,32 @@ function unitsConvertible(note: NoteSegment, ctx: NoteCheckContext): CheckResult
return { id: "units_convertible", passed: null, reason: "no purchase order lines to convert into" };
}
const bad: string[] = [];
const missingPoUnit: string[] = [];
for (const it of note.items) {
const poItem = matchPoItem(it.description, ctx.poItems);
if (!poItem) continue; // product_matched already reports this
// A PO line with no stated unit cannot be converted into. That is a
// purchasing data gap, not a delivery problem — 61 of 148 PO lines
// are affected, and blocking them all would reject 41% of deliveries
// over a missing field on the order.
if (!normaliseUnit(poItem.unit ?? null)) {
missingPoUnit.push(it.description);
continue;
}
const conv = convertQty(it.qty, normaliseUnit(it.unit), normaliseUnit(poItem.unit ?? null));
if (!conv.ok) bad.push(`${it.description} (${it.unit ?? "no unit"} → ${poItem.unit || "no unit"})`);
}
return bad.length === 0
? { id: "units_convertible", passed: true, reason: "all units convert" }
: { id: "units_convertible", passed: false, reason: `cannot convert: ${bad.join(", ")}` };
if (bad.length > 0) {
return { id: "units_convertible", passed: false, reason: `cannot convert: ${bad.join(", ")}` };
}
if (missingPoUnit.length > 0) {
return {
id: "units_convertible",
passed: null,
reason: `purchase order line has no unit: ${missingPoUnit.join(", ")}`,
};
}
return { id: "units_convertible", passed: true, reason: "all units convert" };
}
function notDuplicate(note: NoteSegment, ctx: NoteCheckContext): CheckResult {