diff --git a/backend/db.js b/backend/db.js index d891657..4fee67a 100644 --- a/backend/db.js +++ b/backend/db.js @@ -626,6 +626,11 @@ const initDB = async () => { 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 supply_receipts + ADD COLUMN IF NOT EXISTS purchase_order_id INTEGER REFERENCES supply_purchase_orders(id) ON DELETE SET NULL; + `).catch(() => {}); + await pool.query(` ALTER TABLE production_orders ADD COLUMN IF NOT EXISTS notes TEXT, diff --git a/backend/services/supplyService.js b/backend/services/supplyService.js index 39055e1..1e7d9a4 100644 --- a/backend/services/supplyService.js +++ b/backend/services/supplyService.js @@ -46,6 +46,9 @@ const mapReceipt = (row) => ({ invoice: row.invoice || '', notes: row.notes || '', status: row.status, + purchaseOrderId: row.purchase_order_id === null || row.purchase_order_id === undefined ? null : Number(row.purchase_order_id), + purchaseOrderNumber: row.purchase_order_number || '', + purchaseOrderStatus: row.purchase_order_status || '', createdAt: row.created_at, updatedAt: row.updated_at, approvedAt: row.approved_at @@ -119,7 +122,7 @@ const mapMaterialDetails = (row) => ({ updatedAt: row.updated_at }); -const PURCHASE_ORDER_STATUSES = new Set(['draft', 'sent', 'approved', 'received', 'canceled']); +const PURCHASE_ORDER_STATUSES = new Set(['draft', 'pending_financial', 'sent', 'approved', 'received', 'canceled']); const mapPurchaseOrder = (row) => ({ id: row.id, @@ -148,9 +151,12 @@ const createValidationError = (message) => { const listReceipts = async () => { const result = await pool.query(` - SELECT id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at - FROM supply_receipts - ORDER BY created_at DESC, id DESC + SELECT receipt.id, receipt.category, receipt.product, receipt.quantity, receipt.unit, receipt.supplier, receipt.invoice, + receipt.notes, receipt.status, receipt.purchase_order_id, receipt.created_at, receipt.updated_at, receipt.approved_at, + purchase_order.number AS purchase_order_number, purchase_order.status AS purchase_order_status + FROM supply_receipts receipt + LEFT JOIN supply_purchase_orders purchase_order ON purchase_order.id = receipt.purchase_order_id + ORDER BY receipt.created_at DESC, receipt.id DESC `); return result.rows.map(mapReceipt); }; @@ -930,13 +936,16 @@ const createReceipt = async (payload) => { if (!product) throw createValidationError('Produto ou material é obrigatório.'); if (!quantity) throw createValidationError('Quantidade deve ser maior que zero.'); - const result = await pool.query(` + const client = await pool.connect(); + try { + await client.query('BEGIN'); + const result = await client.query(` INSERT INTO supply_receipts ( category, product, quantity, unit, supplier, invoice, notes, status, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', CURRENT_TIMESTAMP) RETURNING id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at - `, [ + `, [ category, product, quantity, @@ -944,9 +953,31 @@ const createReceipt = async (payload) => { normalizeText(payload.supplier) || null, normalizeText(payload.invoice) || null, normalizeText(payload.notes) || null - ]); - - return mapReceipt(result.rows[0]); + ]); + const receipt = result.rows[0]; + const orderResult = await client.query(` + INSERT INTO supply_purchase_orders (supplier, status, notes, updated_at) + VALUES ($1, 'pending_financial', $2, CURRENT_TIMESTAMP) + RETURNING id; + `, [ + normalizeText(payload.supplier) || 'Sem fornecedor', + `Gerada automaticamente pelo recebimento #${receipt.id}${receipt.invoice ? ` · NF ${receipt.invoice}` : ''}` + ]); + const purchaseOrderId = orderResult.rows[0].id; + await client.query(`UPDATE supply_purchase_orders SET number = $1 WHERE id = $2;`, [`OC-${String(purchaseOrderId).padStart(5, '0')}`, purchaseOrderId]); + await client.query(`INSERT INTO supply_purchase_order_items (purchase_order_id, material, quantity, unit) VALUES ($1, $2, $3, $4);`, [purchaseOrderId, receipt.product, receipt.quantity, receipt.unit]); + const linkedReceipt = await client.query(` + UPDATE supply_receipts SET purchase_order_id = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 + RETURNING id, category, product, quantity, unit, supplier, invoice, notes, status, purchase_order_id, created_at, updated_at, approved_at; + `, [receipt.id, purchaseOrderId]); + await client.query('COMMIT'); + return mapReceipt({ ...linkedReceipt.rows[0], purchase_order_number: `OC-${String(purchaseOrderId).padStart(5, '0')}`, purchase_order_status: 'pending_financial' }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } }; const approveReceipt = async (id) => { @@ -956,10 +987,13 @@ const approveReceipt = async (id) => { await client.query('BEGIN'); const receiptResult = await client.query(` - SELECT id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at - FROM supply_receipts - WHERE id = $1 - FOR UPDATE + SELECT receipt.id, receipt.category, receipt.product, receipt.quantity, receipt.unit, receipt.supplier, receipt.invoice, + receipt.notes, receipt.status, receipt.purchase_order_id, receipt.created_at, receipt.updated_at, receipt.approved_at, + purchase_order.number AS purchase_order_number, purchase_order.status AS purchase_order_status + FROM supply_receipts receipt + LEFT JOIN supply_purchase_orders purchase_order ON purchase_order.id = receipt.purchase_order_id + WHERE receipt.id = $1 + FOR UPDATE OF receipt `, [id]); if (!receiptResult.rowCount) { @@ -972,13 +1006,17 @@ const approveReceipt = async (id) => { return mapReceipt(receipt); } + if (receipt.purchase_order_id && !['approved', 'received'].includes(receipt.purchase_order_status)) { + throw createValidationError(`A ${receipt.purchase_order_number || 'OC vinculada'} ainda aguarda aprovação financeira.`); + } + const updatedReceiptResult = await client.query(` UPDATE supply_receipts SET status = 'approved', approved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1 - RETURNING id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at + RETURNING id, category, product, quantity, unit, supplier, invoice, notes, status, purchase_order_id, created_at, updated_at, approved_at `, [id]); const lotResult = await client.query(` @@ -1012,8 +1050,12 @@ const approveReceipt = async (id) => { `Recebimento aprovado${receipt.invoice ? ` · NF ${receipt.invoice}` : ''}` ]); + if (receipt.purchase_order_id) { + await client.query(`UPDATE supply_purchase_orders SET status = 'received', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND status = 'approved';`, [receipt.purchase_order_id]); + } + await client.query('COMMIT'); - return mapReceipt(updatedReceiptResult.rows[0]); + return mapReceipt({ ...updatedReceiptResult.rows[0], purchase_order_number: receipt.purchase_order_number, purchase_order_status: receipt.purchase_order_id ? 'received' : '' }); } catch (error) { await client.query('ROLLBACK'); throw error; diff --git a/src/pages/Supplies.tsx b/src/pages/Supplies.tsx index 595b05d..b2db5e9 100644 --- a/src/pages/Supplies.tsx +++ b/src/pages/Supplies.tsx @@ -1063,7 +1063,7 @@ const ReceiptList = ({

{receipt.category} · {receipt.invoice ? `NF ${receipt.invoice}` : 'sem NF'} · {formatDateTime(receipt.createdAt)}

{formatNumber(receipt.quantity)} {receipt.unit}

-

{receipt.supplier}

+

{receipt.supplier}

{receipt.purchaseOrderNumber && {receipt.purchaseOrderNumber} · {receipt.purchaseOrderStatus === 'pending_financial' ? 'aguarda financeiro' : receipt.purchaseOrderStatus === 'approved' ? 'aprovada' : receipt.purchaseOrderStatus === 'received' ? 'recebida' : receipt.purchaseOrderStatus}}
- {receipt.status === 'pending' && ( - - )} + {receipt.status === 'pending' && (receipt.purchaseOrderStatus === 'approved' || receipt.purchaseOrderStatus === 'received') && } @@ -1163,7 +1159,7 @@ const ReceiptsTab = ({

Registrar recebimento

- Preencha o que chegou. O financeiro vincula OC/NF e aprova o lançamento. + Ao registrar, o Graphs cria uma OC interna pendente para este recebimento. O material só entra no estoque após a aprovação financeira dessa OC.

1. Qual categoria de produto chegou?

@@ -1193,7 +1189,7 @@ const ReceiptsTab = ({