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:
parent
e66f29b124
commit
703cdedc42
4 changed files with 54 additions and 3 deletions
|
|
@ -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" }],
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue