mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 15:33:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Resumen Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían. ## Qué queda disponible - Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC. - Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada. - Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios. - Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos. - Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar. - Dispersión: corte semanal, corrección antes de definitiva y comprobante. - Historial de alta, baja y reingreso en la ficha, más movimientos IMSS. - Reporte por periodo en Excel y PDF. - Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel. ## Verificación `deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-5e23dc3b-921e-4091-9650-7091d3a52bbf&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
109 lines
4.2 KiB
TypeScript
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) }));
|
|
});
|
|
}
|