164 lines
6.6 KiB
TypeScript
164 lines
6.6 KiB
TypeScript
import { strict as assert } from 'node:assert';
|
|
import { test } from 'node:test';
|
|
import * as XLSX from 'xlsx';
|
|
import type { SaleTemplate } from '@/api/offlineSales';
|
|
import {
|
|
buildTemplateWorkbook,
|
|
parseSalesWorkbook,
|
|
summarise,
|
|
templateFilename,
|
|
toBills,
|
|
} from './salesSheet';
|
|
|
|
const TEMPLATE: SaleTemplate = {
|
|
tenantid: 1135,
|
|
locationid: 0,
|
|
locations: [
|
|
{ locationid: 1, locationname: 'R mart — RS Puram', productcount: 2 },
|
|
{ locationid: 2, locationname: 'R mart — Peelamedu', productcount: 1 },
|
|
],
|
|
products: [
|
|
{ tenantid: 1135, locationid: 1, locationname: 'R mart — RS Puram', productid: 9001, productname: 'Aachi Chilli Powder 100gm', currentstock: 24, price: 35, taxpercent: 5 },
|
|
{ tenantid: 1135, locationid: 1, locationname: 'R mart — RS Puram', productid: 9004, productname: 'Amul Butter 500g', currentstock: 2, price: 265, taxpercent: 12 },
|
|
{ tenantid: 1135, locationid: 2, locationname: 'R mart — Peelamedu', productid: 9007, productname: 'Nestle Milkmaid 400g', currentstock: 40, price: 160, taxpercent: 12 },
|
|
],
|
|
};
|
|
|
|
/** Round-trip helper: build the workbook, fill in cells, hand back a buffer. */
|
|
function filled(edits: Record<number, Record<string, string | number>>): ArrayBuffer {
|
|
const workbook = buildTemplateWorkbook(TEMPLATE);
|
|
const sheet = workbook.Sheets['Sales'] as XLSX.WorkSheet;
|
|
const header = XLSX.utils.sheet_to_json<unknown[]>(sheet, { header: 1 })[0] as string[];
|
|
const column = (name: string) =>
|
|
header.findIndex((cell) => String(cell).toLowerCase().startsWith(name));
|
|
|
|
for (const [row, values] of Object.entries(edits)) {
|
|
for (const [name, value] of Object.entries(values)) {
|
|
const address = XLSX.utils.encode_cell({ r: Number(row) - 1, c: column(name) });
|
|
sheet[address] = typeof value === 'number' ? { t: 'n', v: value } : { t: 's', v: value };
|
|
}
|
|
}
|
|
const out = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }) as ArrayBuffer;
|
|
return out;
|
|
}
|
|
|
|
test('the workbook carries every product, ids filled in', () => {
|
|
const workbook = buildTemplateWorkbook(TEMPLATE);
|
|
assert.deepEqual(workbook.SheetNames, ['Sales', 'Store Info', 'Instructions']);
|
|
const rows = XLSX.utils.sheet_to_json<unknown[]>(workbook.Sheets['Sales'] as XLSX.WorkSheet, {
|
|
header: 1,
|
|
});
|
|
assert.equal(rows.length, 4, 'header plus three products');
|
|
assert.equal((rows[1] as unknown[])[3], 9001, 'productid is pre-filled');
|
|
assert.equal((rows[1] as unknown[])[6], '', 'qtysold is left empty, never defaulted to 0');
|
|
});
|
|
|
|
test('the filename says what the workbook spans', () => {
|
|
assert.equal(templateFilename(TEMPLATE, '2026-08-25'), 'counter-sales-all-stores-2026-08-25.xlsx');
|
|
assert.equal(
|
|
templateFilename({ ...TEMPLATE, locations: [TEMPLATE.locations[0] as never] }, '2026-08-25'),
|
|
'counter-sales-r-mart-rs-puram-2026-08-25.xlsx',
|
|
);
|
|
});
|
|
|
|
test('rows with no quantity are skipped, not reported as problems', () => {
|
|
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 3 } }), { tenantid: 1135 });
|
|
assert.deepEqual(parsed.fatal, []);
|
|
assert.equal(parsed.rows.length, 1);
|
|
assert.equal(parsed.skipped, 2);
|
|
assert.equal(parsed.rows[0]?.productid, 9001);
|
|
});
|
|
|
|
test('a file from another account is refused whole', () => {
|
|
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1 } }), { tenantid: 9999 });
|
|
assert.ok(parsed.fatal[0]?.includes('different account'));
|
|
});
|
|
|
|
test('selling more than the shelf holds is an error on that row', () => {
|
|
const parsed = parseSalesWorkbook(filled({ 3: { qtysold: 5 } }), { tenantid: 1135 });
|
|
assert.equal(parsed.rows.length, 1);
|
|
assert.ok(parsed.rows[0]?.errors.some((problem) => problem.includes('only 2 in stock')));
|
|
});
|
|
|
|
test('a pinned upload refuses rows belonging to another branch', () => {
|
|
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1 }, 4: { qtysold: 1 } }), {
|
|
tenantid: 1135,
|
|
locationid: 1,
|
|
});
|
|
const foreign = parsed.rows.find((row) => row.locationid === 2);
|
|
assert.ok(foreign, 'the row is still parsed, so the operator can see it');
|
|
assert.ok(foreign?.errors.some((problem) => problem.includes('another store')));
|
|
const own = parsed.rows.find((row) => row.locationid === 1);
|
|
assert.deepEqual(own?.errors, []);
|
|
});
|
|
|
|
test('an unpriced line is a warning, not a blocker', () => {
|
|
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1, unitprice: 0 } }), {
|
|
tenantid: 1135,
|
|
});
|
|
assert.deepEqual(parsed.rows[0]?.errors, []);
|
|
assert.ok(parsed.rows[0]?.warnings.some((note) => note.includes('no price')));
|
|
});
|
|
|
|
test('an unknown payment mode is caught before upload', () => {
|
|
const parsed = parseSalesWorkbook(filled({ 2: { qtysold: 1, paymentmode: 'cheque' } }), {
|
|
tenantid: 1135,
|
|
});
|
|
assert.ok(parsed.rows[0]?.errors.some((problem) => problem.includes('not Cash, Card or UPI')));
|
|
});
|
|
|
|
test('the same bill number at two branches stays two bills', () => {
|
|
const parsed = parseSalesWorkbook(
|
|
filled({ 2: { qtysold: 1, billno: '1' }, 4: { qtysold: 1, billno: '1' } }),
|
|
{ tenantid: 1135 },
|
|
);
|
|
const bills = toBills(parsed.rows);
|
|
assert.equal(bills.length, 2);
|
|
assert.deepEqual(
|
|
bills.map((bill) => bill.locationid).sort(),
|
|
[1, 2],
|
|
);
|
|
});
|
|
|
|
test('rows sharing a bill number become one bill with its lines', () => {
|
|
const parsed = parseSalesWorkbook(
|
|
filled({ 2: { qtysold: 2, billno: 'A-7' }, 3: { qtysold: 1, billno: 'A-7' } }),
|
|
{ tenantid: 1135 },
|
|
);
|
|
const bills = toBills(parsed.rows);
|
|
assert.equal(bills.length, 1);
|
|
assert.equal(bills[0]?.items.length, 2);
|
|
assert.equal(bills[0]?.billno, 'A-7');
|
|
});
|
|
|
|
test('bill-level fields are taken from whichever row carries them', () => {
|
|
const parsed = parseSalesWorkbook(
|
|
filled({
|
|
2: { qtysold: 1, billno: 'B-2' },
|
|
3: { qtysold: 1, billno: 'B-2', paymentmode: 'UPI', customername: 'Kavitha' },
|
|
}),
|
|
{ tenantid: 1135 },
|
|
);
|
|
const bill = toBills(parsed.rows)[0];
|
|
assert.equal(bill?.paymentmode, 'UPI');
|
|
assert.equal(bill?.customername, 'Kavitha');
|
|
});
|
|
|
|
test('the preview total is unit price by quantity, less discount', () => {
|
|
const parsed = parseSalesWorkbook(
|
|
filled({ 2: { qtysold: 4, discountamount: 20 }, 4: { qtysold: 2 } }),
|
|
{ tenantid: 1135 },
|
|
);
|
|
const totals = summarise(parsed.rows);
|
|
assert.equal(totals.lines, 2);
|
|
assert.equal(totals.units, 6);
|
|
assert.equal(totals.amount, 35 * 4 - 20 + 160 * 2);
|
|
assert.equal(totals.stores, 2);
|
|
assert.equal(totals.bills, 2, 'no bill number means one bill per branch');
|
|
});
|
|
|
|
test('a file with nothing filled in says so rather than uploading nothing', () => {
|
|
const parsed = parseSalesWorkbook(filled({}), { tenantid: 1135 });
|
|
assert.ok(parsed.fatal[0]?.includes('No sold quantities'));
|
|
});
|