Add editable cutting rules
This commit is contained in:
@@ -70,6 +70,38 @@ test('buildCutPlan subtracts open production quantity from suggested cut need',
|
||||
assert.equal(row.suggestedCutQuantity, 20);
|
||||
});
|
||||
|
||||
test('buildCutPlan estimates rolls when a family yield is configured', () => {
|
||||
const plan = buildCutPlan([product({})], range, 14, {}, { familyYields: { BLCS: 50 } });
|
||||
const [row] = plan.needRows;
|
||||
|
||||
assert.equal(row.suggestedCutQuantity, 120);
|
||||
assert.equal(row.estimatedRolls, 3);
|
||||
assert.deepEqual(row.issues, []);
|
||||
assert.equal(plan.summary.estimatedRolls, 3);
|
||||
});
|
||||
|
||||
test('buildCutPlan applies per-product planning overrides', () => {
|
||||
const plan = buildCutPlan([
|
||||
product({
|
||||
id: 'SKU-2',
|
||||
name: 'BONÉ PRETO',
|
||||
quantitySold: 70,
|
||||
stock: 0
|
||||
})
|
||||
], range, 7, {}, {
|
||||
familyYields: { BLCS: 35 },
|
||||
productOverrides: {
|
||||
'SKU-2': { familyKey: 'BLCS', color: 'Preto', size: 'M' }
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(plan.needRows[0].family.key, 'BLCS');
|
||||
assert.equal(plan.needRows[0].color, 'Preto');
|
||||
assert.equal(plan.needRows[0].size, 'M');
|
||||
assert.equal(plan.needRows[0].estimatedRolls, 2);
|
||||
assert.deepEqual(plan.needRows[0].issues, []);
|
||||
});
|
||||
|
||||
test('buildOpenProductionByProductId matches open OPs by SKU and normalized product variant', () => {
|
||||
const products = [
|
||||
product({ id: 'SKU-1' }),
|
||||
|
||||
@@ -12,6 +12,17 @@ export interface CutFamilyRule {
|
||||
unitsPerRoll: number | null;
|
||||
}
|
||||
|
||||
export interface CutProductOverride {
|
||||
familyKey?: CutFamilyKey | '';
|
||||
color?: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export interface CutPlanOptions {
|
||||
familyYields?: Partial<Record<CutFamilyKey, number>>;
|
||||
productOverrides?: Record<string, CutProductOverride>;
|
||||
}
|
||||
|
||||
export interface CutPlanSkuRow extends ProductAnalyticsItem {
|
||||
family: CutFamilyRule;
|
||||
baseName: string;
|
||||
@@ -99,6 +110,11 @@ export const OUTROS_RULE: CutFamilyRule = {
|
||||
unitsPerRoll: null
|
||||
};
|
||||
|
||||
const FAMILY_RULES_BY_KEY = new Map<CutFamilyKey, CutFamilyRule>([
|
||||
...CUT_FAMILY_RULES.map(rule => [rule.key, rule] as const),
|
||||
[OUTROS_RULE.key, OUTROS_RULE]
|
||||
]);
|
||||
|
||||
export const getRangeDays = (range: DateRange) => {
|
||||
const start = new Date(range.start);
|
||||
const end = new Date(range.end);
|
||||
@@ -121,6 +137,14 @@ export const classifyCutFamily = (baseName: string): CutFamilyRule => {
|
||||
)) || OUTROS_RULE;
|
||||
};
|
||||
|
||||
const ruleWithConfiguredYield = (rule: CutFamilyRule, familyYields?: Partial<Record<CutFamilyKey, number>>): CutFamilyRule => {
|
||||
const configuredYield = familyYields?.[rule.key];
|
||||
return {
|
||||
...rule,
|
||||
unitsPerRoll: configuredYield && configuredYield > 0 ? configuredYield : rule.unitsPerRoll
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeMatchText = (value: string) => normalizeProductText(value)
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
@@ -178,13 +202,18 @@ export const buildCutPlan = (
|
||||
products: ProductAnalyticsItem[],
|
||||
dateRange: DateRange,
|
||||
targetCoverageDays: number,
|
||||
openProductionByProductId: Record<string, number> = {}
|
||||
openProductionByProductId: Record<string, number> = {},
|
||||
options: CutPlanOptions = {}
|
||||
): CutPlan => {
|
||||
const rangeDays = getRangeDays(dateRange);
|
||||
|
||||
const rows = products.map<CutPlanSkuRow>(product => {
|
||||
const metadata = parseProductName(product.name);
|
||||
const family = classifyCutFamily(metadata.baseName);
|
||||
const override = options.productOverrides?.[product.id];
|
||||
const overrideRule = override?.familyKey ? FAMILY_RULES_BY_KEY.get(override.familyKey) : undefined;
|
||||
const family = ruleWithConfiguredYield(overrideRule || classifyCutFamily(metadata.baseName), options.familyYields);
|
||||
const color = normalizeProductText(override?.color || metadata.color);
|
||||
const size = normalizeProductText(override?.size || metadata.size).toUpperCase();
|
||||
const dailySales = product.quantitySold / rangeDays;
|
||||
const projectedDemand = dailySales * targetCoverageDays;
|
||||
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
||||
@@ -197,16 +226,16 @@ export const buildCutPlan = (
|
||||
const issues: CutIssue[] = [];
|
||||
|
||||
if (family.key === 'OUTROS') issues.push('missing_family_rule');
|
||||
if (!metadata.color) issues.push('missing_color');
|
||||
if (!metadata.size) issues.push('missing_size');
|
||||
if (!color) issues.push('missing_color');
|
||||
if (!size) issues.push('missing_size');
|
||||
if (suggestedCutQuantity > 0 && family.key !== 'OUTROS' && !family.unitsPerRoll) issues.push('missing_yield_rule');
|
||||
|
||||
return {
|
||||
...product,
|
||||
family,
|
||||
baseName: metadata.baseName,
|
||||
color: metadata.color,
|
||||
size: metadata.size,
|
||||
color,
|
||||
size,
|
||||
dailySales,
|
||||
projectedDemand,
|
||||
openProductionQuantity,
|
||||
|
||||
Reference in New Issue
Block a user