Add supply purchase order workflow
This commit is contained in:
@@ -490,6 +490,33 @@ const initDB = async () => {
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS supply_purchase_orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
number VARCHAR(40) UNIQUE,
|
||||
supplier TEXT NOT NULL,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'draft',
|
||||
expected_date DATE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS supply_purchase_order_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
purchase_order_id INTEGER NOT NULL REFERENCES supply_purchase_orders(id) ON DELETE CASCADE,
|
||||
material TEXT NOT NULL,
|
||||
quantity NUMERIC(14, 4) NOT NULL CHECK (quantity > 0),
|
||||
unit VARCHAR(30) NOT NULL DEFAULT 'kg',
|
||||
unit_cost NUMERIC(14, 4),
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_purchase_orders_status ON supply_purchase_orders (status, created_at DESC);`);
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_orders
|
||||
ADD COLUMN IF NOT EXISTS notes TEXT,
|
||||
|
||||
@@ -5,13 +5,16 @@ const {
|
||||
approveReceipt,
|
||||
consumeLotForProduction,
|
||||
createFabricPlan,
|
||||
createPurchaseOrder,
|
||||
createReceipt,
|
||||
deleteFabricPlan,
|
||||
deleteReceipt,
|
||||
getSupplySummary,
|
||||
listFabricPlans,
|
||||
listKnownSuppliers,
|
||||
listPurchaseOrders,
|
||||
saveMaterialDetails,
|
||||
updatePurchaseOrderStatus,
|
||||
listLots,
|
||||
listMovements,
|
||||
listPurchaseNeeds,
|
||||
@@ -36,6 +39,30 @@ router.get('/supply/suppliers', verifyToken, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/supply/purchase-orders', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await listPurchaseOrders());
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/supply/purchase-orders', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.status(201).json(await createPurchaseOrder(req.body || {}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/supply/purchase-orders/:id/status', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await updatePurchaseOrderStatus(req.params.id, req.body?.status));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/supply/receipts', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await listReceipts());
|
||||
|
||||
@@ -101,6 +101,27 @@ const mapMaterialDetails = (row) => ({
|
||||
updatedAt: row.updated_at
|
||||
});
|
||||
|
||||
const PURCHASE_ORDER_STATUSES = new Set(['draft', 'sent', 'approved', 'received', 'canceled']);
|
||||
|
||||
const mapPurchaseOrder = (row) => ({
|
||||
id: row.id,
|
||||
number: row.number || '',
|
||||
supplier: row.supplier || '',
|
||||
status: row.status || 'draft',
|
||||
expectedDate: row.expected_date ? String(row.expected_date).slice(0, 10) : null,
|
||||
notes: row.notes || '',
|
||||
total: Number(row.total || 0),
|
||||
items: Array.isArray(row.items) ? row.items.map(item => ({
|
||||
id: item.id,
|
||||
material: item.material || '',
|
||||
quantity: Number(item.quantity || 0),
|
||||
unit: item.unit || 'kg',
|
||||
unitCost: item.unit_cost === null || item.unit_cost === undefined ? null : Number(item.unit_cost)
|
||||
})) : [],
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
});
|
||||
|
||||
const createValidationError = (message) => {
|
||||
const error = new Error(message);
|
||||
error.statusCode = 400;
|
||||
@@ -189,6 +210,63 @@ const listKnownSuppliers = async () => {
|
||||
return result.rows.map(row => row.supplier);
|
||||
};
|
||||
|
||||
const listPurchaseOrders = async () => {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
purchase_order.id, purchase_order.number, purchase_order.supplier, purchase_order.status,
|
||||
purchase_order.expected_date, purchase_order.notes, purchase_order.created_at, purchase_order.updated_at,
|
||||
COALESCE((SELECT SUM(item.quantity * COALESCE(item.unit_cost, 0)) FROM supply_purchase_order_items item WHERE item.purchase_order_id = purchase_order.id), 0) AS total,
|
||||
COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT('id', item.id, 'material', item.material, 'quantity', item.quantity, 'unit', item.unit, 'unit_cost', item.unit_cost) ORDER BY item.id) FROM supply_purchase_order_items item WHERE item.purchase_order_id = purchase_order.id), '[]'::json) AS items
|
||||
FROM supply_purchase_orders purchase_order
|
||||
ORDER BY purchase_order.created_at DESC, purchase_order.id DESC;
|
||||
`);
|
||||
return result.rows.map(mapPurchaseOrder);
|
||||
};
|
||||
|
||||
const createPurchaseOrder = async (payload = {}) => {
|
||||
const supplier = normalizeText(payload.supplier);
|
||||
const expectedDate = /^\d{4}-\d{2}-\d{2}$/.test(normalizeText(payload.expectedDate)) ? normalizeText(payload.expectedDate) : null;
|
||||
const notes = normalizeText(payload.notes);
|
||||
const items = (Array.isArray(payload.items) ? payload.items : []).map(item => ({
|
||||
material: normalizeText(item?.material),
|
||||
quantity: normalizeNumber(item?.quantity),
|
||||
unit: normalizeText(item?.unit) || 'kg',
|
||||
unitCost: normalizeNonNegativeNumber(item?.unitCost)
|
||||
})).filter(item => item.material && item.quantity);
|
||||
if (!supplier) throw createValidationError('Fornecedor é obrigatório para a ordem de compra.');
|
||||
if (!items.length) throw createValidationError('Inclua pelo menos um material na ordem de compra.');
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const orderResult = await client.query(`INSERT INTO supply_purchase_orders (supplier, status, expected_date, notes, updated_at) VALUES ($1, 'draft', $2, NULLIF($3, ''), CURRENT_TIMESTAMP) RETURNING id;`, [supplier, expectedDate, notes]);
|
||||
const id = orderResult.rows[0].id;
|
||||
await client.query(`UPDATE supply_purchase_orders SET number = $1 WHERE id = $2;`, [`OC-${String(id).padStart(5, '0')}`, id]);
|
||||
for (const item of items) {
|
||||
await client.query(`INSERT INTO supply_purchase_order_items (purchase_order_id, material, quantity, unit, unit_cost) VALUES ($1, $2, $3, $4, $5);`, [id, item.material, item.quantity, item.unit, item.unitCost]);
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
return (await listPurchaseOrders()).find(order => order.id === id);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
const updatePurchaseOrderStatus = async (id, status) => {
|
||||
const normalizedStatus = normalizeText(status).toLowerCase();
|
||||
if (!PURCHASE_ORDER_STATUSES.has(normalizedStatus)) throw createValidationError('Status de ordem de compra inválido.');
|
||||
const result = await pool.query(`UPDATE supply_purchase_orders SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING id;`, [normalizedStatus, id]);
|
||||
if (!result.rowCount) {
|
||||
const error = new Error('Ordem de compra não encontrada.');
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
return (await listPurchaseOrders()).find(order => order.id === Number(id));
|
||||
};
|
||||
|
||||
const saveMaterialDetails = async (materialKey, payload) => {
|
||||
const normalizedKey = normalizeKey(materialKey);
|
||||
if (!normalizedKey) throw createValidationError('Material inválido para edição.');
|
||||
@@ -1161,6 +1239,7 @@ module.exports = {
|
||||
approveReceipt,
|
||||
consumeLotForProduction,
|
||||
createFabricPlan,
|
||||
createPurchaseOrder,
|
||||
createReceipt,
|
||||
deleteFabricPlan,
|
||||
deleteReceipt,
|
||||
@@ -1170,9 +1249,11 @@ module.exports = {
|
||||
listFabricPlans,
|
||||
listMaterialDetails,
|
||||
listKnownSuppliers,
|
||||
listPurchaseOrders,
|
||||
listLots,
|
||||
listMovements,
|
||||
listPurchaseNeeds,
|
||||
listReceipts,
|
||||
saveMaterialDetails
|
||||
saveMaterialDetails,
|
||||
updatePurchaseOrderStatus
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user