Add production execution workflow
This commit is contained in:
@@ -488,6 +488,14 @@ const initDB = async () => {
|
||||
ADD COLUMN IF NOT EXISTS completed_date DATE;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_orders
|
||||
ADD COLUMN IF NOT EXISTS actual_cut_quantity NUMERIC(14, 4),
|
||||
ADD COLUMN IF NOT EXISTS rejected_quantity NUMERIC(14, 4) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS finished_quantity NUMERIC(14, 4),
|
||||
ADD COLUMN IF NOT EXISTS quality_notes TEXT;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS production_order_import_runs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const { authenticateAPIKey, verifyToken } = require('../auth');
|
||||
const { createProductionOrders, importTinyProductCompositionExport, listProductionOrders, updateProductionOrderStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
|
||||
const { createProductionOrders, importTinyProductCompositionExport, listProductionOrders, updateProductionOrderExecution, updateProductionOrderStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -50,4 +50,13 @@ router.patch('/production-orders/:id/status', verifyToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/production-orders/:id/execution', verifyToken, async (req, res) => {
|
||||
try {
|
||||
res.json(await updateProductionOrderExecution(req.params.id, req.body || {}));
|
||||
} catch (error) {
|
||||
console.error('Error updating production order execution:', error);
|
||||
res.status(error.statusCode || 500).json({ error: error.message || 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,7 +2,9 @@ const { pool } = require('../db');
|
||||
|
||||
const STATUS_LABELS = {
|
||||
open: 'Em aberto',
|
||||
in_progress: 'Em andamento',
|
||||
cutting: 'Em corte',
|
||||
sewing: 'Em costura',
|
||||
review: 'Em revisão',
|
||||
finished: 'Finalizada',
|
||||
canceled: 'Cancelada'
|
||||
};
|
||||
@@ -10,7 +12,9 @@ const STATUS_LABELS = {
|
||||
const normalizeStatus = (status) => {
|
||||
const normalizedStatus = String(status || 'open').trim().toLowerCase();
|
||||
if (['open', 'em_aberto', 'em aberto', 'aberta'].includes(normalizedStatus)) return 'open';
|
||||
if (['in_progress', 'andamento', 'em andamento'].includes(normalizedStatus)) return 'in_progress';
|
||||
if (['in_progress', 'andamento', 'em andamento', 'em corte', 'corte', 'cutting'].includes(normalizedStatus)) return 'cutting';
|
||||
if (['em costura', 'costura', 'sewing'].includes(normalizedStatus)) return 'sewing';
|
||||
if (['em revisao', 'em revisão', 'revisao', 'revisão', 'review'].includes(normalizedStatus)) return 'review';
|
||||
if (['finished', 'finalizada', 'finalizado'].includes(normalizedStatus)) return 'finished';
|
||||
if (['canceled', 'cancelada', 'cancelado', 'cancelled'].includes(normalizedStatus)) return 'canceled';
|
||||
return normalizedStatus || 'open';
|
||||
@@ -60,6 +64,15 @@ const normalizeNullableQuantity = (value) => {
|
||||
return quantity || null;
|
||||
};
|
||||
|
||||
const normalizeNonNegativeQuantity = (value) => {
|
||||
if (value === '' || value === null || value === undefined) return null;
|
||||
const normalizedValue = typeof value === 'string' && value.includes(',')
|
||||
? value.replace(/\./g, '').replace(',', '.')
|
||||
: value;
|
||||
const quantity = Number(normalizedValue);
|
||||
return Number.isFinite(quantity) && quantity >= 0 ? quantity : null;
|
||||
};
|
||||
|
||||
const normalizeInteger = (value) => {
|
||||
const number = Number(value);
|
||||
return Number.isInteger(number) ? number : null;
|
||||
@@ -267,6 +280,10 @@ const mapProductionOrderRow = (row) => {
|
||||
fabricKg: row.fabric_kg === null ? null : Number(row.fabric_kg),
|
||||
ribKg: row.rib_kg === null ? null : Number(row.rib_kg),
|
||||
yieldPiecesPerKg: row.yield_pieces_per_kg === null ? null : Number(row.yield_pieces_per_kg),
|
||||
actualCutQuantity: row.actual_cut_quantity === null ? null : Number(row.actual_cut_quantity),
|
||||
rejectedQuantity: Number(row.rejected_quantity || 0),
|
||||
finishedQuantity: row.finished_quantity === null ? null : Number(row.finished_quantity),
|
||||
qualityNotes: row.quality_notes || '',
|
||||
markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [],
|
||||
components: Array.isArray(row.components) ? row.components.map(mapComponent) : [],
|
||||
steps: Array.isArray(row.steps) ? row.steps.map(mapStep) : [],
|
||||
@@ -298,6 +315,10 @@ const getOrderById = async (id, client = pool) => {
|
||||
po.fabric_kg,
|
||||
po.rib_kg,
|
||||
po.yield_pieces_per_kg,
|
||||
po.actual_cut_quantity,
|
||||
po.rejected_quantity,
|
||||
po.finished_quantity,
|
||||
po.quality_notes,
|
||||
po.created_at,
|
||||
po.updated_at,
|
||||
COALESCE(
|
||||
@@ -381,6 +402,10 @@ const baseProductionOrderSelect = `
|
||||
po.fabric_kg,
|
||||
po.rib_kg,
|
||||
po.yield_pieces_per_kg,
|
||||
po.actual_cut_quantity,
|
||||
po.rejected_quantity,
|
||||
po.finished_quantity,
|
||||
po.quality_notes,
|
||||
po.created_at,
|
||||
po.updated_at,
|
||||
COALESCE(
|
||||
@@ -482,7 +507,9 @@ const listProductionOrders = async (filters = {}) => {
|
||||
}, {
|
||||
all: 0,
|
||||
open: 0,
|
||||
in_progress: 0,
|
||||
cutting: 0,
|
||||
sewing: 0,
|
||||
review: 0,
|
||||
finished: 0,
|
||||
canceled: 0
|
||||
});
|
||||
@@ -514,7 +541,7 @@ const createProductionOrders = async (orders = []) => {
|
||||
FROM production_orders
|
||||
WHERE COALESCE(order_reference, '') = $1
|
||||
AND COALESCE(product_sku, '') = $2
|
||||
AND status IN ('open', 'in_progress')
|
||||
AND status IN ('open', 'in_progress', 'cutting', 'sewing', 'review')
|
||||
LIMIT 1;
|
||||
`, [orderReference, productSku]);
|
||||
|
||||
@@ -1304,7 +1331,13 @@ const updateProductionOrderStatus = async (id, status) => {
|
||||
|
||||
const result = await pool.query(`
|
||||
UPDATE production_orders
|
||||
SET status = $1, updated_at = CURRENT_TIMESTAMP
|
||||
SET status = $1,
|
||||
completed_date = CASE
|
||||
WHEN $1 = 'finished' THEN COALESCE(completed_date, CURRENT_DATE)
|
||||
WHEN status = 'finished' THEN NULL
|
||||
ELSE completed_date
|
||||
END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
RETURNING id;
|
||||
`, [normalizedStatus, id]);
|
||||
@@ -1318,6 +1351,58 @@ const updateProductionOrderStatus = async (id, status) => {
|
||||
return getOrderById(id);
|
||||
};
|
||||
|
||||
const updateProductionOrderExecution = async (id, payload = {}) => {
|
||||
const actualCutQuantity = normalizeNonNegativeQuantity(payload.actualCutQuantity);
|
||||
const rejectedQuantity = normalizeNonNegativeQuantity(payload.rejectedQuantity);
|
||||
const finishedQuantity = normalizeNonNegativeQuantity(payload.finishedQuantity);
|
||||
const qualityNotes = normalizeText(payload.qualityNotes);
|
||||
|
||||
if (actualCutQuantity === null && rejectedQuantity === null && finishedQuantity === null && !qualityNotes) {
|
||||
const error = new Error('Informe uma quantidade de execução ou uma observação de qualidade.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const currentResult = await pool.query(`
|
||||
SELECT actual_cut_quantity, rejected_quantity, finished_quantity
|
||||
FROM production_orders
|
||||
WHERE id = $1;
|
||||
`, [id]);
|
||||
if (!currentResult.rowCount) {
|
||||
const error = new Error('Production order not found');
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const current = currentResult.rows[0];
|
||||
const effectiveActual = actualCutQuantity === null ? (current.actual_cut_quantity === null ? null : Number(current.actual_cut_quantity)) : actualCutQuantity;
|
||||
const effectiveRejected = rejectedQuantity === null ? Number(current.rejected_quantity || 0) : rejectedQuantity;
|
||||
const effectiveFinished = finishedQuantity === null ? (current.finished_quantity === null ? null : Number(current.finished_quantity)) : finishedQuantity;
|
||||
|
||||
if (effectiveActual !== null && effectiveRejected > effectiveActual) {
|
||||
const error = new Error('As perdas não podem ser maiores que a quantidade cortada.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
if (effectiveActual !== null && effectiveFinished !== null && effectiveFinished > effectiveActual - effectiveRejected) {
|
||||
const error = new Error('A quantidade final não pode ser maior que o saldo após as perdas.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
await pool.query(`
|
||||
UPDATE production_orders
|
||||
SET actual_cut_quantity = $1,
|
||||
rejected_quantity = $2,
|
||||
finished_quantity = $3,
|
||||
quality_notes = NULLIF($4, ''),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $5;
|
||||
`, [effectiveActual, effectiveRejected, effectiveFinished, qualityNotes, id]);
|
||||
|
||||
return getOrderById(id);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createProductionOrders,
|
||||
deleteManualProductComposition,
|
||||
@@ -1329,5 +1414,6 @@ module.exports = {
|
||||
saveManualProductComposition,
|
||||
upsertTinyProductComposition,
|
||||
upsertTinyProductionOrderDetail,
|
||||
updateProductionOrderExecution,
|
||||
updateProductionOrderStatus
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user