Reserve material lots from cut plans
This commit is contained in:
@@ -255,6 +255,18 @@ const mapStep = (step) => ({
|
||||
color: step.color || ''
|
||||
});
|
||||
|
||||
const mapLotReservation = (reservation) => ({
|
||||
id: reservation.id,
|
||||
lotId: Number(reservation.supply_lot_id),
|
||||
materialKey: reservation.material_key || '',
|
||||
quantity: Number(reservation.quantity || 0),
|
||||
consumedQuantity: Number(reservation.consumed_quantity || 0),
|
||||
status: reservation.status || '',
|
||||
lotProduct: reservation.lot_product || '',
|
||||
lotUnit: reservation.lot_unit || '',
|
||||
lotSupplier: reservation.lot_supplier || ''
|
||||
});
|
||||
|
||||
const mapProductionOrderRow = (row) => {
|
||||
const status = normalizeStatus(row.status);
|
||||
|
||||
@@ -284,6 +296,7 @@ const mapProductionOrderRow = (row) => {
|
||||
rejectedQuantity: Number(row.rejected_quantity || 0),
|
||||
finishedQuantity: row.finished_quantity === null ? null : Number(row.finished_quantity),
|
||||
qualityNotes: row.quality_notes || '',
|
||||
lotReservations: Array.isArray(row.lot_reservations) ? row.lot_reservations.map(mapLotReservation) : [],
|
||||
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) : [],
|
||||
@@ -354,6 +367,29 @@ const getOrderById = async (id, client = pool) => {
|
||||
),
|
||||
'[]'::json
|
||||
) as components,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', reservation.id,
|
||||
'supply_lot_id', reservation.supply_lot_id,
|
||||
'material_key', reservation.material_key,
|
||||
'quantity', reservation.quantity,
|
||||
'consumed_quantity', reservation.consumed_quantity,
|
||||
'status', reservation.status,
|
||||
'lot_product', lot.product,
|
||||
'lot_unit', lot.unit,
|
||||
'lot_supplier', lot.supplier
|
||||
)
|
||||
ORDER BY reservation.created_at, reservation.id
|
||||
)
|
||||
FROM cut_plan_lot_reservations reservation
|
||||
JOIN supply_stock_lots lot ON lot.id = reservation.supply_lot_id
|
||||
WHERE reservation.cut_plan_reference = po.order_reference
|
||||
AND reservation.status IN ('active', 'consumed')
|
||||
),
|
||||
'[]'::json
|
||||
) as lot_reservations,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
@@ -441,6 +477,29 @@ const baseProductionOrderSelect = `
|
||||
),
|
||||
'[]'::json
|
||||
) as components,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', reservation.id,
|
||||
'supply_lot_id', reservation.supply_lot_id,
|
||||
'material_key', reservation.material_key,
|
||||
'quantity', reservation.quantity,
|
||||
'consumed_quantity', reservation.consumed_quantity,
|
||||
'status', reservation.status,
|
||||
'lot_product', lot.product,
|
||||
'lot_unit', lot.unit,
|
||||
'lot_supplier', lot.supplier
|
||||
)
|
||||
ORDER BY reservation.created_at, reservation.id
|
||||
)
|
||||
FROM cut_plan_lot_reservations reservation
|
||||
JOIN supply_stock_lots lot ON lot.id = reservation.supply_lot_id
|
||||
WHERE reservation.cut_plan_reference = po.order_reference
|
||||
AND reservation.status IN ('active', 'consumed')
|
||||
),
|
||||
'[]'::json
|
||||
) as lot_reservations,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
@@ -517,7 +576,7 @@ const listProductionOrders = async (filters = {}) => {
|
||||
return { orders, counts };
|
||||
};
|
||||
|
||||
const createProductionOrders = async (orders = []) => {
|
||||
const createProductionOrders = async (orders = [], lotReservations = []) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
@@ -530,6 +589,7 @@ const createProductionOrders = async (orders = []) => {
|
||||
const productSku = normalizeText(order.productSku);
|
||||
const orderReference = normalizeText(order.orderReference);
|
||||
const quantity = normalizeQuantity(order.quantity);
|
||||
const components = normalizeComponents(order.components, quantity);
|
||||
|
||||
if (!productDescription || !quantity) {
|
||||
skipped.push({ productSku, productDescription, reason: 'invalid_payload' });
|
||||
@@ -611,10 +671,76 @@ const createProductionOrders = async (orders = []) => {
|
||||
`, [orderId, label, normalizeText(marker.color) || null]);
|
||||
}
|
||||
|
||||
for (const component of components) {
|
||||
await client.query(`
|
||||
INSERT INTO production_order_components (
|
||||
production_order_id, component_tiny_id, component_sku, component_name,
|
||||
quantity_per_unit, total_quantity, unit
|
||||
)
|
||||
VALUES ($1, NULLIF($2, ''), NULLIF($3, ''), $4, $5, $6, NULLIF($7, ''));
|
||||
`, [
|
||||
orderId,
|
||||
component.componentTinyId,
|
||||
component.componentSku,
|
||||
component.componentName,
|
||||
component.quantityPerUnit,
|
||||
component.totalQuantity,
|
||||
component.unit
|
||||
]);
|
||||
}
|
||||
|
||||
const createdOrder = await getOrderById(orderId, client);
|
||||
if (createdOrder) created.push(createdOrder);
|
||||
}
|
||||
|
||||
if (created.length && Array.isArray(lotReservations)) {
|
||||
for (const reservation of lotReservations) {
|
||||
const lotId = Number(reservation?.lotId);
|
||||
const quantity = normalizeQuantity(reservation?.quantity);
|
||||
const materialKey = normalizeText(reservation?.materialKey);
|
||||
const cutPlanReference = normalizeText(reservation?.cutPlanReference) || normalizeText(created[0]?.orderReference);
|
||||
if (!Number.isInteger(lotId) || !quantity || !materialKey || !cutPlanReference) {
|
||||
const error = new Error('Reserva de lote inválida.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const lotResult = await client.query(`
|
||||
SELECT id, quantity, unit, status
|
||||
FROM supply_stock_lots
|
||||
WHERE id = $1
|
||||
FOR UPDATE;
|
||||
`, [lotId]);
|
||||
if (!lotResult.rowCount || lotResult.rows[0].status !== 'active') {
|
||||
const error = new Error('O lote selecionado não está disponível para reserva.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
const activeReservations = await client.query(`
|
||||
SELECT quantity, consumed_quantity
|
||||
FROM cut_plan_lot_reservations
|
||||
WHERE supply_lot_id = $1
|
||||
AND status = 'active'
|
||||
FOR UPDATE;
|
||||
`, [lotId]);
|
||||
const alreadyReserved = activeReservations.rows.reduce((total, row) => (
|
||||
total + Number(row.quantity || 0) - Number(row.consumed_quantity || 0)
|
||||
), 0);
|
||||
const availableQuantity = Math.max(0, Number(lotResult.rows[0].quantity || 0) - alreadyReserved);
|
||||
if (quantity > availableQuantity) {
|
||||
const error = new Error(`O lote #${lotId} possui apenas ${availableQuantity} ${lotResult.rows[0].unit} disponível para nova reserva.`);
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
await client.query(`
|
||||
INSERT INTO cut_plan_lot_reservations (
|
||||
cut_plan_reference, supply_lot_id, material_key, quantity, consumed_quantity, status, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, 0, 'active', CURRENT_TIMESTAMP);
|
||||
`, [cutPlanReference, lotId, materialKey, quantity]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return { created, skipped };
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user