120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import * as XLSX from "xlsx";
|
||
|
||
export type ParsedImportLine = {
|
||
name: string;
|
||
unit: string;
|
||
quantity: number;
|
||
unitPrice: number;
|
||
gstRate: number;
|
||
};
|
||
|
||
export type ParsedImport = {
|
||
poNumber: string;
|
||
piQuotationNo: string;
|
||
placeOfDelivery: string;
|
||
tcDelivery: string;
|
||
tcDispatch: string;
|
||
tcInspection: string;
|
||
tcTransitInsurance: string;
|
||
tcPaymentTerms: string;
|
||
tcOthers: string;
|
||
vendorName: string;
|
||
vendorAddress: string;
|
||
vendorContact: string;
|
||
lineItems: ParsedImportLine[];
|
||
};
|
||
|
||
export function cellStr(sheet: XLSX.WorkSheet, row: number, col: number): string {
|
||
const addr = XLSX.utils.encode_cell({ r: row, c: col });
|
||
const cell = sheet[addr];
|
||
if (!cell) return "";
|
||
return String(cell.v ?? "").trim();
|
||
}
|
||
|
||
export function cellNum(sheet: XLSX.WorkSheet, row: number, col: number): number {
|
||
const addr = XLSX.utils.encode_cell({ r: row, c: col });
|
||
const cell = sheet[addr];
|
||
if (!cell) return 0;
|
||
const v = parseFloat(String(cell.v));
|
||
return isNaN(v) ? 0 : v;
|
||
}
|
||
|
||
export function parseSheet(sheet: XLSX.WorkSheet): ParsedImport {
|
||
const poNumber = cellStr(sheet, 4, 2);
|
||
const piQuotationNo = cellStr(sheet, 5, 2);
|
||
const placeOfDelivery = cellStr(sheet, 8, 2);
|
||
const vendorName = cellStr(sheet, 12, 2);
|
||
const vendorAddress = cellStr(sheet, 12, 3);
|
||
const vendorContact = cellStr(sheet, 13, 2);
|
||
|
||
// T&C from instruction rows 28–33 (col 1)
|
||
const tcDelivery = cellStr(sheet, 28, 1).replace(/^DELIVERY\s*:\s*/i, "").trim();
|
||
const tcDispatch = cellStr(sheet, 29, 1).replace(/^DISPATCH INSTRUCTIONS:\s*/i, "").trim();
|
||
const tcInspection = cellStr(sheet, 30, 1).replace(/^INSPECTION\s*:\s*/i, "").trim();
|
||
const tcTransitInsurance = cellStr(sheet, 31, 1).replace(/^TRANSIT INSURANCE:\s*/i, "").trim();
|
||
const tcPaymentTerms = cellStr(sheet, 32, 1).replace(/^PAYMENT TERMS:\s*/i, "").trim();
|
||
const tcOthers = cellStr(sheet, 33, 1).trim();
|
||
|
||
const lineItems: ParsedImportLine[] = [];
|
||
for (let r = 15; r <= 100; r++) {
|
||
const sn = cellStr(sheet, r, 0);
|
||
const desc = cellStr(sheet, r, 1);
|
||
|
||
// "INSTRUCTIONS TO VENDORS" in col 0 signals the T&C section — stop here
|
||
if (sn.toUpperCase().includes("INSTRUCTION")) break;
|
||
|
||
if (!desc && !sn) continue;
|
||
if (!desc) continue;
|
||
|
||
if (desc.toLowerCase().includes("total") || desc.toLowerCase().includes("grand")) break;
|
||
|
||
const unitRaw = cellStr(sheet, r, 3);
|
||
const qty = cellNum(sheet, r, 4);
|
||
const unitPrice = cellNum(sheet, r, 5);
|
||
|
||
// Skip rows with no quantity and no unit price (T&C text rows, etc.)
|
||
if (qty === 0 && unitPrice === 0) continue;
|
||
|
||
const gstRaw = cellNum(sheet, r, 7);
|
||
const gstRate = gstRaw > 1 ? gstRaw / 100 : gstRaw;
|
||
|
||
lineItems.push({
|
||
name: desc,
|
||
unit: unitRaw || "pc",
|
||
quantity: qty || 1,
|
||
unitPrice,
|
||
gstRate: gstRate || 0.18,
|
||
});
|
||
}
|
||
|
||
return {
|
||
poNumber,
|
||
piQuotationNo,
|
||
placeOfDelivery,
|
||
tcDelivery,
|
||
tcDispatch,
|
||
tcInspection,
|
||
tcTransitInsurance,
|
||
tcPaymentTerms,
|
||
tcOthers,
|
||
vendorName,
|
||
vendorAddress,
|
||
vendorContact,
|
||
lineItems,
|
||
};
|
||
}
|
||
|
||
export function parseWorkbook(buffer: Buffer): ParsedImport[] {
|
||
const workbook = XLSX.read(buffer, { type: "buffer" });
|
||
const results: ParsedImport[] = [];
|
||
for (const sheetName of workbook.SheetNames) {
|
||
const sheet = workbook.Sheets[sheetName];
|
||
try {
|
||
const parsed = parseSheet(sheet);
|
||
if (parsed.lineItems.length > 0) results.push(parsed);
|
||
} catch {
|
||
// skip unparseable sheets
|
||
}
|
||
}
|
||
return results;
|
||
}
|