mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 23:03:18 +00:00
El toast de error salía vacío porque el preview 400 no traía message. Ahora solo se aceptan .xlsx/.xls, y si el Excel no tiene el formato de presupuesto se explica qué columnas o plantilla usar. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
127 lines
5.8 KiB
TypeScript
127 lines
5.8 KiB
TypeScript
import { assertEquals, assertStringIncludes } from "jsr:@std/assert@1";
|
|
import * as XLSX from "xlsx";
|
|
import {
|
|
BUDGET_FORMAT_MESSAGE,
|
|
buildBudgetTemplate,
|
|
excelFileTypeError,
|
|
parseBudgetRows,
|
|
parseMoney,
|
|
previewBudgetExcel,
|
|
} from "./budget.ts";
|
|
|
|
Deno.test("parsea partidas estilo CLAVE DESCRIPCION UNIDAD", () => {
|
|
const parsed = parseBudgetRows([
|
|
["Obra:", "Barda perimetral"],
|
|
["BARDAS PERIMETRALES", "", "", "", "", ""],
|
|
["CLAVE", "DESCRIPCION", "UNIDAD", "CANTIDAD", "PRECIO U", "IMPORTE"],
|
|
["PRE01", "TRAZO Y NIVELACION", "ml", 92, 10, 920],
|
|
["EXC-001", "EXCAVACION", "M3", 8, 850, 6800],
|
|
["", "", "", "", "SUB TOTAL", 398545.97],
|
|
["", "", "", "", "16% IVA", 63767.36],
|
|
["", "", "", "", "TOTAL", 462313.33],
|
|
]);
|
|
assertEquals(parsed.items.length, 2);
|
|
assertEquals(parsed.items[0].code, "PRE01");
|
|
assertEquals(parsed.items[0].quantity, 92);
|
|
assertEquals(parsed.items[0].amount, 920);
|
|
assertEquals(parsed.items[0].chapter, "BARDAS PERIMETRALES");
|
|
assertEquals(parsed.items[1].code, "EXC-001");
|
|
});
|
|
|
|
Deno.test("parsea plantilla con columna CAPITULO", () => {
|
|
const parsed = parseBudgetRows([
|
|
["CAPITULO", "CLAVE", "DESCRIPCION", "UNIDAD", "CANTIDAD", "PRECIO", "IMPORTE"],
|
|
["Acabados", "ACA01", "Aplanado", "M2", 10, 20, 200],
|
|
["Cimentacion", "CIM01", "Zapata", "PZA", 2, 100, 200],
|
|
]);
|
|
assertEquals(parsed.items.map((item) => item.chapter), ["Acabados", "Cimentacion"]);
|
|
assertEquals(parsed.items[0].amount, 200);
|
|
});
|
|
|
|
Deno.test("lee importes con coma o signo", () => {
|
|
assertEquals(parseMoney("$ 1,500.00"), 1500);
|
|
assertEquals(parseMoney("201.47"), 201.47);
|
|
});
|
|
|
|
Deno.test("parsea Opus/Neodata: agrupadores, continuaciones y totales ignorados", () => {
|
|
const parsed = parseBudgetRows([
|
|
["Código", "Concepto", "Unidad", "Cantidad", "P. Unitario", "Importe", "%"],
|
|
["A", "LOCAL L224", "", "", "", "", ""],
|
|
["A001", "PRELIMINARES Y ALBAÑILERÍA", "", "", "", "", ""],
|
|
["10301-001", "Trazo y nivelación manual para establecer ejes, banco", "M2", 61.9, 15.53, 961.31, 0.000847],
|
|
["", "de nivel y referencias, incluye: materiales, mano de", "", 0, 0, 0, 0],
|
|
["", "obra, equipo y herramienta.", "", 0, 0, 0, 0],
|
|
["A001", "TOTAL PRELIMINARES Y ALBAÑILERÍA", "", "", "", 123219.19, 0.1],
|
|
["", "IVA 16.00%", "", "", "", 181433.44, ""],
|
|
["", "TOTAL DEL PRESUPUESTO MOSTRADO:", "", "", "", 1315392.47, ""],
|
|
]);
|
|
assertEquals(parsed.items.length, 1);
|
|
assertEquals(parsed.groups.map((group) => group.code), ["A", "A001"]);
|
|
assertEquals(parsed.groups[0].kind, "lote");
|
|
assertEquals(parsed.groups[1].parentWbs, parsed.groups[0].wbs);
|
|
assertEquals(parsed.items[0].code, "10301-001");
|
|
assertEquals(parsed.items[0].chapter, "PRELIMINARES Y ALBAÑILERÍA");
|
|
assertEquals(parsed.items[0].parentWbs, parsed.groups[1].wbs);
|
|
assertStringIncludes(parsed.items[0].description, "Trazo y nivelación manual");
|
|
assertStringIncludes(parsed.items[0].description, "banco de nivel y referencias");
|
|
assertStringIncludes(parsed.items[0].description, "equipo y herramienta");
|
|
});
|
|
|
|
Deno.test("parsea plantilla WBS lote-capitulo-concepto", () => {
|
|
const parsed = parseBudgetRows([
|
|
["WBS", "TIPO", "CODIGO", "DESCRIPCION", "UNIDAD", "CANTIDAD", "PRECIO"],
|
|
["1", "lote", "A", "LOCAL L224", "", "", ""],
|
|
["1.01", "capitulo", "A001", "Preliminares y albañilería", "", "", ""],
|
|
["1.01.01", "concepto", "10301-001", "Trazo y nivelación manual", "M2", 61.9, 15.53],
|
|
["1.02", "capitulo", "A002", "Instalación hidrosanitaria", "", "", ""],
|
|
["1.02.01", "concepto", "11402-001", "Inodoro", "PZA", 1, 100],
|
|
]);
|
|
assertEquals(parsed.groups.length, 3);
|
|
assertEquals(parsed.groups[0].kind, "lote");
|
|
assertEquals(parsed.groups[1].parentWbs, "1");
|
|
assertEquals(parsed.items.length, 2);
|
|
assertEquals(parsed.items[0].parentWbs, "1.01");
|
|
assertEquals(parsed.items[0].chapter, "Preliminares y albañilería");
|
|
assertEquals(parsed.items[1].parentWbs, "1.02");
|
|
assertEquals(parsed.items[0].amount, 961.31);
|
|
});
|
|
|
|
Deno.test("la plantilla es un xlsx valido", () => {
|
|
const bytes = buildBudgetTemplate();
|
|
assertEquals(bytes[0], 0x50);
|
|
assertEquals(bytes[1], 0x4b);
|
|
const wb = XLSX.read(bytes, { type: "array" });
|
|
assertEquals(wb.SheetNames.includes("PRESUPUESTO"), true);
|
|
assertEquals(wb.SheetNames.includes("INSTRUCCIONES"), true);
|
|
});
|
|
|
|
Deno.test("preview de plantilla no guarda y arma arbol", () => {
|
|
const preview = previewBudgetExcel(buildBudgetTemplate());
|
|
assertEquals(preview.format, "wbs");
|
|
assertEquals(preview.errors.length, 0);
|
|
assertEquals(preview.tree[0]?.code, "A");
|
|
assertEquals(preview.tree[0]?.children.length, 2);
|
|
assertEquals(preview.totals.item_count, 4);
|
|
});
|
|
|
|
Deno.test("rechaza archivos que no son Excel por nombre", () => {
|
|
assertEquals(excelFileTypeError("obra.pdf")?.includes(".xlsx"), true);
|
|
assertEquals(excelFileTypeError("notas.docx")?.includes("«notas.docx»"), true);
|
|
assertEquals(excelFileTypeError("presupuesto.xlsx"), null);
|
|
assertEquals(excelFileTypeError("PRESUPUESTO.XLS"), null);
|
|
});
|
|
|
|
Deno.test("preview de PDF explica que no es Excel", () => {
|
|
const preview = previewBudgetExcel(new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34]));
|
|
assertEquals(preview.totals.item_count, 0);
|
|
assertStringIncludes(preview.errors[0]?.messages[0] || "", "PDF");
|
|
});
|
|
|
|
Deno.test("preview de Excel sin columnas de presupuesto explica el formato", () => {
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([["Nombre", "Notas"], ["Juan", "sin partidas"]]), "Hoja1");
|
|
const bytes = XLSX.write(wb, { type: "array", bookType: "xlsx" });
|
|
const preview = previewBudgetExcel(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
|
assertEquals(preview.totals.item_count, 0);
|
|
assertEquals(preview.errors[0]?.messages[0], BUDGET_FORMAT_MESSAGE);
|
|
});
|