Text PDFs already get it from pdftotext; photos and scans have no text, so 4 of 5 undated POs were photos that do print a PO Creation Date. - n8n PO reader (stored patch): asked for order_date = 'PO Creation Date' (day/month/year), never Required By or Printed; the callback passes it on. - process-batch accepts extracted.order_date (YYYY-MM-DD); the PDF text wins when both exist. - create_purchase_order / update_purchase_order / move_to_purchase_orders take order_date, so agents record it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.2 KiB
JavaScript
78 lines
3.2 KiB
JavaScript
const sourceItem = $input.item;
|
|
if (!sourceItem) {
|
|
throw new Error('Build Callback Payload: no input item');
|
|
}
|
|
const parsed = (sourceItem.json && typeof sourceItem.json.output === 'object' && sourceItem.json.output !== null)
|
|
? sourceItem.json.output
|
|
: sourceItem.json;
|
|
|
|
// This item's id is the purchase_orders row Atomic Claim just claimed --
|
|
// the placeholder created at upload time (src/lib/po-placeholder.ts).
|
|
// There is no separate batch row anymore.
|
|
const purchase_order_id = $('Atomic Claim').item.json.id;
|
|
const po_number_extracted = String(parsed.po_number || '').trim();
|
|
const supplier = String(parsed.supplier || '').trim();
|
|
const project_code = String(parsed.project_code || '').trim();
|
|
if (!po_number_extracted) {
|
|
throw new Error('AI extraction returned an empty PO number');
|
|
}
|
|
|
|
const po_number_normalized = po_number_extracted
|
|
.replace(/^p\.?o\.?([-#.]|\s+)?/i, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.toUpperCase();
|
|
if (!po_number_normalized) {
|
|
throw new Error('AI Agent po_number normalises to empty string: "' + po_number_extracted + '"');
|
|
}
|
|
|
|
const required_by_date = parsed.required_by_date || undefined;
|
|
// "PO Creation Date" printed on the PO: when it was ordered. Money is dated by it.
|
|
const order_date = /^\d{4}-\d{2}-\d{2}$/.test(String(parsed.order_date || '')) ? String(parsed.order_date) : undefined;
|
|
const ordered_by = parsed.ordered_by || undefined;
|
|
const rawItems = Array.isArray(parsed.items) ? parsed.items : [];
|
|
const items = rawItems.map(it => {
|
|
if (!it) return null;
|
|
return {
|
|
...it,
|
|
qty: Number(it.qty),
|
|
rate: Number(it.rate),
|
|
material_code: (it.material_code && String(it.material_code).trim() !== '' && String(it.material_code).toLowerCase() !== 'null') ? String(it.material_code).trim() : '',
|
|
cost_code: (it.cost_code && String(it.cost_code).trim() !== '' && String(it.cost_code).toLowerCase() !== 'null') ? String(it.cost_code).trim() : '',
|
|
disc_percent: (it.disc_percent !== null && it.disc_percent !== undefined && !isNaN(it.disc_percent)) ? Number(it.disc_percent) : 0,
|
|
vat_percent: (it.vat_percent !== null && it.vat_percent !== undefined && !isNaN(it.vat_percent)) ? Number(it.vat_percent) : 15
|
|
};
|
|
}).filter(it =>
|
|
it &&
|
|
typeof it.description === 'string' && it.description.trim().length > 0 &&
|
|
!isNaN(it.qty) && it.qty >= 0 &&
|
|
!isNaN(it.rate) && it.rate >= 0
|
|
);
|
|
if (items.length === 0) {
|
|
throw new Error('AI Agent returned no valid line items (all missing description/qty/rate)');
|
|
}
|
|
|
|
// Duplicate detection now lives server-side in
|
|
// /api/purchase-orders/process-batch (merge onto the existing PO, delete
|
|
// this placeholder) -- it already owns the purchase_orders schema and is
|
|
// covered by tests. n8n no longer queries or writes purchase_orders
|
|
// directly to pre-empt it.
|
|
//
|
|
// purchase_order_number is the exact field name the route validates --
|
|
// the old contract used po_number here, which would fail every upload.
|
|
return {
|
|
json: {
|
|
purchase_order_id,
|
|
extracted: {
|
|
purchase_order_number: po_number_normalized,
|
|
po_number_extracted,
|
|
supplier,
|
|
project_code,
|
|
required_by_date,
|
|
order_date,
|
|
ordered_by,
|
|
items,
|
|
raw_ocr: { source: 'ai-agent', model: 'MiniMax-M3' },
|
|
},
|
|
},
|
|
};
|