94 lines
4.9 KiB
JavaScript
94 lines
4.9 KiB
JavaScript
/**
|
|
* Revision checks for the Collection Confirm return-to-pool batch.
|
|
* PB runs each child record request (and these hooks) inside the batch's
|
|
* database transaction. A concurrent confirm/invoice/booking therefore
|
|
* either precedes these checks or follows the complete reset; it cannot
|
|
* slip between validation and the writes. Ordinary updates are untouched.
|
|
* Deploy with the app; the service refuses resets without this capability.
|
|
*/
|
|
routerAdd("GET", "/api/wfdash/collection-return-guard", (e) => {
|
|
return e.json(200, { version: 1 });
|
|
}, $apis.requireSuperuserAuth());
|
|
|
|
onRecordUpdateRequest((e) => {
|
|
const info = e.requestInfo();
|
|
const guard = info.body && info.body._collection_return_guard;
|
|
if (!guard) {
|
|
// A confirmer may have read the row before a return committed. Check
|
|
// its current state and save together, so it cannot resurrect a pickup.
|
|
if (e.record.collection().name === "order_items" && info.body && info.body.item_status === "ready_invoice") {
|
|
const originalApp = e.app;
|
|
try {
|
|
return originalApp.runInTransaction((txApp) => {
|
|
e.app = txApp;
|
|
const latest = JSON.parse(JSON.stringify(txApp.findRecordById("order_items", e.record.id || e.record.original().id)));
|
|
const original = JSON.parse(JSON.stringify(e.record.original()));
|
|
if (latest.updated !== original.updated || ["collection_pool", "assigned"].indexOf(latest.item_status) !== -1) {
|
|
throw new BadRequestError("Collection changed. Refresh before confirming for invoicing.");
|
|
}
|
|
return e.next();
|
|
});
|
|
} finally { e.app = originalApp; }
|
|
}
|
|
return e.next();
|
|
}
|
|
if (!e.auth || !e.auth.isSuperuser()) throw new ForbiddenError("Collection resets require the server.");
|
|
if (info.context !== "batch") throw new BadRequestError("Collection resets must run in a batch transaction.");
|
|
|
|
const current = JSON.parse(JSON.stringify(e.record.original()));
|
|
if (!guard.updated || current.updated !== guard.updated) {
|
|
throw new BadRequestError("This item changed. Refresh Collection Confirm and try again.");
|
|
}
|
|
const collection = e.record.collection().name;
|
|
let refType = "order_item";
|
|
let refId = current.id;
|
|
if (collection === "order_items") {
|
|
let details = current.order_item_details;
|
|
if (typeof details === "string") {
|
|
try { details = JSON.parse(details); } catch (_) { details = null; }
|
|
}
|
|
const invoice = details && details.invoice;
|
|
const billed = invoice && (invoice.qbInvoiceId || invoice.qbInvoiceLink || invoice.billingCompany);
|
|
const downstream = ["qty_invoiced", "qty_delivered", "qty_stocked", "qty_picked", "qty_loaded", "surplus_qty"]
|
|
.some((field) => Number(current[field] || 0) > 0);
|
|
if (current.item_status !== "collected" || downstream || billed || current.customer_invoice_number ||
|
|
current.delivery_note_number || current.invoiced_date || current.delivered_at || current.stock_booked_from) {
|
|
throw new BadRequestError("This item has moved on and cannot be returned to the collection pool.");
|
|
}
|
|
} else {
|
|
let parent = current;
|
|
if (collection === "stock_purchase_items") {
|
|
if (current.status !== "collected" || !current.purchase) throw new BadRequestError("Stock item is no longer collected.");
|
|
parent = JSON.parse(JSON.stringify(e.app.findRecordById("stock_purchases", current.purchase)));
|
|
if (!guard.parent_updated || parent.updated !== guard.parent_updated) throw new BadRequestError("Stock PO changed. Refresh before returning it.");
|
|
}
|
|
if (parent.booked_at || ["sent", "received"].indexOf(parent.status) === -1) {
|
|
throw new BadRequestError("Stock PO is booked or no longer open for collection.");
|
|
}
|
|
refType = "stock_purchase";
|
|
refId = parent.id;
|
|
}
|
|
const ledger = e.app.findRecordsByFilter("stock_ledger",
|
|
"reference_type = {:type} && reference_id = {:id}", "", 1, 0,
|
|
{ type: refType, id: refId });
|
|
if (ledger.length) throw new BadRequestError("A stock booking exists; collection cannot be reset.");
|
|
return e.next();
|
|
}, "order_items", "stock_purchase_items", "stock_purchases");
|
|
|
|
// Booking writes the ledger before inventory. If it read the old received
|
|
// quantities before a return, reject its first write after the PO reopens.
|
|
// Conversely, a committed ledger entry makes the return guard refuse.
|
|
onRecordCreateRequest((e) => {
|
|
if (e.record.getString("reference_type") !== "stock_purchase") return e.next();
|
|
const originalApp = e.app;
|
|
try {
|
|
return originalApp.runInTransaction((txApp) => {
|
|
e.app = txApp;
|
|
const purchase = JSON.parse(JSON.stringify(txApp.findRecordById("stock_purchases", e.record.getString("reference_id"))));
|
|
if (purchase.status !== "received" || purchase.booked_at) {
|
|
throw new BadRequestError("Stock PO is no longer awaiting booking. Refresh before trying again.");
|
|
}
|
|
return e.next();
|
|
});
|
|
} finally { e.app = originalApp; }
|
|
}, "stock_ledger");
|