Add supply purchase order workflow

This commit is contained in:
Cauê Faleiros
2026-08-25 11:10:36 -03:00
parent 91f3e5e00c
commit 21d9960145
6 changed files with 223 additions and 4 deletions

View File

@@ -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
};