150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
import { Decimal } from '@prisma/client/runtime/library';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
/**
|
|
* fix-pr-quantities.ts — Restore corrupted PR quantities from specData
|
|
*
|
|
* Bug: analyzeImpact() matched ALL PRs by sourceId, so applyQtyChanged
|
|
* overwrote every PR's quantityPerUnit with the value from one changed line.
|
|
*
|
|
* This script reads specData from the active smeta and restores each PR's
|
|
* quantityPerUnit and quantity by matching on description.
|
|
*
|
|
* Usage: npx tsx scripts/fix-pr-quantities.ts [--dry-run]
|
|
*/
|
|
|
|
interface SpecLine {
|
|
description: string;
|
|
unit: string;
|
|
quantityPerUnit: number;
|
|
estimatedPrice: number | null;
|
|
budgetAmount: number | null;
|
|
materialCategory: string;
|
|
sourceReference: string;
|
|
}
|
|
|
|
function normalizeForMatch(s: string): string {
|
|
return s.trim().toLowerCase().replace(/,/g, '.').replace(/\s+/g, ' ');
|
|
}
|
|
|
|
function findSpecLine(prDesc: string, specLines: SpecLine[]): SpecLine | null {
|
|
const normPR = normalizeForMatch(prDesc);
|
|
|
|
// Exact normalized match
|
|
const exact = specLines.find(sl => normalizeForMatch(sl.description) === normPR);
|
|
if (exact) return exact;
|
|
|
|
// Inclusion match
|
|
const incl = specLines.find(sl => {
|
|
const normSL = normalizeForMatch(sl.description);
|
|
return normPR.includes(normSL) || normSL.includes(normPR);
|
|
});
|
|
if (incl) return incl;
|
|
|
|
return null;
|
|
}
|
|
|
|
async function main() {
|
|
const dryRun = process.argv.includes('--dry-run');
|
|
if (dryRun) console.log('=== DRY RUN MODE — no changes will be made ===\n');
|
|
|
|
// Find all LAUNCHED orders
|
|
const orders = await prisma.order.findMany({
|
|
where: { status: { in: ['LAUNCHED', 'IN_PRODUCTION', 'QUALITY_CONTROL'] } },
|
|
select: { id: true, orderCode: true, quantity: true },
|
|
});
|
|
|
|
console.log(`Found ${orders.length} active orders to check.\n`);
|
|
|
|
let totalFixed = 0;
|
|
|
|
for (const order of orders) {
|
|
// Get active smeta with specData
|
|
const smeta = await prisma.smeta.findFirst({
|
|
where: { orderId: order.id, status: { in: ['APPROVED', 'LOCKED'] } },
|
|
orderBy: { version: 'desc' },
|
|
select: { id: true, version: true, specData: true },
|
|
});
|
|
|
|
if (!smeta?.specData) {
|
|
console.log(`[${order.orderCode}] No active smeta with specData — skipping`);
|
|
continue;
|
|
}
|
|
|
|
const specLines = smeta.specData as unknown as SpecLine[];
|
|
if (!Array.isArray(specLines) || specLines.length === 0) {
|
|
console.log(`[${order.orderCode}] specData empty — skipping`);
|
|
continue;
|
|
}
|
|
|
|
// Get PRs
|
|
const prs = await prisma.purchaseRequirement.findMany({
|
|
where: { orderId: order.id, sourceType: 'SMETA_IMPORT' },
|
|
select: { id: true, description: true, quantity: true, quantityPerUnit: true, unit: true },
|
|
});
|
|
|
|
if (prs.length === 0) {
|
|
console.log(`[${order.orderCode}] No SMETA_IMPORT PRs — skipping`);
|
|
continue;
|
|
}
|
|
|
|
// Check if corrupted: all PRs have same quantityPerUnit
|
|
const distinctQPU = new Set(prs.map(pr => Number(pr.quantityPerUnit)));
|
|
if (distinctQPU.size > 2) {
|
|
console.log(`[${order.orderCode}] PRs have ${distinctQPU.size} distinct qPerUnit values — looks OK, skipping`);
|
|
continue;
|
|
}
|
|
|
|
console.log(`[${order.orderCode}] ⚠ Only ${distinctQPU.size} distinct qPerUnit value(s) for ${prs.length} PRs — CORRUPTED, fixing...`);
|
|
|
|
let fixed = 0;
|
|
let notFound = 0;
|
|
|
|
for (const pr of prs) {
|
|
const specLine = findSpecLine(pr.description, specLines);
|
|
if (!specLine) {
|
|
console.log(` ✗ No specLine match for PR "${pr.description}" — skipping`);
|
|
notFound++;
|
|
continue;
|
|
}
|
|
|
|
const correctQPU = specLine.quantityPerUnit;
|
|
const correctQty = correctQPU * order.quantity;
|
|
const currentQPU = Number(pr.quantityPerUnit);
|
|
|
|
if (Math.abs(currentQPU - correctQPU) < 0.0001) {
|
|
continue; // already correct
|
|
}
|
|
|
|
console.log(` ✓ "${pr.description}": qPerUnit ${currentQPU} → ${correctQPU}, qty ${Number(pr.quantity)} → ${correctQty}`);
|
|
|
|
if (!dryRun) {
|
|
await prisma.purchaseRequirement.update({
|
|
where: { id: pr.id },
|
|
data: {
|
|
quantityPerUnit: new Decimal(correctQPU),
|
|
quantity: new Decimal(correctQty),
|
|
revisionAlert: false,
|
|
revisionNote: null,
|
|
},
|
|
});
|
|
}
|
|
fixed++;
|
|
}
|
|
|
|
console.log(` → Fixed ${fixed} PRs, ${notFound} unmatched\n`);
|
|
totalFixed += fixed;
|
|
}
|
|
|
|
console.log(`\n=== DONE: ${totalFixed} PRs ${dryRun ? 'would be' : ''} fixed ===`);
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
prisma.$disconnect();
|
|
process.exit(1);
|
|
});
|