fix(dn): confirm refuses a slip on another supplier's PO
Co-Authored-By: MiniMax (Pinchy)
This commit is contained in:
parent
2dfe92b7d6
commit
0404688c75
5 changed files with 62 additions and 0 deletions
|
|
@ -317,6 +317,25 @@ export function supplierGuardOverride(manualOverrides: unknown): { reason: strin
|
|||
return reason ? { reason } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Did the supplier guard fail on this note?
|
||||
*
|
||||
* A slip printed by another company is not a posting problem, it is a
|
||||
* linking problem: the note has to be linked to the right order, or an
|
||||
* operator has to record why it is allowed through. Until one of those
|
||||
* happens, nothing on the note may be confirmed.
|
||||
*/
|
||||
export function supplierGuardFailed(note: unknown): boolean {
|
||||
const bag = note as { checks?: unknown } | null | undefined;
|
||||
const checks = bag?.checks;
|
||||
if (!Array.isArray(checks)) return false;
|
||||
return checks.some((check) => {
|
||||
if (!check || typeof check !== "object") return false;
|
||||
const row = check as { id?: unknown; passed?: unknown };
|
||||
return row.id === "supplier_matches_po" && row.passed === false;
|
||||
});
|
||||
}
|
||||
|
||||
function supplierMatchesPoCheck(note: NoteSegment, ctx: NoteCheckContext): CheckResult {
|
||||
const id = "supplier_matches_po";
|
||||
if (!ctx.linkedSupplier) return { id, passed: null, reason: "no supplier linked to this note yet" };
|
||||
|
|
|
|||
|
|
@ -170,3 +170,17 @@ it('refuses to confirm a slip while the site question is open',()=>{
|
|||
const p=threeLinePreview(c);
|
||||
expect(()=>prepareReadyLines(c,p)).toThrow(/Answer the outstanding site question/);
|
||||
});
|
||||
|
||||
// ---------- the supplier guard blocks a confirmation ----------
|
||||
it('refuses to confirm a slip whose supplier does not match the purchase order',()=>{
|
||||
const c=context();
|
||||
c.note.checks=[{id:'supplier_matches_po',passed:false,reason:'The slip is from FH Chamberlain Trading but this purchase order belongs to Avidan Trading CC.'}];
|
||||
const p=buildPreview(c,{allocations:[]});
|
||||
expect(()=>prepareDelivery(c,p)).toThrow(/supplier does not match the purchase order/);
|
||||
expect(()=>prepareReadyLines(c,p)).toThrow(/supplier does not match the purchase order/);
|
||||
});
|
||||
it('confirms a slip whose supplier guard has passed',()=>{
|
||||
const c=threeLines();c.note.checks=[{id:'supplier_matches_po',passed:true,reason:'The slip is from the supplier of this order.'}];
|
||||
const p=threeLinePreview(c);
|
||||
expect(prepareReadyLines(c,p)).toHaveLength(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {buildPreview,snapshotHash,type PreviewAllocation} from './ai-preview';
|
|||
import {PostLineSchema} from './post-line';
|
||||
import {prepareLineBooking,stableSourceKey,type LineBooking} from './line-booking';
|
||||
import {deliveryReady} from './delivery-ready';
|
||||
import {supplierGuardFailed} from './checks';
|
||||
import {readyIndexes} from '@/lib/inbox-three/readiness';
|
||||
import {openSiteDecision} from './site-decision-model';
|
||||
import type {LineConfirmation} from './line-booking';
|
||||
|
|
@ -29,6 +30,9 @@ export function resolveMatch(context:PreviewContext,preview:ProcessingPreview,in
|
|||
function assertConfirmable(context:PreviewContext){
|
||||
if(openSiteDecision(context.note.checks))throw Error('Answer the outstanding site question before confirming delivery');
|
||||
if(Array.isArray(context.note.checks)&&context.note.checks.some(c=>c&&typeof c==='object'&&c.id==='not_duplicate'&&c.passed===false))throw Error('Resolve the possible duplicate delivery before confirming');
|
||||
// A slip from another supplier is not this order's delivery. The link has
|
||||
// to be fixed, or an operator has to say why it is allowed through.
|
||||
if(supplierGuardFailed(context.note))throw Error("This slip's supplier does not match the purchase order's supplier. Fix the link or record an override first.");
|
||||
}
|
||||
|
||||
/** Preflight every line and cumulative totals before the first receipt write. */
|
||||
|
|
|
|||
|
|
@ -194,3 +194,24 @@ describe("readyIndexes", () => {
|
|||
expect(readyIndexes(c, preview)).toEqual([0]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- the supplier guard blocks a ready note ----------
|
||||
const guardedContext = () => context({
|
||||
lines: [{ sourceLineId: "pipe", description: "1050 pipe", qty: 2, unit: "m", unit_price: 1124.35, line_amount: 2248.7, product_code: null }],
|
||||
poItems: [{ id: PIPE_ITEM, description: "1050 pipe", unit: "m", qty: 100, rate: 1124.35, disc_percent: 0, vat_percent: 15 }],
|
||||
});
|
||||
describe("the supplier guard", () => {
|
||||
it("readies nothing on a note whose supplier guard failed", () => {
|
||||
const c = guardedContext();
|
||||
c.note.checks = [{ id: "supplier_matches_po", passed: false, reason: "The slip is from FH Chamberlain Trading but this purchase order belongs to Avidan Trading CC." }];
|
||||
const preview = buildPreview(c, { allocations: [allocation({ sourceLineIds: ["pipe"] })] });
|
||||
expect(readyIndexes(c, preview)).toEqual([]);
|
||||
});
|
||||
|
||||
it("readies the line once the supplier guard has passed", () => {
|
||||
const c = guardedContext();
|
||||
c.note.checks = [{ id: "supplier_matches_po", passed: true, reason: "The slip is from the supplier of this order." }];
|
||||
const preview = buildPreview(c, { allocations: [allocation({ sourceLineIds: ["pipe"] })] });
|
||||
expect(readyIndexes(c, preview)).toEqual([0]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
* ready in one and open in the other.
|
||||
*/
|
||||
import { allocationNeedsHelp } from "@/lib/dn-pipeline/delivery-ready";
|
||||
import { supplierGuardFailed } from "@/lib/dn-pipeline/checks";
|
||||
import { prepareLineBooking } from "@/lib/dn-pipeline/line-booking";
|
||||
import { dims, resolvePoItem, type PoItemRef } from "@/lib/dn-pipeline/match-po-item";
|
||||
import { sameReceivingUnit } from "@/lib/dn-pipeline/receiving-defaults";
|
||||
|
|
@ -199,6 +200,9 @@ export function lineReady(context: PreviewContext, preview: ProcessingPreview, i
|
|||
|
||||
/** The indexes of the lines that can be confirmed without a change. */
|
||||
export function readyIndexes(context: PreviewContext, preview: ProcessingPreview): number[] {
|
||||
// Nothing on a slip from another supplier is ready: the note has to be
|
||||
// linked to the right order first, so the stage never offers Ready to post.
|
||||
if (supplierGuardFailed(context.note)) return [];
|
||||
const ready: number[] = [];
|
||||
preview.allocations.forEach((_, index) => {
|
||||
if (lineReady(context, preview, index)) ready.push(index);
|
||||
|
|
|
|||
Loading…
Reference in a new issue