Reserve material lots from cut plans
This commit is contained in:
@@ -61,6 +61,8 @@ const mapLot = (row) => ({
|
||||
supplier: row.supplier || '',
|
||||
invoice: row.invoice || '',
|
||||
status: row.status,
|
||||
reservedQuantity: Number(row.reserved_quantity || 0),
|
||||
availableQuantity: Number(row.available_quantity === undefined ? row.quantity : row.available_quantity),
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
});
|
||||
@@ -116,10 +118,19 @@ const listReceipts = async () => {
|
||||
|
||||
const listLots = async () => {
|
||||
const result = await pool.query(`
|
||||
SELECT id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at
|
||||
FROM supply_stock_lots
|
||||
WHERE status = 'active'
|
||||
ORDER BY created_at DESC, id DESC
|
||||
SELECT
|
||||
lot.id, lot.receipt_id, lot.category, lot.product, lot.quantity, lot.unit, lot.supplier, lot.invoice, lot.status, lot.created_at, lot.updated_at,
|
||||
COALESCE(reservations.reserved_quantity, 0) AS reserved_quantity,
|
||||
GREATEST(0, lot.quantity - COALESCE(reservations.reserved_quantity, 0)) AS available_quantity
|
||||
FROM supply_stock_lots lot
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(SUM(quantity - consumed_quantity), 0) AS reserved_quantity
|
||||
FROM cut_plan_lot_reservations reservation
|
||||
WHERE reservation.supply_lot_id = lot.id
|
||||
AND reservation.status = 'active'
|
||||
) reservations ON TRUE
|
||||
WHERE lot.status = 'active'
|
||||
ORDER BY lot.created_at DESC, lot.id DESC
|
||||
`);
|
||||
return result.rows.map(mapLot);
|
||||
};
|
||||
@@ -233,7 +244,7 @@ const buildPurchaseNeeds = (plans, lots, receipts) => {
|
||||
lots.forEach(lot => {
|
||||
if (lot.unit !== 'kg') return;
|
||||
const need = needsByMaterial.get(normalizeKey(lot.product));
|
||||
if (need) need.stockKg += lot.quantity;
|
||||
if (need) need.stockKg += Number(lot.availableQuantity ?? lot.quantity);
|
||||
});
|
||||
|
||||
receipts.forEach(receipt => {
|
||||
@@ -624,7 +635,7 @@ const buildProjectPurchaseNeeds = async (lots, receipts) => {
|
||||
lots.forEach(lot => {
|
||||
const need = getNeedForInventory(lot.unit, '', lot.product)
|
||||
|| needsByMaterial.get(`${normalizeUnit(lot.unit)}:${normalizeKey(lot.product)}`);
|
||||
if (need) need.stockKg += lot.quantity;
|
||||
if (need) need.stockKg += Number(lot.availableQuantity ?? lot.quantity);
|
||||
});
|
||||
|
||||
receipts.forEach(receipt => {
|
||||
@@ -962,6 +973,19 @@ const updateLotQuantity = async (client, lotId, quantity) => {
|
||||
return mapLot(result.rows[0]);
|
||||
};
|
||||
|
||||
const getActiveReservationQuantity = async (client, lotId) => {
|
||||
const result = await client.query(`
|
||||
SELECT quantity, consumed_quantity
|
||||
FROM cut_plan_lot_reservations
|
||||
WHERE supply_lot_id = $1
|
||||
AND status = 'active'
|
||||
FOR UPDATE;
|
||||
`, [lotId]);
|
||||
return result.rows.reduce((total, reservation) => (
|
||||
total + Number(reservation.quantity || 0) - Number(reservation.consumed_quantity || 0)
|
||||
), 0);
|
||||
};
|
||||
|
||||
const adjustInventoryLot = async (id, payload) => {
|
||||
const countedQuantity = normalizeNonNegativeNumber(payload.countedQuantity);
|
||||
const reason = normalizeText(payload.reason);
|
||||
@@ -985,6 +1009,10 @@ const adjustInventoryLot = async (id, payload) => {
|
||||
|
||||
const lot = lotResult.rows[0];
|
||||
const currentQuantity = Number(lot.quantity);
|
||||
const reservedQuantity = await getActiveReservationQuantity(client, lot.id);
|
||||
if (countedQuantity < reservedQuantity) {
|
||||
throw createValidationError(`A contagem não pode ficar abaixo de ${reservedQuantity} ${lot.unit} já reservados para corte.`);
|
||||
}
|
||||
const difference = countedQuantity - currentQuantity;
|
||||
const updatedLot = await updateLotQuantity(client, lot.id, countedQuantity);
|
||||
|
||||
@@ -1039,6 +1067,35 @@ const consumeLotForProduction = async (id, payload) => {
|
||||
if (lot.status !== 'active' || currentQuantity <= 0) throw createValidationError('Lote sem saldo disponível.');
|
||||
if (quantity > currentQuantity) throw createValidationError('Quantidade de saída maior que o saldo do lote.');
|
||||
|
||||
let reference = '';
|
||||
if (productionOrderNumber) {
|
||||
const orderResult = await client.query(`
|
||||
SELECT order_reference
|
||||
FROM production_orders
|
||||
WHERE number = $1
|
||||
LIMIT 1;
|
||||
`, [productionOrderNumber]);
|
||||
reference = normalizeText(orderResult.rows[0]?.order_reference);
|
||||
}
|
||||
const allReservationsResult = await client.query(`
|
||||
SELECT id, cut_plan_reference, quantity, consumed_quantity
|
||||
FROM cut_plan_lot_reservations
|
||||
WHERE supply_lot_id = $1
|
||||
AND status = 'active'
|
||||
ORDER BY created_at, id
|
||||
FOR UPDATE;
|
||||
`, [lot.id]);
|
||||
const totalReserved = allReservationsResult.rows.reduce((total, reservation) => (
|
||||
total + Number(reservation.quantity) - Number(reservation.consumed_quantity)
|
||||
), 0);
|
||||
const reservedByThisPlan = allReservationsResult.rows
|
||||
.filter(reservation => reference && reservation.cut_plan_reference === reference)
|
||||
.reduce((total, reservation) => total + Number(reservation.quantity) - Number(reservation.consumed_quantity), 0);
|
||||
const permittedQuantity = Math.max(0, currentQuantity - totalReserved + reservedByThisPlan);
|
||||
if (quantity > permittedQuantity) {
|
||||
throw createValidationError(`O lote possui ${Math.max(0, currentQuantity - totalReserved)} ${lot.unit} livres e ${reservedByThisPlan} ${lot.unit} reservados para esta OP.`);
|
||||
}
|
||||
|
||||
const updatedLot = await updateLotQuantity(client, lot.id, currentQuantity - quantity);
|
||||
const movementReason = [
|
||||
productionOrderNumber ? `OP ${productionOrderNumber}` : '',
|
||||
@@ -1059,6 +1116,36 @@ const consumeLotForProduction = async (id, payload) => {
|
||||
movementReason || 'Saída para produção'
|
||||
]);
|
||||
|
||||
if (productionOrderNumber) {
|
||||
if (reference) {
|
||||
const reservationsResult = await client.query(`
|
||||
SELECT id, quantity, consumed_quantity
|
||||
FROM cut_plan_lot_reservations
|
||||
WHERE supply_lot_id = $1
|
||||
AND cut_plan_reference = $2
|
||||
AND status = 'active'
|
||||
ORDER BY created_at, id
|
||||
FOR UPDATE;
|
||||
`, [lot.id, reference]);
|
||||
let remainingToApply = quantity;
|
||||
for (const reservation of reservationsResult.rows) {
|
||||
if (remainingToApply <= 0) break;
|
||||
const reservedRemaining = Number(reservation.quantity) - Number(reservation.consumed_quantity);
|
||||
const consumed = Math.min(reservedRemaining, remainingToApply);
|
||||
if (consumed <= 0) continue;
|
||||
const nextConsumedQuantity = Number(reservation.consumed_quantity) + consumed;
|
||||
await client.query(`
|
||||
UPDATE cut_plan_lot_reservations
|
||||
SET consumed_quantity = $1,
|
||||
status = CASE WHEN $1 >= quantity THEN 'consumed' ELSE 'active' END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2;
|
||||
`, [nextConsumedQuantity, reservation.id]);
|
||||
remainingToApply -= consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return updatedLot;
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user