Informes de facturas de cliente

This commit is contained in:
David Arranz 2025-11-20 13:03:54 +01:00
parent 156dc9db0f
commit 747d11a956
23 changed files with 167 additions and 161 deletions

View File

@ -52,7 +52,7 @@
// other vscode settings // other vscode settings
"[handlebars]": { "[handlebars]": {
"editor.defaultFormatter": "vscode.html-language-features" "editor.defaultFormatter": "mfeckies.handlebars-formatter"
}, },
"[sql]": { "[sql]": {
"editor.defaultFormatter": "cweijan.vscode-mysql-client2" "editor.defaultFormatter": "cweijan.vscode-mysql-client2"

View File

@ -292,6 +292,14 @@
"enabled": true "enabled": true
} }
}, },
"html": {
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
}
},
"overrides": [ "overrides": [
{ {
"includes": ["**/*.test.{js,ts,tsx}", "**/*.spec.{js,ts,tsx}", "**/__tests__/**"], "includes": ["**/*.test.{js,ts,tsx}", "**/*.spec.{js,ts,tsx}", "**/__tests__/**"],

View File

@ -1,4 +1,4 @@
import { IPresenter } from "./presenter.interface"; import type { IPresenter } from "./presenter.interface";
/** /**
* 🔑 Claves de proyección comunes para seleccionar presenters * 🔑 Claves de proyección comunes para seleccionar presenters

View File

@ -1,10 +1,11 @@
import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server"; import { type Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import httpStatus from "http-status"; import httpStatus from "http-status";
import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper";
import { type ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper";
import { import {
ApiError, type ApiError,
ConflictApiError, ConflictApiError,
ForbiddenApiError, ForbiddenApiError,
InternalApiError, InternalApiError,
@ -13,7 +14,7 @@ import {
UnavailableApiError, UnavailableApiError,
ValidationApiError, ValidationApiError,
} from "./errors"; } from "./errors";
import { GuardFn } from "./express-guards"; import type { GuardFn } from "./express-guards";
export abstract class ExpressController { export abstract class ExpressController {
protected req!: Request; protected req!: Request;
@ -125,6 +126,15 @@ export abstract class ExpressController {
return this.res.send(pdfBuffer); return this.res.send(pdfBuffer);
} }
public downloadHTML(htmlString: string) {
this.res.set({
"Content-Type": "text/html; charset=utf-8",
"Content-Length": Buffer.byteLength(htmlString, "utf-8"),
});
return this.res.send(htmlString);
}
protected clientError(message: string, errors?: any[] | any) { protected clientError(message: string, errors?: any[] | any) {
return this.handleApiError( return this.handleApiError(
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]) new ValidationApiError(message, Array.isArray(errors) ? errors : [errors])

View File

@ -5,11 +5,6 @@ import { lookup } from "mime-types";
import { TemplateResolver } from "./template-resolver"; import { TemplateResolver } from "./template-resolver";
interface AssetHelperOptions {
baseDir: string;
mode: "local_file" | "base64";
}
export class HandlebarsTemplateResolver extends TemplateResolver { export class HandlebarsTemplateResolver extends TemplateResolver {
protected readonly hbs = Handlebars.create(); protected readonly hbs = Handlebars.create();
protected registered = false; protected registered = false;
@ -18,18 +13,16 @@ export class HandlebarsTemplateResolver extends TemplateResolver {
/** /**
* Registra el helper "asset". * Registra el helper "asset".
* *
* - Si `mode === "local_file"` devuelve file://...
* - Si `mode === "base64"`:
* - Si el fichero termina en .b64 se asume que el contenido ya es base64 * - Si el fichero termina en .b64 se asume que el contenido ya es base64
* - Si no se lee binario y se convierte a base64 * - Si no se lee binario y se convierte a base64
*/ */
protected registerAssetHelper(templateDir: string, mode: "local_file" | "base64") { protected registerAssetHelper(templateDir: string) {
// Si ya está registrado, no hacer nada // Si ya está registrado, no hacer nada
if (this.registered) return; if (this.registered) return;
this.hbs.registerHelper("asset", (resource: string) => { this.hbs.registerHelper("asset", (resource: string) => {
const assetPath = this.resolveAssetPath(templateDir, resource); const assetPath = this.resolveAssetPath(templateDir, resource);
const cacheKey = `${mode}:${assetPath}`; const cacheKey = `${assetPath}`;
// 1) Caché en memoria // 1) Caché en memoria
const cached = this.assetCache.get(cacheKey); const cached = this.assetCache.get(cacheKey);
@ -41,18 +34,12 @@ export class HandlebarsTemplateResolver extends TemplateResolver {
throw new Error(`Asset not found: ${assetPath}`); throw new Error(`Asset not found: ${assetPath}`);
} }
// 2) Modo "local_file": solo devolver la ruta de fichero
if (mode === "local_file") {
const value = `file://${assetPath.replace(/\\/g, "/")}`;
this.assetCache.set(cacheKey, value);
return value;
}
// 3) Modo "base64" // 3) Modo "base64"
const isPreencoded = assetPath.endsWith(".b64"); const isPreencoded = assetPath.endsWith(".b64");
let base64: string; let base64: string;
let mimeType: string; let mimeType: string;
let value: string;
if (isPreencoded) { if (isPreencoded) {
// Fichero ya contiene el base64 en texto plano // Fichero ya contiene el base64 en texto plano
@ -61,14 +48,24 @@ export class HandlebarsTemplateResolver extends TemplateResolver {
// Para el MIME usamos el nombre "original" sin .b64 // Para el MIME usamos el nombre "original" sin .b64
const mimeLookupPath = assetPath.replace(/\.b64$/, ""); const mimeLookupPath = assetPath.replace(/\.b64$/, "");
mimeType = (lookup(mimeLookupPath) || "application/octet-stream") as string; mimeType = (lookup(mimeLookupPath) || "application/octet-stream") as string;
value = `data:${mimeType};base64,${base64}`;
} else { } else {
// Fichero binario normal → convertimos a base64 // Fichero normal
const buffer = readFileSync(assetPath);
mimeType = (lookup(assetPath) || "application/octet-stream") as string; // Si es un CSS no se convierte y se incrusta
base64 = buffer.toString("base64"); const isCSS = assetPath.endsWith(".css");
if (isCSS) {
const buffer = readFileSync(assetPath);
value = buffer.toString();
} else {
// En otro caso, se transforma a Base64
const buffer = readFileSync(assetPath);
mimeType = (lookup(assetPath) || "application/octet-stream") as string;
base64 = buffer.toString("base64");
value = `data:${mimeType};base64,${base64}`;
}
} }
const value = `data:${mimeType};base64,${base64}`;
this.assetCache.set(cacheKey, value); this.assetCache.set(cacheKey, value);
return value; return value;
}); });
@ -87,14 +84,12 @@ export class HandlebarsTemplateResolver extends TemplateResolver {
companySlug: string, companySlug: string,
templateName: string templateName: string
): Handlebars.TemplateDelegate { ): Handlebars.TemplateDelegate {
const isDev = process.env.NODE_ENV === "development";
// 1) Directorio de plantillas // 1) Directorio de plantillas
const templateDir = this.resolveTemplateDirectory(module, companySlug); const templateDir = this.resolveTemplateDirectory(module, companySlug);
const templatePath = this.resolveTemplatePath(module, companySlug, templateName); // 2) Path completo del template const templatePath = this.resolveTemplatePath(module, companySlug, templateName); // 2) Path completo del template
const source = this.readTemplateFile(templatePath); // Contenido const source = this.readTemplateFile(templatePath); // Contenido
this.registerAssetHelper(templateDir, isDev ? "local_file" : "base64"); this.registerAssetHelper(templateDir);
// 5) Compilar // 5) Compilar
return this.compile(source); return this.compile(source);

View File

@ -55,8 +55,7 @@
"libphonenumber-js": "^1.12.7", "libphonenumber-js": "^1.12.7",
"lucide-react": "^0.503.0", "lucide-react": "^0.503.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"puppeteer": "^24.20.0", "puppeteer": "^24.30.0",
"puppeteer-report": "^3.2.0",
"react-hook-form": "^7.58.1", "react-hook-form": "^7.58.1",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",

View File

@ -21,7 +21,7 @@ export class IssuedInvoiceVerifactuFullPresenter extends Presenter {
}), }),
() => ({ () => ({
id: "", id: "",
status: "", status: "Pendiente",
url: "", url: "",
qr_code: "", qr_code: "",
}) })

View File

@ -10,6 +10,7 @@ type ReportIssuedInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
companySlug: string; companySlug: string;
invoice_id: string; invoice_id: string;
format: "pdf" | "html";
}; };
export class ReportIssuedInvoiceUseCase { export class ReportIssuedInvoiceUseCase {
@ -20,7 +21,7 @@ export class ReportIssuedInvoiceUseCase {
) {} ) {}
public async execute(params: ReportIssuedInvoiceUseCaseInput) { public async execute(params: ReportIssuedInvoiceUseCaseInput) {
const { invoice_id, companyId, companySlug } = params; const { invoice_id, companyId, companySlug, format } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);
@ -32,7 +33,7 @@ export class ReportIssuedInvoiceUseCase {
const pdfPresenter = this.presenterRegistry.getPresenter({ const pdfPresenter = this.presenterRegistry.getPresenter({
resource: "issued-invoice", resource: "issued-invoice",
projection: "REPORT", projection: "REPORT",
format: "PDF", format,
}) as IssuedInvoiceReportPDFPresenter; }) as IssuedInvoiceReportPDFPresenter;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
@ -48,10 +49,18 @@ export class ReportIssuedInvoiceUseCase {
} }
const invoice = invoiceOrError.data; const invoice = invoiceOrError.data;
const pdfData = await pdfPresenter.toOutput(invoice, { companySlug }); const reportData = await pdfPresenter.toOutput(invoice, { companySlug });
if (format === "html") {
return Result.ok({
data: String(reportData),
filename: undefined,
});
}
return Result.ok({ return Result.ok({
data: pdfData, data: reportData as Buffer<ArrayBuffer>,
filename: `invoice-${invoice.invoiceNumber}.pdf`, filename: `proforma-${invoice.invoiceNumber}.pdf`,
}); });
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1,12 +1,12 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import puppeteer from "puppeteer"; import puppeteer from "puppeteer";
import report from "puppeteer-report";
import type { CustomerInvoice } from "../../../../../domain"; import type { CustomerInvoice } from "../../../../../domain";
import type { IssuedInvoiceReportHTMLPresenter } from "./issued-invoice.report.html"; import type { IssuedInvoiceReportHTMLPresenter } from "./issued-invoice.report.html";
// https://plnkr.co/edit/lWk6Yd?preview // https://plnkr.co/edit/lWk6Yd?preview
// https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports
export class IssuedInvoiceReportPDFPresenter extends Presenter< export class IssuedInvoiceReportPDFPresenter extends Presenter<
CustomerInvoice, CustomerInvoice,
@ -27,35 +27,35 @@ export class IssuedInvoiceReportPDFPresenter extends Presenter<
// Generar el PDF con Puppeteer // Generar el PDF con Puppeteer
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless: "new", //headless: "new",
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
args: ["--font-render-hinting=medium"], args: ["--font-render-hinting=medium"],
}); });
const page = await browser.newPage(); const page = await browser.newPage();
page.setDefaultNavigationTimeout(60000); //page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000); //page.setDefaultTimeout(60000);
await page.setContent(htmlData, { await page.setContent(htmlData, {
waitUntil: "networkidle0", waitUntil: "networkidle2",
}); });
// Espera extra opcional si hay imágenes base64 muy grandes // Espera extra opcional si hay imágenes base64 muy grandes
await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 }); await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 });
const reportPDF = await report.pdfPage(page, { const reportPDF = await page.pdf({
format: "A4", format: "A4",
margin: { margin: {
bottom: "10mm", top: 0,
left: "10mm", left: 0,
right: "10mm", right: 0,
top: "10mm", bottom: 0,
}, },
landscape: false, landscape: false,
preferCSSPageSize: true, preferCSSPageSize: true,
omitBackground: false, omitBackground: false,
printBackground: true, printBackground: true,
displayHeaderFooter: false, displayHeaderFooter: true,
headerTemplate: "<div />", headerTemplate: "<div />",
footerTemplate: footerTemplate:
'<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></span></div>', '<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></span></div>',

View File

@ -6,7 +6,6 @@ import {
IssueCustomerInvoiceDomainService, IssueCustomerInvoiceDomainService,
ProformaCustomerInvoiceDomainService, ProformaCustomerInvoiceDomainService,
} from "../../../domain"; } from "../../../domain";
import type { ProformaFullPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services"; import type { CustomerInvoiceApplicationService } from "../../services";
type IssueProformaUseCaseInput = { type IssueProformaUseCaseInput = {
@ -42,10 +41,6 @@ export class IssueProformaUseCase {
if (idOrError.isFailure) return Result.fail(idOrError.error); if (idOrError.isFailure) return Result.fail(idOrError.error);
const proformaId = idOrError.data; const proformaId = idOrError.data;
const presenter = this.presenterRegistry.getPresenter({
resource: "issued-invoice",
projection: "FULL",
}) as ProformaFullPresenter;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
@ -97,7 +92,12 @@ export class IssueProformaUseCase {
transaction transaction
); );
const dto = presenter.toOutput(saveInvoiceResult.data); const invoice = saveInvoiceResult.data;
const dto = {
proforma_id: proforma.id.toString(),
invoice_id: invoice.id.toString(),
customer_id: invoice.customerId.toString(),
};
return Result.ok(dto); return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -4,12 +4,11 @@ import { Result } from "@repo/rdx-utils";
import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service";
import type { ProformaReportPDFPresenter } from "./reporter";
type ReportProformaUseCaseInput = { type ReportProformaUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
companySlug: string; companySlug: string;
proforma_id: string; proforma_id: string;
format: "pdf" | "html";
}; };
export class ReportProformaUseCase { export class ReportProformaUseCase {
@ -20,7 +19,7 @@ export class ReportProformaUseCase {
) {} ) {}
public async execute(params: ReportProformaUseCaseInput) { public async execute(params: ReportProformaUseCaseInput) {
const { proforma_id, companySlug, companyId } = params; const { proforma_id, companySlug, companyId, format } = params;
const idOrError = UniqueID.create(proforma_id); const idOrError = UniqueID.create(proforma_id);
@ -29,11 +28,11 @@ export class ReportProformaUseCase {
} }
const proformaId = idOrError.data; const proformaId = idOrError.data;
const pdfPresenter = this.presenterRegistry.getPresenter({ const reportPresenter = this.presenterRegistry.getPresenter({
resource: "proforma", resource: "proforma",
projection: "REPORT", projection: "REPORT",
format: "PDF", format,
}) as ProformaReportPDFPresenter; });
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
@ -47,9 +46,17 @@ export class ReportProformaUseCase {
} }
const proforma = proformaOrError.data; const proforma = proformaOrError.data;
const pdfData = await pdfPresenter.toOutput(proforma, { companySlug }); const reportData = await reportPresenter.toOutput(proforma, { companySlug });
if (format === "html") {
return Result.ok({
data: String(reportData),
filename: undefined,
});
}
return Result.ok({ return Result.ok({
data: pdfData, data: reportData as Buffer<ArrayBuffer>,
filename: `proforma-${proforma.invoiceNumber}.pdf`, filename: `proforma-${proforma.invoiceNumber}.pdf`,
}); });
} catch (error: unknown) { } catch (error: unknown) {

View File

@ -1,12 +1,12 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import puppeteer from "puppeteer"; import puppeteer from "puppeteer";
import report from "puppeteer-report";
import type { CustomerInvoice } from "../../../../../domain"; import type { CustomerInvoice } from "../../../../../domain";
import type { ProformaReportHTMLPresenter } from "./proforma.report.html"; import type { ProformaReportHTMLPresenter } from "./proforma.report.html";
// https://plnkr.co/edit/lWk6Yd?preview // https://plnkr.co/edit/lWk6Yd?preview
// https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports
export class ProformaReportPDFPresenter extends Presenter< export class ProformaReportPDFPresenter extends Presenter<
CustomerInvoice, CustomerInvoice,
@ -25,38 +25,37 @@ export class ProformaReportPDFPresenter extends Presenter<
const htmlData = htmlPresenter.toOutput(proforma, params); const htmlData = htmlPresenter.toOutput(proforma, params);
// Generar el PDF con Puppeteer
// Generar el PDF con Puppeteer // Generar el PDF con Puppeteer
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless: "new", //headless: "new",
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
args: ["--font-render-hinting=medium"], args: ["--font-render-hinting=medium"],
}); });
const page = await browser.newPage(); const page = await browser.newPage();
page.setDefaultNavigationTimeout(60000); //page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000); //page.setDefaultTimeout(60000);
await page.setContent(htmlData, { await page.setContent(htmlData, {
waitUntil: "networkidle0", waitUntil: "networkidle2",
}); });
// Espera extra opcional si hay imágenes base64 muy grandes // Espera extra opcional si hay imágenes base64 muy grandes
await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 }); await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 });
const reportPDF = await report.pdfPage(page, { const reportPDF = await page.pdf({
format: "A4", format: "A4",
margin: { margin: {
bottom: "10mm", bottom: 0,
left: "10mm", left: 0,
right: "10mm", right: 0,
top: "10mm", top: 0,
}, },
landscape: false, landscape: false,
preferCSSPageSize: true, preferCSSPageSize: true,
omitBackground: false, omitBackground: false,
printBackground: true, printBackground: true,
displayHeaderFooter: false, displayHeaderFooter: true,
headerTemplate: "<div />", headerTemplate: "<div />",
footerTemplate: footerTemplate:
'<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></span></div>', '<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></span></div>',

View File

@ -20,11 +20,13 @@ export class ReportIssuedInvoiceController extends ExpressController {
const { companySlug } = this.getUser(); const { companySlug } = this.getUser();
const { invoice_id } = this.req.params; const { invoice_id } = this.req.params;
const { format } = this.req.query as { format: "pdf" | "html" };
const result = await this.useCase.execute({ invoice_id, companyId, companySlug }); const result = await this.useCase.execute({ invoice_id, companyId, companySlug, format });
return result.match( return result.match(
({ data, filename }) => this.downloadPDF(data, filename), ({ data, filename }) =>
filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string),
(err) => this.handleError(err) (err) => this.handleError(err)
); );
} }

View File

@ -20,11 +20,13 @@ export class ReportProformaController extends ExpressController {
const { companySlug } = this.getUser(); const { companySlug } = this.getUser();
const { proforma_id } = this.req.params; const { proforma_id } = this.req.params;
const { format } = this.req.query as { format: "pdf" | "html" };
const result = await this.useCase.execute({ proforma_id, companyId, companySlug }); const result = await this.useCase.execute({ proforma_id, companyId, companySlug, format });
return result.match( return result.match(
({ data, filename }) => this.downloadPDF(data, filename), ({ data, filename }) =>
filename ? this.downloadPDF(data, filename) : this.downloadHTML(data as string),
(err) => this.handleError(err) (err) => this.handleError(err)
); );
} }

View File

@ -7,7 +7,8 @@ import type { Sequelize } from "sequelize";
import { import {
GetIssueInvoiceByIdRequestSchema, GetIssueInvoiceByIdRequestSchema,
ListIssuedInvoicesRequestSchema, ListIssuedInvoicesRequestSchema,
ReportIssueInvoiceByIdRequestSchema, ReportIssueInvoiceByIdParamsRequestSchema,
ReportIssueInvoiceByIdQueryRequestSchema,
} from "../../../common/dto"; } from "../../../common/dto";
import { buildIssuedInvoicesDependencies } from "../issued-invoices-dependencies"; import { buildIssuedInvoicesDependencies } from "../issued-invoices-dependencies";
@ -71,7 +72,8 @@ export const issuedInvoicesRouter = (params: ModuleParams) => {
router.get( router.get(
"/:invoice_id/report", "/:invoice_id/report",
//checkTabContext, //checkTabContext,
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"), validateRequest(ReportIssueInvoiceByIdParamsRequestSchema, "params"),
validateRequest(ReportIssueInvoiceByIdQueryRequestSchema, "query"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report_issued_invoice(); const useCase = deps.useCases.report_issued_invoice();
const controller = new ReportIssuedInvoiceController(useCase); const controller = new ReportIssuedInvoiceController(useCase);

View File

@ -12,7 +12,8 @@ import {
GetProformaByIdRequestSchema, GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema, IssueProformaByIdParamsRequestSchema,
ListProformasRequestSchema, ListProformasRequestSchema,
ReportProformaByIdRequestSchema, ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema,
UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdParamsRequestSchema,
UpdateProformaByIdRequestSchema, UpdateProformaByIdRequestSchema,
} from "../../../common"; } from "../../../common";
@ -121,7 +122,8 @@ export const proformasRouter = (params: ModuleParams) => {
router.get( router.get(
"/:proforma_id/report", "/:proforma_id/report",
//checkTabContext, //checkTabContext,
validateRequest(ReportProformaByIdRequestSchema, "params"), validateRequest(ReportProformaByIdParamsRequestSchema, "params"),
validateRequest(ReportProformaByIdQueryRequestSchema, "query"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report_proforma(); const useCase = deps.useCases.report_proforma();
const controller = new ReportProformaController(useCase); const controller = new ReportProformaController(useCase);

View File

@ -78,8 +78,6 @@ export class CustomerInvoiceRepository
}); });
const dtoResult = mapper.mapToPersistence(invoice); const dtoResult = mapper.mapToPersistence(invoice);
console.log("DTO to persist:", dtoResult);
if (dtoResult.isFailure) { if (dtoResult.isFailure) {
return Result.fail(dtoResult.error); return Result.fail(dtoResult.error);
} }
@ -358,6 +356,12 @@ export class CustomerInvoiceRepository
], ],
include: [ include: [
...normalizedInclude, ...normalizedInclude,
{
model: VerifactuRecordModel,
as: "verifactu",
required: false,
attributes: ["id", "estado", "url", "uuid"],
},
{ {
model: CustomerModel, model: CustomerModel,
as: "current_customer", as: "current_customer",

View File

@ -1,7 +1,17 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
export const ReportIssueInvoiceByIdRequestSchema = z.object({ export const ReportIssueInvoiceByIdParamsRequestSchema = z.object({
invoice_id: z.string(), invoice_id: z.string(),
}); });
export type ReportIssueInvoiceByIdRequestDTO = z.infer<typeof ReportIssueInvoiceByIdRequestSchema>; export type ReportIssueInvoiceByIdParamsRequestDTO = z.infer<
typeof ReportIssueInvoiceByIdParamsRequestSchema
>;
export const ReportIssueInvoiceByIdQueryRequestSchema = z.object({
format: z.enum(["pdf", "html"]).default("pdf"),
});
export type ReportIssueInvoiceByIdQueryRequestDTO = z.infer<
typeof ReportIssueInvoiceByIdQueryRequestSchema
>;

View File

@ -1,7 +1,16 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
export const ReportProformaByIdRequestSchema = z.object({ export const ReportProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(), proforma_id: z.string(),
}); });
export type ReportProformaByIdRequestDTO = z.infer<typeof ReportProformaByIdRequestSchema>; export type ReportProformaByIdParamsRequestDTO = z.infer<
typeof ReportProformaByIdParamsRequestSchema
>;
export const ReportProformaByIdQueryRequestSchema = z.object({
format: z.enum(["pdf", "html"]).default("pdf"),
});
export type ReportProformaByIdQueryRequestDTO = z.infer<
typeof ReportProformaByIdQueryRequestSchema
>;

View File

@ -3,13 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style type="text/css"> <style>{{ asset 'tailwind.css' }}</style>
{
{
asset 'tailwind.css.b64'
}
}
</style>
<title>Factura</title> <title>Factura</title>
<style> <style>
/* ---------------------------- */ /* ---------------------------- */

View File

@ -3,15 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style type="text/css"> <style>{{ asset 'tailwind.css' }}</style>
{
{
asset 'tailwind.css.b64'
}
}
</style>
<title>Factura proforma</title> <title>Factura proforma</title>
<style> <style type="text/css">
/* ---------------------------- */ /* ---------------------------- */
/* ESTRUCTURA CABECERA */ /* ESTRUCTURA CABECERA */
/* ---------------------------- */ /* ---------------------------- */
@ -43,7 +37,7 @@
.company-text { .company-text {
font-size: 7pt; font-size: 7pt;
line-height: 1.2; line-height: 1;
padding-left: 10px; padding-left: 10px;
} }

File diff suppressed because one or more lines are too long

View File

@ -511,11 +511,8 @@ importers:
specifier: ^2.3.4 specifier: ^2.3.4
version: 2.3.4 version: 2.3.4
puppeteer: puppeteer:
specifier: ^24.20.0 specifier: ^24.30.0
version: 24.28.0(typescript@5.9.3) version: 24.30.0(typescript@5.9.3)
puppeteer-report:
specifier: ^3.2.0
version: 3.2.0
react-hook-form: react-hook-form:
specifier: ^7.58.1 specifier: ^7.58.1
version: 7.66.0(react@19.2.0) version: 7.66.0(react@19.2.0)
@ -1768,12 +1765,6 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
'@pdf-lib/standard-fonts@1.0.0':
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
'@pdf-lib/upng@1.0.1':
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -3167,8 +3158,8 @@ packages:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
chromium-bidi@10.5.1: chromium-bidi@11.0.0:
resolution: {integrity: sha512-rlj6OyhKhVTnk4aENcUme3Jl9h+cq4oXu4AzBcvr8RMmT6BR4a3zSNT9dbIfXr9/BS6ibzRyDhowuw4n2GgzsQ==} resolution: {integrity: sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==}
peerDependencies: peerDependencies:
devtools-protocol: '*' devtools-protocol: '*'
@ -4810,9 +4801,6 @@ packages:
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@2.1.1: param-case@2.1.1:
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
@ -4910,9 +4898,6 @@ packages:
pause@0.0.1: pause@0.0.1:
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
pdf-lib@1.17.1:
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
pend@1.2.0: pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@ -5060,15 +5045,12 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
puppeteer-core@24.28.0: puppeteer-core@24.30.0:
resolution: {integrity: sha512-QpAqaYgeZHF5/xAZ4jAOzsU+l0Ed4EJoWkRdfw8rNqmSN7itcdYeCJaSPQ0s5Pyn/eGNC4xNevxbgY+5bzNllw==} resolution: {integrity: sha512-2S3Smy0t0W4wJnNvDe7W0bE7wDmZjfZ3ljfMgJd6hn2Hq/f0jgN+x9PULZo2U3fu5UUIJ+JP8cNUGllu8P91Pg==}
engines: {node: '>=18'} engines: {node: '>=18'}
puppeteer-report@3.2.0: puppeteer@24.30.0:
resolution: {integrity: sha512-c2JNsAeLa4Ik4FVPdTRyWYfXO61uSI0ZJ9BNPuf84sImTmwBT4CY/Vn88iQ7q7LQstJStkPuNWAzJgNEmBONmQ==} resolution: {integrity: sha512-A5OtCi9WpiXBQgJ2vQiZHSyrAzQmO/WDsvghqlN4kgw21PhxA5knHUaUQq/N3EMt8CcvSS0RM+kmYLJmedR3TQ==}
puppeteer@24.28.0:
resolution: {integrity: sha512-KLRGFNCGmXJpocEBbEIoHJB0vNRZLQNBjl5ExXEv0z7MIU+qqVEQcfWTyat+qxPDk/wZvSf+b30cQqAfWxX0zg==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@ -6815,14 +6797,6 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-ia32': 2.5.1
'@parcel/watcher-win32-x64': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1
'@pdf-lib/standard-fonts@1.0.0':
dependencies:
pako: 1.0.11
'@pdf-lib/upng@1.0.1':
dependencies:
pako: 1.0.11
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -8284,7 +8258,7 @@ snapshots:
chownr@2.0.0: {} chownr@2.0.0: {}
chromium-bidi@10.5.1(devtools-protocol@0.0.1521046): chromium-bidi@11.0.0(devtools-protocol@0.0.1521046):
dependencies: dependencies:
devtools-protocol: 0.0.1521046 devtools-protocol: 0.0.1521046
mitt: 3.0.1 mitt: 3.0.1
@ -9963,8 +9937,6 @@ snapshots:
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
pako@1.0.11: {}
param-case@2.1.1: param-case@2.1.1:
dependencies: dependencies:
no-case: 2.3.2 no-case: 2.3.2
@ -10060,13 +10032,6 @@ snapshots:
pause@0.0.1: {} pause@0.0.1: {}
pdf-lib@1.17.1:
dependencies:
'@pdf-lib/standard-fonts': 1.0.0
'@pdf-lib/upng': 1.0.1
pako: 1.0.11
tslib: 1.14.1
pend@1.2.0: {} pend@1.2.0: {}
pg-connection-string@2.9.1: {} pg-connection-string@2.9.1: {}
@ -10215,10 +10180,10 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
puppeteer-core@24.28.0: puppeteer-core@24.30.0:
dependencies: dependencies:
'@puppeteer/browsers': 2.10.13 '@puppeteer/browsers': 2.10.13
chromium-bidi: 10.5.1(devtools-protocol@0.0.1521046) chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046)
debug: 4.4.3 debug: 4.4.3
devtools-protocol: 0.0.1521046 devtools-protocol: 0.0.1521046
typed-query-selector: 2.12.0 typed-query-selector: 2.12.0
@ -10232,17 +10197,13 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
puppeteer-report@3.2.0: puppeteer@24.30.0(typescript@5.9.3):
dependencies:
pdf-lib: 1.17.1
puppeteer@24.28.0(typescript@5.9.3):
dependencies: dependencies:
'@puppeteer/browsers': 2.10.13 '@puppeteer/browsers': 2.10.13
chromium-bidi: 10.5.1(devtools-protocol@0.0.1521046) chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046)
cosmiconfig: 9.0.0(typescript@5.9.3) cosmiconfig: 9.0.0(typescript@5.9.3)
devtools-protocol: 0.0.1521046 devtools-protocol: 0.0.1521046
puppeteer-core: 24.28.0 puppeteer-core: 24.30.0
typed-query-selector: 2.12.0 typed-query-selector: 2.12.0
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller - bare-abort-controller