261 lines
20 KiB
JavaScript
261 lines
20 KiB
JavaScript
// services/sales/samples.js
|
|
// Sample Requests — msale's two sample workflows:
|
|
//
|
|
// Domestic (sales_sample): raised by Sales (SM/BUH) for a customer
|
|
// 1 Submitted ──buh_review──▶ 2 Approved | 3 Modification | 4 Rejected
|
|
// 3 ──creator edits──▶ 5 Resubmitted ──buh_review──▶ 2/3/4
|
|
// 2/5→ Logistics: stock available? yes ▶ 6 QA pending (batch nos, due +2d) / no ▶ 7 In production
|
|
// 7 ──logistics──▶ 6 ; 6 ──logistics (QA verified + challan/courier)──▶ 8 Shipped
|
|
// 8 ──feedback (Sales)──▶ 9 Completed
|
|
// Export (sales_export_sample): raised by Export team
|
|
// 1 Submitted ──logistics──▶ 2 In production | 3 QA pending ; 2 ──▶ 3 ; 3 ──QA verified + dispatch docs──▶ 4 Dispatched
|
|
'use strict';
|
|
const { query, one, tx } = require('./db');
|
|
const { SAMPLE, EXPORT_SAMPLE, sampleStatusLabel, exportSampleStatusLabel } = require('./constants');
|
|
const masters = require('./masters');
|
|
const notify = require('./notify');
|
|
const { hasStepPerm } = require('../../middleware/auth');
|
|
const { appendRemark, refNo, log, getDocs } = require('./orders');
|
|
|
|
function fail(msg, code = 400) { const e = new Error(msg); e.status = code; throw e; }
|
|
const canS = (actor, key, perm) => hasStepPerm(actor.user, `sales_sample:${key}`, perm);
|
|
const canE = (actor, key, perm) => hasStepPerm(actor.user, `sales_export_sample:${key}`, perm);
|
|
function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
|
|
|
|
async function itemsFor(id, kind) {
|
|
return (await query(`SELECT * FROM dbo.ZSO_SAMPLE_ITEMS WHERE SAMPLE_ID=? AND KIND=? AND ACTIVE=1 ORDER BY ID`, [parseInt(id), kind]))
|
|
.map(r => ({ id: r.ID, productId: r.PRODUCT_ID, itemCode: r.ITEM_CODE, displayCode: r.DISPLAY_CODE || r.ITEM_CODE, description: r.DESCRIPTION || '',
|
|
qty: Number(r.QTY), nop: r.NOP, batchNo: r.BATCH_NO || '' }));
|
|
}
|
|
async function priceSampleLines(lines, divisionId, catalog) {
|
|
if (!Array.isArray(lines) || !lines.length) fail('Please add at least one product');
|
|
const out = [];
|
|
for (const [i, l] of lines.entries()) {
|
|
const qty = Number(l.qty);
|
|
if (!(qty > 0)) fail(`Line ${i + 1}: quantity must be greater than 0`);
|
|
const p = await masters.resolveProduct(l.key, divisionId, catalog);
|
|
if (!p) fail(`Line ${i + 1}: product not found`);
|
|
out.push({ productId: p.id, itemCode: p.itemCode, displayCode: p.displayCode, description: p.description, qty, nop: Math.max(1, Math.ceil(qty / (p.packSize || 1))) });
|
|
}
|
|
return out;
|
|
}
|
|
async function writeItems(id, kind, items, r) {
|
|
await query(`UPDATE dbo.ZSO_SAMPLE_ITEMS SET ACTIVE=0 WHERE SAMPLE_ID=? AND KIND=?`, [id, kind], r);
|
|
for (const it of items) {
|
|
await query(`INSERT INTO dbo.ZSO_SAMPLE_ITEMS (SAMPLE_ID,KIND,PRODUCT_ID,ITEM_CODE,DISPLAY_CODE,DESCRIPTION,QTY,NOP) VALUES (?,?,?,?,?,?,?,?)`,
|
|
[id, kind, it.productId || null, it.itemCode, it.displayCode, it.description, it.qty, it.nop], r);
|
|
}
|
|
}
|
|
|
|
// ── Domestic ────────────────────────────────────────────────────────────────
|
|
function sFromRow(r) {
|
|
return { id: r.ID, refNo: r.REF_NO, cardCode: r.CARD_CODE, cardName: r.CARD_NAME, address: r.ADDRESS || '', bbLicenceNo: r.BB_LICENCE_NO || '', bbName: r.BB_NAME || '',
|
|
divisionId: r.DIVISION_ID, reqDate: r.REQ_DATE, deliveryDays: r.DELIVERY_DAYS, status: r.STATUS, statusLabel: sampleStatusLabel(r.STATUS),
|
|
internalRemarks: r.INTERNAL_REMARKS || '', modificationRemarks: r.MODIFICATION_REMARKS || '', rejectionRemarks: r.REJECTION_REMARKS || '',
|
|
stockAvailable: r.STOCK_AVAILABLE == null ? null : !!r.STOCK_AVAILABLE, qaDueDate: r.QA_DUE_DATE, qaVerified: r.QA_VERIFIED == null ? null : !!r.QA_VERIFIED,
|
|
challanNo: r.CHALLAN_NO || '', challanDate: r.CHALLAN_DATE, courierNo: r.COURIER_NO || '', dispatchInfo: r.DISPATCH_INFO || '', feedback: r.FEEDBACK || '',
|
|
createdBy: r.CREATED_BY, createdByName: r.CREATED_BY_NAME || '', createdAt: r.CREATED_AT, updatedAt: r.UPDATED_AT, updatedByName: r.UPDATED_BY_NAME || '', legacy: !!r.LEGACY };
|
|
}
|
|
async function sampleScope(actor) {
|
|
const prof = await masters.getProfile(actor.user);
|
|
return prof;
|
|
}
|
|
async function listSamples(actor, f = {}) {
|
|
if (!canS(actor, 'view', 'view')) fail('You are not assigned "view" on Sample Requests', 403);
|
|
const prof = await sampleScope(actor);
|
|
if (!prof.divisions.length) return [];
|
|
const w = [`DIVISION_ID IN (${prof.divisions.map(Number).join(',')})`]; const p = [];
|
|
// A Sales Manager sees only the requests they raised (msale behaviour).
|
|
if (prof.scope !== 'all') { w.push('CREATED_BY=?'); p.push(parseInt(actor.user.id)); }
|
|
if (f.statuses) { const st = String(f.statuses).split(',').map(Number).filter(Boolean); if (st.length) w.push(`STATUS IN (${st.join(',')})`); }
|
|
if (f.search) { w.push('(REF_NO LIKE ? OR CARD_NAME LIKE ? OR CARD_CODE LIKE ?)'); const s = `%${f.search}%`; p.push(s, s, s); }
|
|
return (await query(`SELECT TOP 2000 * FROM dbo.ZSO_SAMPLES WHERE ${w.join(' AND ')} ORDER BY ID DESC`, p)).map(sFromRow);
|
|
}
|
|
async function getSample(actor, id) {
|
|
const r = await one(`SELECT * FROM dbo.ZSO_SAMPLES WHERE ID=?`, [parseInt(id)]);
|
|
if (!r || !canS(actor, 'view', 'view')) fail('Sample request not found', 404);
|
|
const prof = await sampleScope(actor);
|
|
if (!prof.divisions.includes(Number(r.DIVISION_ID)) || (prof.scope !== 'all' && r.CREATED_BY !== parseInt(actor.user.id))) fail('Sample request not found', 404);
|
|
const s = sFromRow(r);
|
|
s.items = await itemsFor(s.id, 'domestic');
|
|
s.log = (await query(`SELECT * FROM dbo.ZSO_LOG WHERE ENTITY='sample' AND ENTITY_ID=? ORDER BY ID`, [s.id])).map(l => ({ action: l.ACTION, fromStatus: l.FROM_STATUS, toStatus: l.TO_STATUS, remarks: l.REMARKS || '', actorName: l.ACTOR_NAME || '', at: l.AT }));
|
|
s.actions = sampleActions(actor, s);
|
|
return s;
|
|
}
|
|
function sampleActions(actor, s) {
|
|
const a = [];
|
|
if ([SAMPLE.SUBMITTED, SAMPLE.RESUBMITTED].includes(s.status) && canS(actor, 'buh_review', 'approve')) a.push('buh_approve', 'buh_modify', 'buh_reject');
|
|
if (s.status === SAMPLE.MODIFICATION && canS(actor, 'create', 'edit') && s.createdBy === parseInt(actor.user.id)) a.push('resubmit');
|
|
if (s.status === SAMPLE.APPROVED && canS(actor, 'logistics', 'approve')) a.push('stock');
|
|
if (s.status === SAMPLE.IN_PRODUCTION && canS(actor, 'logistics', 'approve')) a.push('stock');
|
|
if (s.status === SAMPLE.QA_PENDING && canS(actor, 'logistics', 'approve')) a.push('dispatch', 'qa_extend');
|
|
if (s.status === SAMPLE.SHIPPED && canS(actor, 'feedback', 'approve')) a.push('feedback');
|
|
return a;
|
|
}
|
|
async function createSample(actor, d) {
|
|
if (!canS(actor, 'create', 'add')) fail('You are not assigned "add" on sales_sample:create', 403);
|
|
const prof = await masters.getProfile(actor.user);
|
|
const divisionId = parseInt(d.divisionId);
|
|
if (!prof.divisions.includes(divisionId)) fail('This product category is not in your divisions', 403);
|
|
const div = await masters.getDivision(divisionId);
|
|
if (!div || !div.active) fail('This product category is disabled');
|
|
if (!d.cardCode) fail('Customer is required');
|
|
const items = await priceSampleLines(d.lines, divisionId, 'domestic');
|
|
const id = await tx(async (r) => {
|
|
const row = await one(`INSERT INTO dbo.ZSO_SAMPLES (CARD_CODE,CARD_NAME,ADDRESS,BB_LICENCE_NO,BB_NAME,DIVISION_ID,REQ_DATE,DELIVERY_DAYS,STATUS,CREATED_BY,CREATED_BY_NAME)
|
|
OUTPUT INSERTED.ID, INSERTED.CREATED_AT VALUES (?,?,?,?,?,?,?,?,1,?,?)`,
|
|
[d.cardCode, d.cardName || '', d.address || '', d.bbLicenceNo || '', d.bbName || '', divisionId, d.reqDate || new Date(), parseInt(d.deliveryDays) || null,
|
|
parseInt(actor.user.id), actor.name], r);
|
|
await query(`UPDATE dbo.ZSO_SAMPLES SET REF_NO=? WHERE ID=?`, [refNo(row.ID, row.CREATED_AT), row.ID], r);
|
|
await writeItems(row.ID, 'domestic', items, r);
|
|
await log('sample', row.ID, 'created', null, 1, d.remarks, actor, r);
|
|
return row.ID;
|
|
});
|
|
notify.sampleEvent('domestic', `Sample request submitted`, [`${actor.name} raised a sample request for ${d.cardName || d.cardCode}.`], 'sales_sample:buh_review', divisionId);
|
|
return id;
|
|
}
|
|
// data per action:
|
|
// buh_approve {internalRemarks} · buh_modify {modificationRemarks} · buh_reject {rejectionRemarks}
|
|
// resubmit {lines, ...header} · stock {stockAvailable, batchNos:{itemId:batch}, outOfStockDate}
|
|
// dispatch {challanNo, challanDate, courierNo, dispatchInfo} · qa_extend {} · feedback {feedback}
|
|
async function sampleAct(actor, id, action, d = {}) {
|
|
const s = await getSample(actor, id);
|
|
if (!s.actions.includes(action)) fail('This action is not available for you at the current status', 403);
|
|
let to = s.status; const sets = []; const vals = []; let notifyArgs = null;
|
|
switch (action) {
|
|
case 'buh_approve': to = SAMPLE.APPROVED; sets.push('INTERNAL_REMARKS=?'); vals.push(appendRemark(s.internalRemarks, actor.name, d.internalRemarks));
|
|
notifyArgs = ['Sample request approved', [`Sample ${s.refNo} for ${s.cardName} approved by ${actor.name}.`], 'sales_sample:logistics']; break;
|
|
case 'buh_modify': if (!String(d.modificationRemarks || '').trim()) fail('Modification remarks are required');
|
|
to = SAMPLE.MODIFICATION; sets.push('MODIFICATION_REMARKS=?'); vals.push(d.modificationRemarks); break;
|
|
case 'buh_reject': if (!String(d.rejectionRemarks || '').trim()) fail('Rejection remarks are required');
|
|
to = SAMPLE.REJECTED; sets.push('REJECTION_REMARKS=?'); vals.push(d.rejectionRemarks); break;
|
|
case 'resubmit': {
|
|
const items = await priceSampleLines(d.lines, s.divisionId, 'domestic');
|
|
to = SAMPLE.RESUBMITTED;
|
|
sets.push('BB_LICENCE_NO=?', 'BB_NAME=?', 'DELIVERY_DAYS=?', 'ADDRESS=?'); vals.push(d.bbLicenceNo ?? s.bbLicenceNo, d.bbName ?? s.bbName, parseInt(d.deliveryDays) || s.deliveryDays, d.address ?? s.address);
|
|
await tx(r => writeItems(s.id, 'domestic', items, r));
|
|
notifyArgs = ['Sample request resubmitted', [`Sample ${s.refNo} was modified and resubmitted.`], 'sales_sample:buh_review']; break;
|
|
}
|
|
case 'stock': {
|
|
if (d.stockAvailable === undefined || d.stockAvailable === null || d.stockAvailable === '') fail('Select whether stock is available');
|
|
const yes = d.stockAvailable === true || d.stockAvailable === 'true' || d.stockAvailable === 1 || d.stockAvailable === '1';
|
|
sets.push('STOCK_AVAILABLE=?'); vals.push(yes);
|
|
if (yes) {
|
|
const batches = d.batchNos || {};
|
|
if (s.items.some(it => !String(batches[it.id] || '').trim())) fail('Enter the batch no. for every item');
|
|
to = SAMPLE.QA_PENDING; sets.push('QA_DUE_DATE=?'); vals.push(addDays(new Date(), 2));
|
|
await tx(async (r) => { for (const it of s.items) await query(`UPDATE dbo.ZSO_SAMPLE_ITEMS SET BATCH_NO=? WHERE ID=?`, [String(batches[it.id]).trim(), it.id], r); });
|
|
notifyArgs = ['Sample — QA verification pending', [`Sample ${s.refNo}: stock available, QA verification due in 2 days.`], null];
|
|
} else { to = SAMPLE.IN_PRODUCTION; sets.push('OUT_OF_STOCK_DATE=?'); vals.push(d.outOfStockDate || new Date()); }
|
|
break;
|
|
}
|
|
case 'qa_extend': sets.push('QA_DUE_DATE=?'); vals.push(addDays(new Date(), 2)); break;
|
|
case 'dispatch':
|
|
if (!String(d.challanNo || '').trim() || !d.challanDate || !String(d.courierNo || '').trim()) fail('Challan no., challan date and courier no. are required');
|
|
to = SAMPLE.SHIPPED; sets.push('QA_VERIFIED=1', 'CHALLAN_NO=?', 'CHALLAN_DATE=?', 'COURIER_NO=?', 'DISPATCH_INFO=?'); vals.push(d.challanNo, d.challanDate, d.courierNo, d.dispatchInfo || '');
|
|
notifyArgs = ['Sample shipped — feedback awaited', [`Sample ${s.refNo} for ${s.cardName} shipped. Courier: ${d.courierNo}.`], null]; break;
|
|
case 'feedback': if (!String(d.feedback || '').trim()) fail('Feedback is required');
|
|
to = SAMPLE.COMPLETED; sets.push('FEEDBACK=?'); vals.push(d.feedback); break;
|
|
default: fail('Unknown action');
|
|
}
|
|
await tx(async (r) => {
|
|
await query(`UPDATE dbo.ZSO_SAMPLES SET STATUS=?, ${sets.length ? sets.join(', ') + ',' : ''} UPDATED_AT=SYSDATETIME(), UPDATED_BY_NAME=? WHERE ID=?`, [to, ...vals, actor.name, s.id], r);
|
|
await log('sample', s.id, action, s.status, to, d.internalRemarks || d.modificationRemarks || d.rejectionRemarks || d.feedback || d.dispatchInfo || '', actor, r);
|
|
});
|
|
const creator = await one(`SELECT EMAIL FROM dbo.ZCUST_USERS WHERE ID=?`, [s.createdBy]).catch(() => null);
|
|
if (notifyArgs) notify.sampleEvent('domestic', notifyArgs[0], notifyArgs[1], notifyArgs[2], s.divisionId, creator && creator.EMAIL ? [creator.EMAIL] : []);
|
|
else if (creator && creator.EMAIL) notify.sampleEvent('domestic', `Sample ${s.refNo}: ${sampleStatusLabel(to)}`, [`Sample request ${s.refNo} is now "${sampleStatusLabel(to)}".`], null, s.divisionId, [creator.EMAIL]);
|
|
return getSample(actor, s.id);
|
|
}
|
|
|
|
// ── Export ──────────────────────────────────────────────────────────────────
|
|
function eFromRow(r) {
|
|
return { id: r.ID, refNo: r.REF_NO, region: r.REGION || '', sampleAgainst: r.SAMPLE_AGAINST || '', custName: r.CUST_NAME, country: r.COUNTRY || '',
|
|
shippingAddress: r.SHIPPING_ADDRESS || '', contactPerson: r.CONTACT_PERSON || '', contactEmail: r.CONTACT_EMAIL || '', contactNo: r.CONTACT_NO || '',
|
|
accountDetails: r.ACCOUNT_DETAILS || '', courierByCustomer: !!r.COURIER_BY_CUSTOMER, divisionId: r.DIVISION_ID, reqDate: r.REQ_DATE, deliveryDate: r.DELIVERY_DATE,
|
|
deliveryDays: r.DELIVERY_DAYS, remarksQa: r.REMARKS_QA || '', remarksLogistics: r.REMARKS_LOGISTICS || '', status: r.STATUS, statusLabel: exportSampleStatusLabel(r.STATUS),
|
|
stockAvailable: r.STOCK_AVAILABLE == null ? null : !!r.STOCK_AVAILABLE, qaDueDate: r.QA_DUE_DATE, qaVerified: r.QA_VERIFIED == null ? null : !!r.QA_VERIFIED,
|
|
challanNo: r.CHALLAN_NO || '', challanDate: r.CHALLAN_DATE, courierNo: r.COURIER_NO || '', dispatchDetails: r.DISPATCH_DETAILS || '',
|
|
createdBy: r.CREATED_BY, createdByName: r.CREATED_BY_NAME || '', createdAt: r.CREATED_AT, updatedAt: r.UPDATED_AT, legacy: !!r.LEGACY };
|
|
}
|
|
async function listExport(actor, f = {}) {
|
|
if (!canE(actor, 'view', 'view')) fail('You are not assigned "view" on Export Sample Requests', 403);
|
|
const w = ['1=1']; const p = [];
|
|
if (f.statuses) { const st = String(f.statuses).split(',').map(Number).filter(Boolean); if (st.length) w.push(`STATUS IN (${st.join(',')})`); }
|
|
if (f.search) { w.push('(REF_NO LIKE ? OR CUST_NAME LIKE ? OR COUNTRY LIKE ?)'); const s = `%${f.search}%`; p.push(s, s, s); }
|
|
return (await query(`SELECT TOP 2000 * FROM dbo.ZSO_EXPORT_SAMPLES WHERE ${w.join(' AND ')} ORDER BY ID DESC`, p)).map(eFromRow);
|
|
}
|
|
async function getExport(actor, id) {
|
|
if (!canE(actor, 'view', 'view')) fail('Not found', 404);
|
|
const r = await one(`SELECT * FROM dbo.ZSO_EXPORT_SAMPLES WHERE ID=?`, [parseInt(id)]);
|
|
if (!r) fail('Export sample request not found', 404);
|
|
const e = eFromRow(r);
|
|
e.items = await itemsFor(e.id, 'export');
|
|
e.docs = await getDocs('export_sample', e.id);
|
|
e.log = (await query(`SELECT * FROM dbo.ZSO_LOG WHERE ENTITY='export_sample' AND ENTITY_ID=? ORDER BY ID`, [e.id])).map(l => ({ action: l.ACTION, fromStatus: l.FROM_STATUS, toStatus: l.TO_STATUS, remarks: l.REMARKS || '', actorName: l.ACTOR_NAME || '', at: l.AT }));
|
|
e.actions = [];
|
|
if (canE(actor, 'logistics', 'approve') && [EXPORT_SAMPLE.SUBMITTED, EXPORT_SAMPLE.IN_PRODUCTION].includes(e.status)) e.actions.push('stock');
|
|
if (canE(actor, 'logistics', 'approve') && e.status === EXPORT_SAMPLE.QA_PENDING) e.actions.push('dispatch');
|
|
if (canE(actor, 'create', 'edit') && e.status === EXPORT_SAMPLE.SUBMITTED) e.actions.push('edit');
|
|
return e;
|
|
}
|
|
const EXPORT_FIELDS = ['region', 'sampleAgainst', 'custName', 'country', 'shippingAddress', 'contactPerson', 'contactEmail', 'contactNo', 'accountDetails'];
|
|
async function saveExport(actor, d) {
|
|
const editing = !!d.id;
|
|
if (!canE(actor, 'create', editing ? 'edit' : 'add')) fail(`You are not assigned "${editing ? 'edit' : 'add'}" on sales_export_sample:create`, 403);
|
|
for (const f of ['custName', 'country', 'shippingAddress', 'contactPerson', 'contactEmail', 'contactNo']) if (!String(d[f] || '').trim()) fail(`${f} is required`);
|
|
const items = await priceSampleLines(d.lines, null, 'export');
|
|
const vals = [...EXPORT_FIELDS.map(f => d[f] || ''), !!d.courierByCustomer, d.reqDate || new Date(), parseInt(d.deliveryDays) || null, d.remarksQa || '', d.remarksLogistics || ''];
|
|
return tx(async (r) => {
|
|
let id;
|
|
if (editing) {
|
|
const cur = await one(`SELECT STATUS FROM dbo.ZSO_EXPORT_SAMPLES WHERE ID=?`, [parseInt(d.id)], r);
|
|
if (!cur || cur.STATUS !== EXPORT_SAMPLE.SUBMITTED) fail('Only submitted requests can be edited');
|
|
await query(`UPDATE dbo.ZSO_EXPORT_SAMPLES SET REGION=?, SAMPLE_AGAINST=?, CUST_NAME=?, COUNTRY=?, SHIPPING_ADDRESS=?, CONTACT_PERSON=?, CONTACT_EMAIL=?, CONTACT_NO=?,
|
|
ACCOUNT_DETAILS=?, COURIER_BY_CUSTOMER=?, REQ_DATE=?, DELIVERY_DAYS=?, REMARKS_QA=?, REMARKS_LOGISTICS=?, UPDATED_AT=SYSDATETIME(), UPDATED_BY_NAME=? WHERE ID=?`,
|
|
[...vals, actor.name, parseInt(d.id)], r);
|
|
id = parseInt(d.id);
|
|
await log('export_sample', id, 'edited', 1, 1, '', actor, r);
|
|
} else {
|
|
const row = await one(`INSERT INTO dbo.ZSO_EXPORT_SAMPLES (REGION,SAMPLE_AGAINST,CUST_NAME,COUNTRY,SHIPPING_ADDRESS,CONTACT_PERSON,CONTACT_EMAIL,CONTACT_NO,ACCOUNT_DETAILS,
|
|
COURIER_BY_CUSTOMER,REQ_DATE,DELIVERY_DAYS,REMARKS_QA,REMARKS_LOGISTICS,STATUS,CREATED_BY,CREATED_BY_NAME) OUTPUT INSERTED.ID, INSERTED.CREATED_AT
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)`, [...vals, parseInt(actor.user.id), actor.name], r);
|
|
id = row.ID;
|
|
await query(`UPDATE dbo.ZSO_EXPORT_SAMPLES SET REF_NO=? WHERE ID=?`, [refNo(id, row.CREATED_AT), id], r);
|
|
await log('export_sample', id, 'created', null, 1, '', actor, r);
|
|
}
|
|
await writeItems(id, 'export', items, r);
|
|
if (!editing) notify.sampleEvent('export', 'Export sample request submitted', [`${actor.name} raised an export sample request for ${d.custName} (${d.country}).`], 'sales_export_sample:logistics', null);
|
|
return id;
|
|
});
|
|
}
|
|
// stock {stockAvailable, batchNos, deliveryDate} · dispatch {challanNo, challanDate, courierNo, dispatchDetails}
|
|
async function exportAct(actor, id, action, d = {}) {
|
|
const e = await getExport(actor, id);
|
|
if (!e.actions.includes(action)) fail('This action is not available for you at the current status', 403);
|
|
let to = e.status; const sets = []; const vals = [];
|
|
if (action === 'stock') {
|
|
const yes = d.stockAvailable === true || d.stockAvailable === 'true' || d.stockAvailable === 1 || d.stockAvailable === '1';
|
|
sets.push('STOCK_AVAILABLE=?', 'DELIVERY_DATE=?'); vals.push(yes, d.deliveryDate || null);
|
|
if (yes) {
|
|
const b = d.batchNos || {};
|
|
if (e.items.some(it => !String(b[it.id] || '').trim())) fail('Enter the batch no. for every item');
|
|
to = EXPORT_SAMPLE.QA_PENDING; sets.push('QA_DUE_DATE=?'); vals.push(addDays(new Date(), 2));
|
|
await tx(async (r) => { for (const it of e.items) await query(`UPDATE dbo.ZSO_SAMPLE_ITEMS SET BATCH_NO=? WHERE ID=?`, [String(b[it.id]).trim(), it.id], r); });
|
|
} else to = EXPORT_SAMPLE.IN_PRODUCTION;
|
|
} else if (action === 'dispatch') {
|
|
if (!String(d.courierNo || '').trim()) fail('Courier / AWB no. is required');
|
|
to = EXPORT_SAMPLE.DISPATCHED; sets.push('QA_VERIFIED=1', 'CHALLAN_NO=?', 'CHALLAN_DATE=?', 'COURIER_NO=?', 'DISPATCH_DETAILS=?');
|
|
vals.push(d.challanNo || '', d.challanDate || null, d.courierNo, d.dispatchDetails || '');
|
|
} else fail('Unknown action');
|
|
await tx(async (r) => {
|
|
await query(`UPDATE dbo.ZSO_EXPORT_SAMPLES SET STATUS=?, ${sets.join(', ')}, UPDATED_AT=SYSDATETIME(), UPDATED_BY_NAME=? WHERE ID=?`, [to, ...vals, actor.name, e.id], r);
|
|
await log('export_sample', e.id, action, e.status, to, d.dispatchDetails || '', actor, r);
|
|
});
|
|
const creator = await one(`SELECT EMAIL FROM dbo.ZCUST_USERS WHERE ID=?`, [e.createdBy]).catch(() => null);
|
|
if (creator && creator.EMAIL) notify.sampleEvent('export', `Export sample ${e.refNo}: ${exportSampleStatusLabel(to)}`, [`Export sample ${e.refNo} for ${e.custName} is now "${exportSampleStatusLabel(to)}".`], null, null, [creator.EMAIL]);
|
|
return getExport(actor, e.id);
|
|
}
|
|
|
|
module.exports = { listSamples, getSample, createSample, sampleAct, listExport, getExport, saveExport, exportAct };
|