panels-origin/api/invoices_http.ts
Cursor Agent caf40e29fb
feat: facturas, ajustes de nómina, finiquitos, dispersión e IMSS
Reutiliza el catálogo de proveedores y agrega carga de facturas, conceptos de la semana, préstamos editables, finiquitos, dispersión, historial de personal, reportes y jornal de cotización.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-24 16:40:08 +00:00

109 lines
4.2 KiB
TypeScript

import type { Hono } from "hono";
import * as XLSX from "xlsx";
import type { AuthUser } from "./auth.ts";
import { tenantScope } from "./auth.ts";
import { requireCoreAuth } from "./scope.ts";
import { requirePermission } from "./permissions.ts";
import { callCoreFn } from "./rpc.ts";
import { respondApiError, respondRpc, routeLabel } from "./http_errors.ts";
import type { Db } from "./db.ts";
type App = Hono<{ Variables: { user: AuthUser; db: Db } }>;
function tid(c: { get: (k: "user") => AuthUser }): number {
return tenantScope(c.get("user")) ?? 0;
}
function num(v: unknown): number {
const n = Number(String(v ?? "").replace(/[$,\s]/g, ""));
return Number.isFinite(n) ? n : 0;
}
function mapInvoiceRow(raw: Record<string, unknown>) {
const r: Record<string, string> = {};
for (const [k, v] of Object.entries(raw)) {
r[k.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().trim()] = String(v ?? "").trim();
}
const pick = (...keys: string[]) => keys.map((k) => r[k]).find((v) => v) ?? "";
return {
direction: pick("direccion", "dirección", "tipo") || "recibida",
rfc: pick("rfc"),
supplier_name: pick("proveedor", "nombre"),
uuid: pick("uuid"),
serie: pick("serie"),
folio: pick("folio"),
invoice_date: pick("fecha", "invoice_date"),
description: pick("descripcion", "descripción", "concepto"),
quantity: num(pick("cantidad")),
unit: pick("unidad"),
amount: num(pick("importe", "subtotal")),
iva: num(pick("iva")),
total: num(pick("total")),
project_code: pick("obra", "clave obra", "proyecto"),
item_code: pick("clave concepto", "concepto presupuesto", "partida"),
};
}
export function registerInvoiceRoutes(app: App) {
app.get("/v1/invoices", ...requireCoreAuth, requirePermission("invoices.view"), async (c) => {
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_invoice_list", {
tenant_id: tid(c),
direction: c.req.query("direction"),
supplier_id: c.req.query("supplier_id"),
rfc: c.req.query("rfc"),
uuid: c.req.query("uuid"),
folio: c.req.query("folio"),
q: c.req.query("q"),
amount_min: c.req.query("amount_min"),
amount_max: c.req.query("amount_max"),
date_from: c.req.query("date_from"),
date_to: c.req.query("date_to"),
project_id: c.req.query("project_id"),
classified: c.req.query("classified"),
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.get("/v1/invoices/:id", ...requireCoreAuth, requirePermission("invoices.view"), async (c) => {
const db = c.get("db");
return respondRpc(c, await callCoreFn(db, "core.fn_invoice_get", { id: Number(c.req.param("id")) }, { route: routeLabel(c) }));
});
app.post("/v1/invoices/import", ...requireCoreAuth, requirePermission("invoices.create"), async (c) => {
const body = await c.req.parseBody();
const companyId = Number(body.company_id);
const file = body.file;
if (!(file instanceof File)) {
return respondApiError(c, "VALIDATION", "Adjunte el Excel de facturas", { route: routeLabel(c) });
}
const wb = XLSX.read(new Uint8Array(await file.arrayBuffer()), { type: "array", cellDates: true });
const sheet = wb.Sheets[wb.SheetNames[0]];
const raw = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, { defval: "", raw: false });
const rows = raw.map(mapInvoiceRow).map((row) => {
const date = row.invoice_date;
const iso = /^\d{4}-\d{2}-\d{2}/.test(date)
? date.slice(0, 10)
: /^\d{2}\/\d{2}\/\d{4}/.test(date)
? `${date.slice(6, 10)}-${date.slice(3, 5)}-${date.slice(0, 2)}`
: date;
return { ...row, invoice_date: iso };
});
const db = c.get("db");
return respondRpc(c, await callCoreFn(db, "core.fn_invoice_import", {
tenant_id: tid(c),
company_id: companyId,
rows,
}, { route: routeLabel(c) }));
});
app.post("/v1/invoices/:id/classify", ...requireCoreAuth, requirePermission("invoices.update"), async (c) => {
const body = await c.req.json<Record<string, unknown>>();
const db = c.get("db");
return respondRpc(c, await callCoreFn(db, "core.fn_invoice_classify", {
...body,
id: Number(c.req.param("id")),
tenant_id: tid(c),
}, { route: routeLabel(c) }));
});
}