Files
graphs/backend/services/supplyService.js
Cauê Faleiros 800eb976ab
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m16s
Derive consumption references from Tiny OP sync
2026-07-23 11:53:18 -03:00

836 lines
29 KiB
JavaScript

const { pool } = require('../db');
const DEFAULT_SUPPLY_LOOKBACK_DAYS = 30;
const DEFAULT_SUPPLY_COVERAGE_DAYS = 30;
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
const normalizeNumber = (value) => {
if (value === '' || value === null || value === undefined) return null;
const number = Number(String(value).replace(',', '.'));
return Number.isFinite(number) && number > 0 ? number : null;
};
const normalizeNonNegativeNumber = (value) => {
if (value === '' || value === null || value === undefined) return null;
const number = Number(String(value).replace(',', '.'));
return Number.isFinite(number) && number >= 0 ? number : null;
};
const normalizeKey = (value) => normalizeText(value)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
const normalizeSku = (value) => normalizeText(value).toUpperCase();
const normalizeUnit = (value) => {
const unit = normalizeText(value).toLowerCase();
if (['kg', 'quilo', 'quilos'].includes(unit)) return 'kg';
if (['un', 'und', 'un.', 'unidade', 'unidades'].includes(unit)) return 'un.';
return unit || 'un.';
};
const mapReceipt = (row) => ({
id: row.id,
category: row.category,
product: row.product,
quantity: Number(row.quantity),
unit: row.unit,
supplier: row.supplier || '',
invoice: row.invoice || '',
notes: row.notes || '',
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at,
approvedAt: row.approved_at
});
const mapLot = (row) => ({
id: row.id,
receiptId: row.receipt_id,
category: row.category,
product: row.product,
quantity: Number(row.quantity),
unit: row.unit,
supplier: row.supplier || '',
invoice: row.invoice || '',
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at
});
const mapMovement = (row) => ({
id: row.id,
receiptId: row.receipt_id,
lotId: row.lot_id,
type: row.type,
category: row.category,
product: row.product,
quantity: Number(row.quantity),
unit: row.unit,
reason: row.reason || '',
createdAt: row.created_at
});
const mapFabricPlan = (row) => ({
id: row.id,
material: row.material,
color: row.color || '',
quantityKg: Number(row.quantity_kg),
supplier: row.supplier || '',
priority: row.priority,
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at
});
const createValidationError = (message) => {
const error = new Error(message);
error.statusCode = 400;
return error;
};
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
`);
return result.rows.map(mapReceipt);
};
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
`);
return result.rows.map(mapLot);
};
const listMovements = async () => {
const result = await pool.query(`
SELECT id, receipt_id, lot_id, type, category, product, quantity, unit, reason, created_at
FROM supply_movements
ORDER BY created_at DESC, id DESC
`);
return result.rows.map(mapMovement);
};
const listFabricPlans = async () => {
const result = await pool.query(`
SELECT id, material, color, quantity_kg, supplier, priority, status, created_at, updated_at
FROM supply_fabric_plans
WHERE status = 'active'
ORDER BY
CASE priority
WHEN 'Crítico' THEN 1
WHEN 'Atenção' THEN 2
ELSE 3
END,
created_at DESC,
id DESC
`);
return result.rows.map(mapFabricPlan);
};
const buildPurchaseNeeds = (plans, lots, receipts) => {
const needsByMaterial = new Map();
plans.forEach(plan => {
const key = normalizeKey(plan.material);
if (!key) return;
const current = needsByMaterial.get(key) || {
material: plan.material,
plannedKg: 0,
stockKg: 0,
pendingKg: 0,
purchaseKg: 0,
priority: 'Normal',
suppliers: new Set(),
colors: new Set()
};
current.plannedKg += plan.quantityKg;
if (plan.supplier) current.suppliers.add(plan.supplier);
if (plan.color) current.colors.add(plan.color);
if (plan.priority === 'Crítico') current.priority = 'Crítico';
if (plan.priority === 'Atenção' && current.priority !== 'Crítico') current.priority = 'Atenção';
needsByMaterial.set(key, current);
});
lots.forEach(lot => {
if (lot.unit !== 'kg') return;
const need = needsByMaterial.get(normalizeKey(lot.product));
if (need) need.stockKg += lot.quantity;
});
receipts.forEach(receipt => {
if (receipt.status !== 'pending' || receipt.unit !== 'kg') return;
const need = needsByMaterial.get(normalizeKey(receipt.product));
if (need) need.pendingKg += receipt.quantity;
});
return Array.from(needsByMaterial.values())
.map(need => {
const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0);
let status = 'ok';
if (purchaseKg > 0 && (need.priority === 'Crítico' || need.stockKg === 0)) status = 'critical';
else if (purchaseKg > 0) status = 'attention';
return {
material: need.material,
plannedKg: need.plannedKg,
stockKg: need.stockKg,
pendingKg: need.pendingKg,
purchaseKg,
priority: need.priority,
status,
suppliers: Array.from(need.suppliers),
colors: Array.from(need.colors)
};
})
.sort((a, b) => {
const statusOrder = { critical: 1, attention: 2, ok: 3 };
return statusOrder[a.status] - statusOrder[b.status] || b.purchaseKg - a.purchaseKg || a.material.localeCompare(b.material);
});
};
const getReferenceYield = (reference) => {
if (reference.general_yield !== null && Number(reference.general_yield) > 0) {
return Number(reference.general_yield);
}
const sizeYields = reference.size_yields && typeof reference.size_yields === 'object'
? Object.values(reference.size_yields).map(Number).filter(value => Number.isFinite(value) && value > 0)
: [];
if (!sizeYields.length) return null;
return sizeYields.reduce((total, value) => total + value, 0) / sizeYields.length;
};
const listProjectDemandRows = async () => {
const result = await pool.query(`
WITH bounds AS (
SELECT MAX(data_pedido_date) AS end_date
FROM orders
WHERE data_pedido_date IS NOT NULL
),
period_orders AS (
SELECT
produto_id,
MAX(produto_descricao) AS product_name,
SUM(quantidade)::numeric AS quantity_sold
FROM orders, bounds
WHERE data_pedido_date IS NOT NULL
AND bounds.end_date IS NOT NULL
AND data_pedido_date >= (bounds.end_date - ($1::int - 1) * INTERVAL '1 day')::date
AND data_pedido_date <= bounds.end_date
GROUP BY produto_id
)
SELECT
COALESCE(period_orders.produto_id, stock.produto_id) AS product_id,
COALESCE(NULLIF(period_orders.product_name, ''), NULLIF(stock.nome, ''), 'Produto sem nome') AS product_name,
COALESCE(period_orders.quantity_sold, 0)::numeric AS quantity_sold,
COALESCE(stock.saldo, 0)::numeric AS stock_quantity
FROM period_orders
FULL OUTER JOIN stock ON stock.produto_id = period_orders.produto_id
WHERE COALESCE(period_orders.produto_id, stock.produto_id, '') <> ''
ORDER BY quantity_sold DESC, product_name;
`, [DEFAULT_SUPPLY_LOOKBACK_DAYS]);
return result.rows;
};
const listConsumptionReferenceRows = async () => {
const result = await pool.query(`
SELECT
r.product_id,
p.sku AS product_sku,
p.name AS product_name,
r.material_product_id,
m.sku AS material_sku,
m.name AS material_name,
r.color,
r.general_yield,
r.size_yields,
r.consumption_quantity,
r.consumption_unit,
r.source
FROM consumption_references r
JOIN catalog_products p ON p.id = r.product_id
LEFT JOIN catalog_products m ON m.id = r.material_product_id
ORDER BY p.sku, r.color NULLS FIRST;
`);
return result.rows;
};
const mergeNeedLine = (needsByMaterial, key, patch) => {
const current = needsByMaterial.get(key) || {
material: patch.material,
plannedKg: 0,
stockKg: 0,
pendingKg: 0,
purchaseKg: 0,
priority: patch.priority || 'Normal',
suppliers: new Set(),
colors: new Set(),
unit: patch.unit || 'kg',
source: patch.source || 'manual_plan',
missingReference: Boolean(patch.missingReference),
products: []
};
current.plannedKg += patch.plannedKg || 0;
current.priority = patch.priority === 'Crítico' || current.priority === 'Crítico'
? 'Crítico'
: patch.priority === 'Atenção' || current.priority === 'Atenção'
? 'Atenção'
: current.priority;
current.missingReference = current.missingReference || Boolean(patch.missingReference);
current.source = current.source === patch.source ? current.source : 'mixed';
if (patch.supplier) current.suppliers.add(patch.supplier);
if (patch.color) current.colors.add(patch.color);
if (patch.product) current.products.push(patch.product);
needsByMaterial.set(key, current);
return current;
};
const buildProjectPurchaseNeeds = async (lots, receipts) => {
const [demandRows, referenceRows] = await Promise.all([
listProjectDemandRows(),
listConsumptionReferenceRows()
]);
const referencesBySku = referenceRows.reduce((references, reference) => {
const sku = normalizeSku(reference.product_sku);
if (!sku) return references;
references.set(sku, [...(references.get(sku) || []), reference]);
return references;
}, new Map());
const needsByMaterial = new Map();
demandRows.forEach(row => {
const productId = normalizeSku(row.product_id);
if (!productId) return;
const quantitySold = Number(row.quantity_sold || 0);
const stockQuantity = Number(row.stock_quantity || 0);
const projectedDemand = quantitySold * (DEFAULT_SUPPLY_COVERAGE_DAYS / DEFAULT_SUPPLY_LOOKBACK_DAYS);
const suggestedQuantity = Math.max(Math.ceil(projectedDemand - stockQuantity), 0);
if (suggestedQuantity <= 0) return;
const references = referencesBySku.get(productId) || [];
if (!references.length) {
mergeNeedLine(needsByMaterial, `missing:${productId}`, {
material: `Cadastrar consumo: ${normalizeText(row.product_name) || productId}`,
plannedKg: suggestedQuantity,
priority: 'Crítico',
unit: 'un.',
source: 'project_demand',
missingReference: true,
product: {
productId,
name: normalizeText(row.product_name),
suggestedQuantity,
quantitySold,
stockQuantity
}
});
return;
}
references.forEach(reference => {
const consumptionQuantity = Number(reference.consumption_quantity || 0);
const consumptionUnit = normalizeUnit(reference.consumption_unit);
const materialName = normalizeText(reference.material_name) || normalizeText(reference.material_sku) || 'Material sem cadastro';
if (consumptionQuantity > 0) {
mergeNeedLine(needsByMaterial, `${consumptionUnit}:${normalizeKey(materialName)}`, {
material: materialName,
plannedKg: suggestedQuantity * consumptionQuantity,
priority: 'Atenção',
unit: consumptionUnit,
source: reference.source || 'project_demand',
color: reference.color,
product: {
productId,
name: normalizeText(row.product_name),
suggestedQuantity,
quantitySold,
stockQuantity,
consumptionQuantity,
consumptionUnit
}
});
return;
}
const yieldPerKg = getReferenceYield(reference);
if (!yieldPerKg) {
mergeNeedLine(needsByMaterial, `missing-yield:${productId}:${reference.material_product_id || 'material'}`, {
material: `Cadastrar rendimento: ${normalizeText(row.product_name) || productId}`,
plannedKg: suggestedQuantity,
priority: 'Crítico',
unit: 'un.',
source: 'project_demand',
missingReference: true,
color: reference.color,
product: {
productId,
name: normalizeText(row.product_name),
suggestedQuantity,
quantitySold,
stockQuantity
}
});
return;
}
mergeNeedLine(needsByMaterial, `kg:${normalizeKey(materialName)}`, {
material: materialName,
plannedKg: suggestedQuantity / yieldPerKg,
priority: 'Atenção',
unit: 'kg',
source: reference.source || 'project_demand',
color: reference.color,
product: {
productId,
name: normalizeText(row.product_name),
suggestedQuantity,
quantitySold,
stockQuantity,
yieldPerKg
}
});
});
});
lots.forEach(lot => {
const need = needsByMaterial.get(`${normalizeUnit(lot.unit)}:${normalizeKey(lot.product)}`);
if (need) need.stockKg += lot.quantity;
});
receipts.forEach(receipt => {
if (receipt.status !== 'pending') return;
const need = needsByMaterial.get(`${normalizeUnit(receipt.unit)}:${normalizeKey(receipt.product)}`);
if (need) need.pendingKg += receipt.quantity;
});
return Array.from(needsByMaterial.values()).map(need => {
const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0);
let status = 'ok';
if (purchaseKg > 0 && (need.priority === 'Crítico' || need.stockKg === 0)) status = 'critical';
else if (purchaseKg > 0) status = 'attention';
return {
material: need.material,
plannedKg: need.plannedKg,
stockKg: need.stockKg,
pendingKg: need.pendingKg,
purchaseKg,
priority: need.priority,
status,
suppliers: Array.from(need.suppliers),
colors: Array.from(need.colors),
unit: need.unit,
source: need.source,
missingReference: need.missingReference,
products: need.products
};
});
};
const mergePurchaseNeeds = (manualNeeds, projectNeeds) => {
const mergedByKey = new Map();
[...manualNeeds, ...projectNeeds].forEach(need => {
const key = `${need.unit || 'kg'}:${normalizeKey(need.material)}:${need.missingReference ? 'missing' : 'mapped'}`;
const current = mergedByKey.get(key);
if (!current) {
mergedByKey.set(key, {
...need,
suppliers: new Set(need.suppliers || []),
colors: new Set(need.colors || []),
products: [...(need.products || [])]
});
return;
}
current.plannedKg += need.plannedKg;
current.stockKg += need.stockKg;
current.pendingKg += need.pendingKg;
current.purchaseKg += need.purchaseKg;
current.priority = need.priority === 'Crítico' || current.priority === 'Crítico'
? 'Crítico'
: need.priority === 'Atenção' || current.priority === 'Atenção'
? 'Atenção'
: current.priority;
current.status = current.status === 'critical' || need.status === 'critical'
? 'critical'
: current.status === 'attention' || need.status === 'attention'
? 'attention'
: 'ok';
current.source = current.source === need.source ? current.source : 'mixed';
current.missingReference = current.missingReference || Boolean(need.missingReference);
(need.suppliers || []).forEach(supplier => current.suppliers.add(supplier));
(need.colors || []).forEach(color => current.colors.add(color));
current.products.push(...(need.products || []));
});
return Array.from(mergedByKey.values())
.map(need => ({
...need,
suppliers: Array.from(need.suppliers),
colors: Array.from(need.colors)
}))
.sort((a, b) => {
const statusOrder = { critical: 1, attention: 2, ok: 3 };
return statusOrder[a.status] - statusOrder[b.status] || b.purchaseKg - a.purchaseKg || a.material.localeCompare(b.material);
});
};
const buildStats = (receipts, lots, purchaseNeeds) => {
const totalQuantityKg = lots.reduce((total, lot) => (
lot.unit === 'kg' ? total + lot.quantity : total
), 0);
return {
totalQuantityKg,
activeLots: lots.length,
rolls: lots.filter(lot => lot.unit === 'rolos').reduce((total, lot) => total + lot.quantity, 0),
alerts: purchaseNeeds.filter(need => need.status !== 'ok').length,
pendingReceipts: receipts.filter(receipt => receipt.status === 'pending').length,
approvedReceipts: receipts.filter(receipt => receipt.status === 'approved').length
};
};
const getSupplySummary = async () => {
const [receipts, lots, movements, fabricPlans] = await Promise.all([
listReceipts(),
listLots(),
listMovements(),
listFabricPlans()
]);
const manualPurchaseNeeds = buildPurchaseNeeds(fabricPlans, lots, receipts);
const projectPurchaseNeeds = await buildProjectPurchaseNeeds(lots, receipts);
const purchaseNeeds = mergePurchaseNeeds(manualPurchaseNeeds, projectPurchaseNeeds);
return {
receipts,
lots,
movements,
fabricPlans,
purchaseNeeds,
stats: buildStats(receipts, lots, purchaseNeeds)
};
};
const listPurchaseNeeds = async () => {
const [plans, lots, receipts] = await Promise.all([
listFabricPlans(),
listLots(),
listReceipts()
]);
return mergePurchaseNeeds(
buildPurchaseNeeds(plans, lots, receipts),
await buildProjectPurchaseNeeds(lots, receipts)
);
};
const createReceipt = async (payload) => {
const category = normalizeText(payload.category);
const product = normalizeText(payload.product);
const quantity = normalizeNumber(payload.quantity);
const unit = normalizeText(payload.unit) || 'kg';
if (!category) throw createValidationError('Categoria é obrigatória.');
if (!product) throw createValidationError('Produto ou material é obrigatório.');
if (!quantity) throw createValidationError('Quantidade deve ser maior que zero.');
const result = await pool.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,
unit,
normalizeText(payload.supplier) || null,
normalizeText(payload.invoice) || null,
normalizeText(payload.notes) || null
]);
return mapReceipt(result.rows[0]);
};
const approveReceipt = async (id) => {
const client = await pool.connect();
try {
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
`, [id]);
if (!receiptResult.rowCount) {
throw createValidationError('Recebimento não encontrado.');
}
const receipt = receiptResult.rows[0];
if (receipt.status === 'approved') {
await client.query('COMMIT');
return mapReceipt(receipt);
}
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
`, [id]);
const lotResult = await client.query(`
INSERT INTO supply_stock_lots (
receipt_id, category, product, quantity, unit, supplier, invoice, status, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'active', CURRENT_TIMESTAMP)
RETURNING id
`, [
receipt.id,
receipt.category,
receipt.product,
receipt.quantity,
receipt.unit,
receipt.supplier,
receipt.invoice
]);
await client.query(`
INSERT INTO supply_movements (
receipt_id, lot_id, type, category, product, quantity, unit, reason
)
VALUES ($1, $2, 'receipt', $3, $4, $5, $6, $7)
`, [
receipt.id,
lotResult.rows[0].id,
receipt.category,
receipt.product,
receipt.quantity,
receipt.unit,
`Recebimento aprovado${receipt.invoice ? ` · NF ${receipt.invoice}` : ''}`
]);
await client.query('COMMIT');
return mapReceipt(updatedReceiptResult.rows[0]);
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
const deleteReceipt = async (id) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM supply_movements WHERE receipt_id = $1', [id]);
await client.query('DELETE FROM supply_stock_lots WHERE receipt_id = $1', [id]);
await client.query('DELETE FROM supply_receipts WHERE id = $1', [id]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
const createFabricPlan = async (payload) => {
const material = normalizeText(payload.material);
const quantityKg = normalizeNumber(payload.quantityKg);
if (!material) throw createValidationError('Malha ou tecido é obrigatório.');
if (!quantityKg) throw createValidationError('Quantidade deve ser maior que zero.');
const result = await pool.query(`
INSERT INTO supply_fabric_plans (
material, color, quantity_kg, supplier, priority, status, updated_at
)
VALUES ($1, $2, $3, $4, $5, 'active', CURRENT_TIMESTAMP)
RETURNING id, material, color, quantity_kg, supplier, priority, status, created_at, updated_at
`, [
material,
normalizeText(payload.color) || 'Todas as cores',
quantityKg,
normalizeText(payload.supplier) || null,
normalizeText(payload.priority) || 'Normal'
]);
return mapFabricPlan(result.rows[0]);
};
const deleteFabricPlan = async (id) => {
await pool.query(`
UPDATE supply_fabric_plans
SET status = 'removed',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [id]);
};
const updateLotQuantity = async (client, lotId, quantity) => {
const status = quantity > 0 ? 'active' : 'depleted';
const result = await client.query(`
UPDATE supply_stock_lots
SET quantity = $2,
status = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at
`, [lotId, quantity, status]);
return mapLot(result.rows[0]);
};
const adjustInventoryLot = async (id, payload) => {
const countedQuantity = normalizeNonNegativeNumber(payload.countedQuantity);
const reason = normalizeText(payload.reason);
if (countedQuantity === null) throw createValidationError('Quantidade contada deve ser zero ou maior.');
if (!reason) throw createValidationError('Justificativa do ajuste é obrigatória.');
const client = await pool.connect();
try {
await client.query('BEGIN');
const lotResult = await client.query(`
SELECT id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at
FROM supply_stock_lots
WHERE id = $1
FOR UPDATE
`, [id]);
if (!lotResult.rowCount) throw createValidationError('Lote não encontrado.');
const lot = lotResult.rows[0];
const currentQuantity = Number(lot.quantity);
const difference = countedQuantity - currentQuantity;
const updatedLot = await updateLotQuantity(client, lot.id, countedQuantity);
await client.query(`
INSERT INTO supply_movements (
lot_id, type, category, product, quantity, unit, reason
)
VALUES ($1, 'inventory_adjustment', $2, $3, $4, $5, $6)
`, [
lot.id,
lot.category,
lot.product,
difference,
lot.unit,
`${reason} · sistema ${currentQuantity} ${lot.unit} · contado ${countedQuantity} ${lot.unit}`
]);
await client.query('COMMIT');
return updatedLot;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
const consumeLotForProduction = async (id, payload) => {
const quantity = normalizeNumber(payload.quantity);
const reason = normalizeText(payload.reason);
const productionOrderNumber = normalizeText(payload.productionOrderNumber);
if (!quantity) throw createValidationError('Quantidade de saída deve ser maior que zero.');
if (!productionOrderNumber && !reason) throw createValidationError('Informe a OP ou uma justificativa para a saída.');
const client = await pool.connect();
try {
await client.query('BEGIN');
const lotResult = await client.query(`
SELECT id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at
FROM supply_stock_lots
WHERE id = $1
FOR UPDATE
`, [id]);
if (!lotResult.rowCount) throw createValidationError('Lote não encontrado.');
const lot = lotResult.rows[0];
const currentQuantity = Number(lot.quantity);
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.');
const updatedLot = await updateLotQuantity(client, lot.id, currentQuantity - quantity);
const movementReason = [
productionOrderNumber ? `OP ${productionOrderNumber}` : '',
reason
].filter(Boolean).join(' · ');
await client.query(`
INSERT INTO supply_movements (
lot_id, type, category, product, quantity, unit, reason
)
VALUES ($1, 'production_exit', $2, $3, $4, $5, $6)
`, [
lot.id,
lot.category,
lot.product,
-quantity,
lot.unit,
movementReason || 'Saída para produção'
]);
await client.query('COMMIT');
return updatedLot;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
module.exports = {
adjustInventoryLot,
approveReceipt,
consumeLotForProduction,
createFabricPlan,
createReceipt,
deleteFabricPlan,
deleteReceipt,
getSupplySummary,
buildProjectPurchaseNeeds,
buildPurchaseNeeds,
listFabricPlans,
listLots,
listMovements,
listPurchaseNeeds,
listReceipts
};