Derive consumption references from Tiny OP sync
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m16s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m16s
This commit is contained in:
@@ -245,6 +245,10 @@ const initDB = async () => {
|
|||||||
efficiency_percent NUMERIC(7, 3),
|
efficiency_percent NUMERIC(7, 3),
|
||||||
rib_g_per_piece NUMERIC(14, 4),
|
rib_g_per_piece NUMERIC(14, 4),
|
||||||
material_cost_per_kg NUMERIC(14, 4),
|
material_cost_per_kg NUMERIC(14, 4),
|
||||||
|
consumption_quantity NUMERIC(14, 4),
|
||||||
|
consumption_unit VARCHAR(30),
|
||||||
|
source VARCHAR(60) DEFAULT 'manual',
|
||||||
|
last_production_order_id INTEGER REFERENCES production_orders(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -381,6 +385,14 @@ const initDB = async () => {
|
|||||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||||
`).catch(() => {});
|
`).catch(() => {});
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
ALTER TABLE consumption_references
|
||||||
|
ADD COLUMN IF NOT EXISTS consumption_quantity NUMERIC(14, 4),
|
||||||
|
ADD COLUMN IF NOT EXISTS consumption_unit VARCHAR(30),
|
||||||
|
ADD COLUMN IF NOT EXISTS source VARCHAR(60) DEFAULT 'manual',
|
||||||
|
ADD COLUMN IF NOT EXISTS last_production_order_id INTEGER REFERENCES production_orders(id) ON DELETE SET NULL;
|
||||||
|
`).catch(() => {});
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
ALTER TABLE consumption_references
|
ALTER TABLE consumption_references
|
||||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||||
@@ -497,6 +509,17 @@ const initDB = async () => {
|
|||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_category_id ON catalog_products (category_id);`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_category_id ON catalog_products (category_id);`);
|
||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_consumption_references_product_id ON consumption_references (product_id);`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_consumption_references_product_id ON consumption_references (product_id);`);
|
||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_consumption_references_material_product_id ON consumption_references (material_product_id);`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_consumption_references_material_product_id ON consumption_references (material_product_id);`);
|
||||||
|
await pool.query(`
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS unique_consumption_reference_source
|
||||||
|
ON consumption_references (
|
||||||
|
product_id,
|
||||||
|
COALESCE(material_product_id, 0),
|
||||||
|
COALESCE(color, ''),
|
||||||
|
COALESCE(source, 'manual')
|
||||||
|
);
|
||||||
|
`).catch(err => {
|
||||||
|
console.error('Notice: Could not create unique consumption reference source index:', err.message);
|
||||||
|
});
|
||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_status ON supply_receipts (status);`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_status ON supply_receipts (status);`);
|
||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_created_at ON supply_receipts (created_at DESC);`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_created_at ON supply_receipts (created_at DESC);`);
|
||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_stock_lots_status ON supply_stock_lots (status);`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_stock_lots_status ON supply_stock_lots (status);`);
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ const mapConsumptionReference = (row) => ({
|
|||||||
efficiencyPercent: row.efficiency_percent === null ? null : Number(row.efficiency_percent),
|
efficiencyPercent: row.efficiency_percent === null ? null : Number(row.efficiency_percent),
|
||||||
ribGPerPiece: row.rib_g_per_piece === null ? null : Number(row.rib_g_per_piece),
|
ribGPerPiece: row.rib_g_per_piece === null ? null : Number(row.rib_g_per_piece),
|
||||||
materialCostPerKg: row.material_cost_per_kg === null ? null : Number(row.material_cost_per_kg),
|
materialCostPerKg: row.material_cost_per_kg === null ? null : Number(row.material_cost_per_kg),
|
||||||
|
consumptionQuantity: row.consumption_quantity === null ? null : Number(row.consumption_quantity),
|
||||||
|
consumptionUnit: row.consumption_unit || '',
|
||||||
|
source: row.source || 'manual',
|
||||||
|
lastProductionOrderId: row.last_production_order_id === null ? null : Number(row.last_production_order_id),
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
});
|
});
|
||||||
@@ -185,7 +189,8 @@ const listConsumptionReferences = async () => {
|
|||||||
r.material_product_id, m.sku AS material_sku, m.name AS material_name,
|
r.material_product_id, m.sku AS material_sku, m.name AS material_name,
|
||||||
r.color, r.general_yield, r.size_yields, r.size_areas,
|
r.color, r.general_yield, r.size_yields, r.size_areas,
|
||||||
r.gramature, r.efficiency_percent, r.rib_g_per_piece,
|
r.gramature, r.efficiency_percent, r.rib_g_per_piece,
|
||||||
r.material_cost_per_kg, r.created_at, r.updated_at
|
r.material_cost_per_kg, r.consumption_quantity, r.consumption_unit,
|
||||||
|
r.source, r.last_production_order_id, r.created_at, r.updated_at
|
||||||
FROM consumption_references r
|
FROM consumption_references r
|
||||||
JOIN catalog_products p ON p.id = r.product_id
|
JOIN catalog_products p ON p.id = r.product_id
|
||||||
LEFT JOIN catalog_products m ON m.id = r.material_product_id
|
LEFT JOIN catalog_products m ON m.id = r.material_product_id
|
||||||
@@ -208,9 +213,10 @@ const createConsumptionReference = async (payload) => {
|
|||||||
const calculatedYield = Object.values(sizeYields).length
|
const calculatedYield = Object.values(sizeYields).length
|
||||||
? Object.values(sizeYields).reduce((total, value) => total + value, 0) / Object.values(sizeYields).length
|
? Object.values(sizeYields).reduce((total, value) => total + value, 0) / Object.values(sizeYields).length
|
||||||
: null;
|
: null;
|
||||||
|
const consumptionQuantity = normalizeNumber(payload.consumptionQuantity);
|
||||||
|
|
||||||
if (!generalYield && !calculatedYield) {
|
if (!generalYield && !calculatedYield && !consumptionQuantity) {
|
||||||
const error = new Error('Informe o rendimento geral ou por tamanho.');
|
const error = new Error('Informe o rendimento ou consumo por peça.');
|
||||||
error.statusCode = 400;
|
error.statusCode = 400;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -219,9 +225,9 @@ const createConsumptionReference = async (payload) => {
|
|||||||
INSERT INTO consumption_references (
|
INSERT INTO consumption_references (
|
||||||
product_id, material_product_id, color, general_yield, size_yields,
|
product_id, material_product_id, color, general_yield, size_yields,
|
||||||
size_areas, gramature, efficiency_percent, rib_g_per_piece,
|
size_areas, gramature, efficiency_percent, rib_g_per_piece,
|
||||||
material_cost_per_kg, updated_at
|
material_cost_per_kg, consumption_quantity, consumption_unit, source, updated_at
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8, $9, $10, CURRENT_TIMESTAMP)
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8, $9, $10, $11, $12, 'manual', CURRENT_TIMESTAMP)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, [
|
`, [
|
||||||
productId,
|
productId,
|
||||||
@@ -233,7 +239,9 @@ const createConsumptionReference = async (payload) => {
|
|||||||
normalizeNumber(payload.gramature),
|
normalizeNumber(payload.gramature),
|
||||||
normalizeNumber(payload.efficiencyPercent),
|
normalizeNumber(payload.efficiencyPercent),
|
||||||
normalizeNumber(payload.ribGPerPiece),
|
normalizeNumber(payload.ribGPerPiece),
|
||||||
normalizeNumber(payload.materialCostPerKg)
|
normalizeNumber(payload.materialCostPerKg),
|
||||||
|
consumptionQuantity,
|
||||||
|
normalizeText(payload.consumptionUnit) || null
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const references = await listConsumptionReferences();
|
const references = await listConsumptionReferences();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const sampleSpecs = {
|
|||||||
orderBy: ['updated_at', 'created_at', 'id']
|
orderBy: ['updated_at', 'created_at', 'id']
|
||||||
},
|
},
|
||||||
consumption_references: {
|
consumption_references: {
|
||||||
columns: ['id', 'product_id', 'material_product_id', 'color', 'general_yield', 'size_yields', 'size_areas', 'gramature', 'efficiency_percent', 'rib_g_per_piece', 'material_cost_per_kg', 'created_at', 'updated_at'],
|
columns: ['id', 'product_id', 'material_product_id', 'color', 'general_yield', 'size_yields', 'size_areas', 'gramature', 'efficiency_percent', 'rib_g_per_piece', 'material_cost_per_kg', 'consumption_quantity', 'consumption_unit', 'source', 'last_production_order_id', 'created_at', 'updated_at'],
|
||||||
orderBy: ['updated_at', 'created_at', 'id']
|
orderBy: ['updated_at', 'created_at', 'id']
|
||||||
},
|
},
|
||||||
cutting_family_rules: {
|
cutting_family_rules: {
|
||||||
|
|||||||
@@ -65,6 +65,157 @@ const normalizeInteger = (value) => {
|
|||||||
return Number.isInteger(number) ? number : null;
|
return Number.isInteger(number) ? number : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeSku = (value) => normalizeText(value).toUpperCase();
|
||||||
|
|
||||||
|
const normalizeUnit = (value) => normalizeText(value).toLowerCase();
|
||||||
|
|
||||||
|
const normalizeCategoryName = (name) => normalizeText(name)
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/\p{Diacritic}/gu, '')
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
const classifyCatalogCategoryName = (name, type) => {
|
||||||
|
const normalizedName = normalizeCategoryName(name);
|
||||||
|
if (type === 'finished_product') {
|
||||||
|
if (/\bDTF\b/.test(normalizedName)) return 'DTF';
|
||||||
|
if (/\b(?:MOLETOM|CANGURU)\b/.test(normalizedName)) return 'Moletom';
|
||||||
|
if (/\b(?:OVERSIZE|OVERSIZED)\b/.test(normalizedName)) return 'Oversized';
|
||||||
|
if (/\bINFANTIL\b/.test(normalizedName)) return 'Camiseta infantil';
|
||||||
|
if (/\b(?:BONE|ACESSORIO|ACESSORIOS)\b/.test(normalizedName)) return 'Acessórios';
|
||||||
|
return 'Camiseta regular';
|
||||||
|
}
|
||||||
|
if (/\b(?:MALHA|TECIDO|RIBANA|FIO)\b/.test(normalizedName)) return 'Malha';
|
||||||
|
if (/\b(?:ETIQUETA|TAG|EMBALAGEM|SACO|SACOLA)\b/.test(normalizedName)) return 'Embalagens';
|
||||||
|
if (/\b(?:ATACADOR|ILHOS|LINHA|FITA)\b/.test(normalizedName)) return 'Aviamentos';
|
||||||
|
if (/\bDTF\b/.test(normalizedName)) return 'DTF';
|
||||||
|
return 'Insumos gerais';
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureCategoryId = async (client, name) => {
|
||||||
|
const result = await client.query(`
|
||||||
|
INSERT INTO catalog_categories (name, description, updated_at)
|
||||||
|
VALUES ($1, $2, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (name) DO UPDATE
|
||||||
|
SET updated_at = catalog_categories.updated_at
|
||||||
|
RETURNING id;
|
||||||
|
`, [name, 'Categoria criada automaticamente pela sincronização de produção.']);
|
||||||
|
|
||||||
|
return result.rows[0].id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertCatalogProductFromSync = async (client, { sku, name, type, categoryName, notes }) => {
|
||||||
|
const normalizedSku = normalizeSku(sku);
|
||||||
|
const normalizedName = normalizeText(name);
|
||||||
|
if (!normalizedSku || !normalizedName) return null;
|
||||||
|
|
||||||
|
const categoryId = await ensureCategoryId(client, categoryName || classifyCatalogCategoryName(normalizedName, type));
|
||||||
|
const result = await client.query(`
|
||||||
|
INSERT INTO catalog_products (
|
||||||
|
type, sku, name, category_id, notes, updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (sku) DO UPDATE
|
||||||
|
SET type = EXCLUDED.type,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
category_id = COALESCE(catalog_products.category_id, EXCLUDED.category_id),
|
||||||
|
notes = COALESCE(NULLIF(catalog_products.notes, ''), EXCLUDED.notes),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING id;
|
||||||
|
`, [
|
||||||
|
type,
|
||||||
|
normalizedSku,
|
||||||
|
normalizedName,
|
||||||
|
categoryId,
|
||||||
|
normalizeText(notes) || null
|
||||||
|
]);
|
||||||
|
|
||||||
|
return result.rows[0].id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertConsumptionReferencesFromComponents = async (client, orderId, order, components) => {
|
||||||
|
if (!order.productSku || !components.length) {
|
||||||
|
return { referenceCount: 0, skippedReferenceCount: components.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
const productId = await upsertCatalogProductFromSync(client, {
|
||||||
|
sku: order.productSku,
|
||||||
|
name: order.productDescription,
|
||||||
|
type: 'finished_product',
|
||||||
|
categoryName: classifyCatalogCategoryName(order.productDescription, 'finished_product'),
|
||||||
|
notes: `Sincronizado da OP ${order.number || order.tinyId || orderId}.`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!productId) return { referenceCount: 0, skippedReferenceCount: components.length };
|
||||||
|
|
||||||
|
let referenceCount = 0;
|
||||||
|
let skippedReferenceCount = 0;
|
||||||
|
|
||||||
|
for (const component of components) {
|
||||||
|
const materialId = await upsertCatalogProductFromSync(client, {
|
||||||
|
sku: component.componentSku,
|
||||||
|
name: component.componentName,
|
||||||
|
type: 'raw_material',
|
||||||
|
categoryName: classifyCatalogCategoryName(component.componentName, 'raw_material'),
|
||||||
|
notes: `Sincronizado da composição da OP ${order.number || order.tinyId || orderId}.`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!materialId || !component.quantityPerUnit) {
|
||||||
|
skippedReferenceCount += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = normalizeUnit(component.unit);
|
||||||
|
const generalYield = unit === 'kg' ? 1 / component.quantityPerUnit : null;
|
||||||
|
|
||||||
|
const existingReferenceResult = await client.query(`
|
||||||
|
SELECT id
|
||||||
|
FROM consumption_references
|
||||||
|
WHERE product_id = $1
|
||||||
|
AND COALESCE(material_product_id, 0) = $2
|
||||||
|
AND COALESCE(color, '') = ''
|
||||||
|
AND COALESCE(source, 'manual') = 'tiny_op'
|
||||||
|
LIMIT 1;
|
||||||
|
`, [productId, materialId]);
|
||||||
|
|
||||||
|
if (existingReferenceResult.rows.length) {
|
||||||
|
await client.query(`
|
||||||
|
UPDATE consumption_references
|
||||||
|
SET general_yield = $1,
|
||||||
|
consumption_quantity = $2,
|
||||||
|
consumption_unit = $3,
|
||||||
|
last_production_order_id = $4,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $5;
|
||||||
|
`, [
|
||||||
|
generalYield,
|
||||||
|
component.quantityPerUnit,
|
||||||
|
component.unit || null,
|
||||||
|
orderId,
|
||||||
|
existingReferenceResult.rows[0].id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO consumption_references (
|
||||||
|
product_id, material_product_id, general_yield, consumption_quantity,
|
||||||
|
consumption_unit, source, last_production_order_id, updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, 'tiny_op', $6, CURRENT_TIMESTAMP);
|
||||||
|
`, [
|
||||||
|
productId,
|
||||||
|
materialId,
|
||||||
|
generalYield,
|
||||||
|
component.quantityPerUnit,
|
||||||
|
component.unit || null,
|
||||||
|
orderId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { referenceCount, skippedReferenceCount };
|
||||||
|
};
|
||||||
|
|
||||||
const mapComponent = (component) => ({
|
const mapComponent = (component) => ({
|
||||||
id: component.id,
|
id: component.id,
|
||||||
componentTinyId: component.component_tiny_id || '',
|
componentTinyId: component.component_tiny_id || '',
|
||||||
@@ -640,11 +791,15 @@ const upsertTinyProductionOrderDetail = async (payload = {}) => {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const referenceSync = await upsertConsumptionReferencesFromComponents(client, orderId, order, components);
|
||||||
|
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
return {
|
return {
|
||||||
order: await getOrderById(orderId),
|
order: await getOrderById(orderId),
|
||||||
componentCount: components.length,
|
componentCount: components.length,
|
||||||
stepCount: steps.length
|
stepCount: steps.length,
|
||||||
|
referenceCount: referenceSync.referenceCount,
|
||||||
|
skippedReferenceCount: referenceSync.skippedReferenceCount
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await client.query('ROLLBACK');
|
await client.query('ROLLBACK');
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ const normalizeKey = (value) => normalizeText(value)
|
|||||||
|
|
||||||
const normalizeSku = (value) => normalizeText(value).toUpperCase();
|
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) => ({
|
const mapReceipt = (row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
category: row.category,
|
category: row.category,
|
||||||
@@ -249,7 +256,10 @@ const listConsumptionReferenceRows = async () => {
|
|||||||
m.name AS material_name,
|
m.name AS material_name,
|
||||||
r.color,
|
r.color,
|
||||||
r.general_yield,
|
r.general_yield,
|
||||||
r.size_yields
|
r.size_yields,
|
||||||
|
r.consumption_quantity,
|
||||||
|
r.consumption_unit,
|
||||||
|
r.source
|
||||||
FROM consumption_references r
|
FROM consumption_references r
|
||||||
JOIN catalog_products p ON p.id = r.product_id
|
JOIN catalog_products p ON p.id = r.product_id
|
||||||
LEFT JOIN catalog_products m ON m.id = r.material_product_id
|
LEFT JOIN catalog_products m ON m.id = r.material_product_id
|
||||||
@@ -296,7 +306,12 @@ const buildProjectPurchaseNeeds = async (lots, receipts) => {
|
|||||||
listProjectDemandRows(),
|
listProjectDemandRows(),
|
||||||
listConsumptionReferenceRows()
|
listConsumptionReferenceRows()
|
||||||
]);
|
]);
|
||||||
const referencesBySku = new Map(referenceRows.map(reference => [normalizeSku(reference.product_sku), reference]));
|
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();
|
const needsByMaterial = new Map();
|
||||||
|
|
||||||
demandRows.forEach(row => {
|
demandRows.forEach(row => {
|
||||||
@@ -309,8 +324,8 @@ const buildProjectPurchaseNeeds = async (lots, receipts) => {
|
|||||||
const suggestedQuantity = Math.max(Math.ceil(projectedDemand - stockQuantity), 0);
|
const suggestedQuantity = Math.max(Math.ceil(projectedDemand - stockQuantity), 0);
|
||||||
if (suggestedQuantity <= 0) return;
|
if (suggestedQuantity <= 0) return;
|
||||||
|
|
||||||
const reference = referencesBySku.get(productId);
|
const references = referencesBySku.get(productId) || [];
|
||||||
if (!reference) {
|
if (!references.length) {
|
||||||
mergeNeedLine(needsByMaterial, `missing:${productId}`, {
|
mergeNeedLine(needsByMaterial, `missing:${productId}`, {
|
||||||
material: `Cadastrar consumo: ${normalizeText(row.product_name) || productId}`,
|
material: `Cadastrar consumo: ${normalizeText(row.product_name) || productId}`,
|
||||||
plannedKg: suggestedQuantity,
|
plannedKg: suggestedQuantity,
|
||||||
@@ -329,56 +344,81 @@ const buildProjectPurchaseNeeds = async (lots, receipts) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const yieldPerKg = getReferenceYield(reference);
|
references.forEach(reference => {
|
||||||
if (!yieldPerKg) {
|
const consumptionQuantity = Number(reference.consumption_quantity || 0);
|
||||||
mergeNeedLine(needsByMaterial, `missing-yield:${productId}`, {
|
const consumptionUnit = normalizeUnit(reference.consumption_unit);
|
||||||
material: `Cadastrar rendimento: ${normalizeText(row.product_name) || productId}`,
|
const materialName = normalizeText(reference.material_name) || normalizeText(reference.material_sku) || 'Material sem cadastro';
|
||||||
plannedKg: suggestedQuantity,
|
|
||||||
priority: 'Crítico',
|
if (consumptionQuantity > 0) {
|
||||||
unit: 'un.',
|
mergeNeedLine(needsByMaterial, `${consumptionUnit}:${normalizeKey(materialName)}`, {
|
||||||
source: 'project_demand',
|
material: materialName,
|
||||||
missingReference: true,
|
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,
|
color: reference.color,
|
||||||
product: {
|
product: {
|
||||||
productId,
|
productId,
|
||||||
name: normalizeText(row.product_name),
|
name: normalizeText(row.product_name),
|
||||||
suggestedQuantity,
|
suggestedQuantity,
|
||||||
quantitySold,
|
quantitySold,
|
||||||
stockQuantity
|
stockQuantity,
|
||||||
|
yieldPerKg
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const materialName = normalizeText(reference.material_name) || normalizeText(reference.material_sku) || 'Material sem cadastro';
|
|
||||||
mergeNeedLine(needsByMaterial, normalizeKey(materialName), {
|
|
||||||
material: materialName,
|
|
||||||
plannedKg: suggestedQuantity / yieldPerKg,
|
|
||||||
priority: 'Atenção',
|
|
||||||
unit: 'kg',
|
|
||||||
source: 'project_demand',
|
|
||||||
color: reference.color,
|
|
||||||
product: {
|
|
||||||
productId,
|
|
||||||
name: normalizeText(row.product_name),
|
|
||||||
suggestedQuantity,
|
|
||||||
quantitySold,
|
|
||||||
stockQuantity,
|
|
||||||
yieldPerKg
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
lots.forEach(lot => {
|
lots.forEach(lot => {
|
||||||
if (lot.unit !== 'kg') return;
|
const need = needsByMaterial.get(`${normalizeUnit(lot.unit)}:${normalizeKey(lot.product)}`);
|
||||||
const need = needsByMaterial.get(normalizeKey(lot.product));
|
if (need) need.stockKg += lot.quantity;
|
||||||
if (need && need.unit === 'kg') need.stockKg += lot.quantity;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
receipts.forEach(receipt => {
|
receipts.forEach(receipt => {
|
||||||
if (receipt.status !== 'pending' || receipt.unit !== 'kg') return;
|
if (receipt.status !== 'pending') return;
|
||||||
const need = needsByMaterial.get(normalizeKey(receipt.product));
|
const need = needsByMaterial.get(`${normalizeUnit(receipt.unit)}:${normalizeKey(receipt.product)}`);
|
||||||
if (need && need.unit === 'kg') need.pendingKg += receipt.quantity;
|
if (need) need.pendingKg += receipt.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(needsByMaterial.values()).map(need => {
|
return Array.from(needsByMaterial.values()).map(need => {
|
||||||
|
|||||||
@@ -92,6 +92,16 @@ const getAverageYield = (reference: ConsumptionReference) => {
|
|||||||
return reference.generalYield;
|
return reference.generalYield;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getReferenceSourceLabel = (reference: ConsumptionReference) => (
|
||||||
|
reference.source === 'tiny_op' ? 'Tiny OP' : 'Manual'
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatConsumptionPerPiece = (reference: ConsumptionReference) => {
|
||||||
|
if (!reference.consumptionQuantity) return '';
|
||||||
|
const unit = reference.consumptionUnit || 'un.';
|
||||||
|
return `${formatNumber(reference.consumptionQuantity, 4)} ${unit}/peça`;
|
||||||
|
};
|
||||||
|
|
||||||
const Registrations = () => {
|
const Registrations = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [catalog, setCatalog] = useState<CatalogSummary>(emptyCatalog);
|
const [catalog, setCatalog] = useState<CatalogSummary>(emptyCatalog);
|
||||||
@@ -663,6 +673,13 @@ const Registrations = () => {
|
|||||||
<span className="rounded-full border border-dark-border bg-dark-input px-2 py-0.5 text-[10px] font-bold text-dark-muted">
|
<span className="rounded-full border border-dark-border bg-dark-input px-2 py-0.5 text-[10px] font-bold text-dark-muted">
|
||||||
{Object.keys(reference.sizeYields || {}).length ? 'Por tamanho' : 'Geral'}
|
{Object.keys(reference.sizeYields || {}).length ? 'Por tamanho' : 'Geral'}
|
||||||
</span>
|
</span>
|
||||||
|
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-bold ${
|
||||||
|
reference.source === 'tiny_op'
|
||||||
|
? 'border-sky-400/30 bg-sky-400/10 text-sky-300'
|
||||||
|
: 'border-dark-border bg-dark-input text-dark-muted'
|
||||||
|
}`}>
|
||||||
|
{getReferenceSourceLabel(reference)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-1 truncate text-sm font-bold text-dark-text">{reference.productName}</h3>
|
<h3 className="mt-1 truncate text-sm font-bold text-dark-text">{reference.productName}</h3>
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
||||||
@@ -680,10 +697,19 @@ const Registrations = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-start gap-3">
|
<div className="flex shrink-0 items-start gap-3">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-lg font-bold text-emerald-300">{formatNumber(getAverageYield(reference), 3)} pç/kg</div>
|
<div className="text-lg font-bold text-emerald-300">
|
||||||
<div className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">
|
{formatConsumptionPerPiece(reference) || `${formatNumber(getAverageYield(reference), 3)} pç/kg`}
|
||||||
{Object.keys(reference.sizeYields || {}).length ? 'media' : 'geral'}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">
|
||||||
|
{reference.consumptionQuantity
|
||||||
|
? 'consumo'
|
||||||
|
: Object.keys(reference.sizeYields || {}).length ? 'media' : 'geral'}
|
||||||
|
</div>
|
||||||
|
{reference.consumptionQuantity && getAverageYield(reference) ? (
|
||||||
|
<div className="mt-1 text-[11px] font-semibold text-dark-muted">
|
||||||
|
{formatNumber(getAverageYield(reference), 3)} pç/kg
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={() => void removeReference(reference)} className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 cursor-pointer">
|
<button type="button" onClick={() => void removeReference(reference)} className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 cursor-pointer">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
|
|||||||
@@ -185,6 +185,10 @@ export interface ConsumptionReference {
|
|||||||
efficiencyPercent: number | null;
|
efficiencyPercent: number | null;
|
||||||
ribGPerPiece: number | null;
|
ribGPerPiece: number | null;
|
||||||
materialCostPerKg: number | null;
|
materialCostPerKg: number | null;
|
||||||
|
consumptionQuantity: number | null;
|
||||||
|
consumptionUnit: string;
|
||||||
|
source: string;
|
||||||
|
lastProductionOrderId: number | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -226,6 +230,8 @@ export type ConsumptionReferencePayload = {
|
|||||||
efficiencyPercent?: number | string | null;
|
efficiencyPercent?: number | string | null;
|
||||||
ribGPerPiece?: number | string | null;
|
ribGPerPiece?: number | string | null;
|
||||||
materialCostPerKg?: number | string | null;
|
materialCostPerKg?: number | string | null;
|
||||||
|
consumptionQuantity?: number | string | null;
|
||||||
|
consumptionUnit?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SupplyReceiptStatus = 'pending' | 'approved';
|
export type SupplyReceiptStatus = 'pending' | 'approved';
|
||||||
@@ -304,6 +310,8 @@ export interface SupplyPurchaseNeed {
|
|||||||
quantitySold: number;
|
quantitySold: number;
|
||||||
stockQuantity: number;
|
stockQuantity: number;
|
||||||
yieldPerKg?: number;
|
yieldPerKg?: number;
|
||||||
|
consumptionQuantity?: number;
|
||||||
|
consumptionUnit?: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user