feat(collection): return mistaken pickups to pool
This commit is contained in:
parent
dde816f676
commit
ebc045a145
2 changed files with 95 additions and 0 deletions
1
DEPLOY
1
DEPLOY
|
|
@ -8,3 +8,4 @@
|
|||
quotes.pb.js
|
||||
audit_log_utils.js
|
||||
audit_log.pb.js
|
||||
collection_return.pb.js
|
||||
|
|
|
|||
94
collection_return.pb.js
Normal file
94
collection_return.pb.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* 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");
|
||||
Loading…
Reference in a new issue