Facturas de cliente
This commit is contained in:
parent
241ee4da93
commit
c8c71cf91c
@ -1 +1,2 @@
|
|||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
|
export * from "./presenters";
|
||||||
|
|||||||
2
modules/core/src/api/application/presenters/index.ts
Normal file
2
modules/core/src/api/application/presenters/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./presenter-registry";
|
||||||
|
export * from "./presenter-registry.interface";
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { IPresenter } from "./presenter.interface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔑 Claves de proyección comunes para seleccionar presenters
|
||||||
|
*/
|
||||||
|
export type PresenterKey = {
|
||||||
|
resource: string; // "customer-invoice"
|
||||||
|
projection: string; //"detail" | "summary" | "created" | "status" | "export";
|
||||||
|
format: string; //"json" | "pdf" | "csv" | "xml";
|
||||||
|
version?: number; // 1 | 2
|
||||||
|
locale?: string; // es | en | fr
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejemplo de uso:
|
||||||
|
*
|
||||||
|
* const registry = new InMemoryPresenterRegistry();
|
||||||
|
*
|
||||||
|
* // Registro
|
||||||
|
* registry.register(
|
||||||
|
* { resource: "customer-invoice", projection: "detail", format: "json", version: 1 },
|
||||||
|
* new CustomerInvoiceDetailPresenter()
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* registry.register(
|
||||||
|
* { resource: "customer-invoice", projection: "detail", format: "pdf", version: 1 },
|
||||||
|
* new CustomerInvoicePdfPresenter()
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Resolución
|
||||||
|
* const presenterOrNone = registry.resolve({
|
||||||
|
* resource: "customer-invoice",
|
||||||
|
* projection: "detail",
|
||||||
|
* format: "pdf",
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* presenterOrNone.map(async (presenter) => {
|
||||||
|
* const output = await (presenter as IAsyncPresenter<any, any>).toOutput(invoice);
|
||||||
|
* console.log("PDF generado:", output);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
|
||||||
|
export interface IPresenterRegistry {
|
||||||
|
/**
|
||||||
|
* Obtiene un mapper de dominio por clave de proyección.
|
||||||
|
*/
|
||||||
|
getPresenter<TSource, TOutput>(key: PresenterKey): IPresenter<TSource, TOutput>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra un mapper de dominio bajo una clave de proyección.
|
||||||
|
*/
|
||||||
|
registerPresenter<TSource, TOutput>(
|
||||||
|
key: PresenterKey,
|
||||||
|
presenter: IPresenter<TSource, TOutput>
|
||||||
|
): void;
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { ApplicationError } from "../errors";
|
||||||
|
import { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface";
|
||||||
|
import { IPresenter } from "./presenter.interface";
|
||||||
|
|
||||||
|
export class InMemoryPresenterRegistry implements IPresenterRegistry {
|
||||||
|
private registry: Map<string, IPresenter<any, any>> = new Map();
|
||||||
|
|
||||||
|
getPresenter<TSource, TOutput>(key: PresenterKey): IPresenter<TSource, TOutput> {
|
||||||
|
const exactKey = this._buildKey(key);
|
||||||
|
|
||||||
|
// 1) Intentar clave exacta
|
||||||
|
if (this.registry.has(exactKey)) {
|
||||||
|
return this.registry.get(exactKey)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback por versión: si no se indicó, buscar la última registrada
|
||||||
|
if (key.version === undefined) {
|
||||||
|
const candidates = [...this.registry.keys()].filter((k) =>
|
||||||
|
k.startsWith(this._buildKey({ ...key, version: undefined, locale: undefined }))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
const latest = candidates.sort().pop()!; // simplificación: versión más alta lexicográficamente
|
||||||
|
return this.registry.get(latest)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Fallback por locale: intentar sin locale si no se encuentra exacto
|
||||||
|
if (key.locale) {
|
||||||
|
const withoutLocale = this._buildKey({ ...key, locale: undefined });
|
||||||
|
if (this.registry.has(withoutLocale)) {
|
||||||
|
return this.registry.get(withoutLocale)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.registry.has(exactKey)) {
|
||||||
|
throw new ApplicationError(`Error. Presenter ${key} not registred!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApplicationError(`Error. Presenter ${key} not registred!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPresenter<TSource, TOutput>(
|
||||||
|
key: PresenterKey,
|
||||||
|
presenter: IPresenter<TSource, TOutput>
|
||||||
|
): void {
|
||||||
|
const exactKey = this._buildKey(key);
|
||||||
|
this.registry.set(exactKey, presenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Construye la clave única para el registro.
|
||||||
|
*/
|
||||||
|
private _buildKey(key: PresenterKey): string {
|
||||||
|
const { resource, projection, format, version, locale } = key;
|
||||||
|
return [
|
||||||
|
resource.toLowerCase(),
|
||||||
|
projection.toLowerCase(),
|
||||||
|
format.toLowerCase(),
|
||||||
|
version ?? "latest",
|
||||||
|
locale ?? "default",
|
||||||
|
].join("::");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
export type DTO<T = unknown> = T;
|
||||||
|
export type BinaryOutput = Buffer; // Puedes ampliar a Readable si usas streams
|
||||||
|
|
||||||
|
interface ISyncPresenter<TSource, TOutput = DTO> {
|
||||||
|
toOutput(source: TSource): TOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAsyncPresenter<TSource, TOutput = DTO> {
|
||||||
|
toOutput(source: TSource): Promise<TOutput>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proyección SINCRÓNICA de colecciones.
|
||||||
|
* Útil para listados paginados, exportaciones ligeras, etc.
|
||||||
|
*/
|
||||||
|
/*export interface ISyncBulkPresenter<TSource, TOutput = BinaryOutput> {
|
||||||
|
toOutput(source: TSource): TOutput;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proyección ASÍNCRONA de colecciones.
|
||||||
|
* Útil para generar varios PDFs/CSVs.
|
||||||
|
*/
|
||||||
|
/*export interface IAsyncBulkPresenter<TSource, TOutput = BinaryOutput> {
|
||||||
|
toOutput(source: TSource): Promise<TOutput>;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
export type IPresenter<TSource, TOutput = DTO | BinaryOutput> =
|
||||||
|
| ISyncPresenter<TSource, TOutput>
|
||||||
|
| IAsyncPresenter<TSource, TOutput>;
|
||||||
|
//| ISyncBulkPresenter<TSource, TOutput>
|
||||||
|
//| IAsyncBulkPresenter<TSource, TOutput>;
|
||||||
@ -2,4 +2,5 @@ export * from "./create-customer-invoice";
|
|||||||
export * from "./delete-customer-invoice";
|
export * from "./delete-customer-invoice";
|
||||||
export * from "./get-customer-invoice";
|
export * from "./get-customer-invoice";
|
||||||
export * from "./list-customer-invoices";
|
export * from "./list-customer-invoices";
|
||||||
export * from "./update-customer-invoice";
|
export * from "./report-customer-invoice";
|
||||||
|
//export * from "./update-customer-invoice";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./report-customer-invoice.use-case";
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
import { CustomerInvoiceService } from "../../domain";
|
||||||
|
import { ReportCustomerInvoiceAssembler } from "./assembler";
|
||||||
|
|
||||||
|
type ReportCustomerInvoiceUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
invoice_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ReportCustomerInvoiceUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly service: CustomerInvoiceService,
|
||||||
|
private readonly transactionManager: ITransactionManager,
|
||||||
|
private readonly assembler: ReportCustomerInvoiceAssembler
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(params: ReportCustomerInvoiceUseCaseInput) {
|
||||||
|
const { invoice_id, companyId } = params;
|
||||||
|
|
||||||
|
const idOrError = UniqueID.create(invoice_id);
|
||||||
|
|
||||||
|
if (idOrError.isFailure) {
|
||||||
|
return Result.fail(idOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceId = idOrError.data;
|
||||||
|
|
||||||
|
return this.transactionManager.complete(async (transaction) => {
|
||||||
|
try {
|
||||||
|
const invoiceOrError = await this.service.getInvoiceByIdInCompany(
|
||||||
|
companyId,
|
||||||
|
invoiceId,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
if (invoiceOrError.isFailure) {
|
||||||
|
return Result.fail(invoiceOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceDto = this.registry.getPresenter("").toDTO(invoideOIrError.data);
|
||||||
|
|
||||||
|
const pdfData = this.assembler.toPDF(invoiceDto);
|
||||||
|
return Result.ok(pdfData);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
import { toEmptyString } from "@repo/rdx-ddd";
|
||||||
|
import * as handlebars from "handlebars";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import puppeteer from "puppeteer";
|
||||||
|
import report from "puppeteer-report";
|
||||||
|
import { CustomerInvoice } from "../../../domain";
|
||||||
|
|
||||||
|
export interface ICustomerInvoiceReporter {
|
||||||
|
toHTML: (invoice: CustomerInvoice) => Promise<string>;
|
||||||
|
toPDF: (invoice: CustomerInvoice) => Promise<Buffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://plnkr.co/edit/lWk6Yd?preview
|
||||||
|
|
||||||
|
export const CustomerInvoiceReporter: ICustomerInvoiceReporter = {
|
||||||
|
toHTML: async (invoice: CustomerInvoice): Promise<string> => {
|
||||||
|
const quote_dto = await map(quote, context);
|
||||||
|
|
||||||
|
// Obtener y compilar la plantilla HTML
|
||||||
|
const templateHtml = readFileSync(
|
||||||
|
path.join(__dirname, "./templates/quote/template.hbs")
|
||||||
|
).toString();
|
||||||
|
const template = handlebars.compile(templateHtml, {});
|
||||||
|
return template(quote_dto);
|
||||||
|
},
|
||||||
|
|
||||||
|
toPDF: async (quote: CustomerInvoice, context: ISalesContext): Promise<Buffer> => {
|
||||||
|
const html = await CustomerInvoiceReporter.toHTML(quote, context);
|
||||||
|
|
||||||
|
// Generar el PDF con Puppeteer
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
args: [
|
||||||
|
"--disable-extensions",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--disable-gpu",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const navigationPromise = page.waitForNavigation();
|
||||||
|
await page.setContent(html, { waitUntil: "networkidle2" });
|
||||||
|
|
||||||
|
await navigationPromise;
|
||||||
|
const reportPDF = await report.pdfPage(page, {
|
||||||
|
format: "A4",
|
||||||
|
margin: {
|
||||||
|
bottom: "10mm",
|
||||||
|
left: "10mm",
|
||||||
|
right: "10mm",
|
||||||
|
top: "10mm",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
return Buffer.from(reportPDF);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = async (invoice: CustomerInvoice) => {
|
||||||
|
return {
|
||||||
|
id: invoice.id.toString(),
|
||||||
|
company_id: invoice.companyId.toString(),
|
||||||
|
|
||||||
|
invoice_number: invoice.invoiceNumber.toString(),
|
||||||
|
status: invoice.status.toPrimitive(),
|
||||||
|
series: toEmptyString(invoice.series, (value) => value.toString()),
|
||||||
|
|
||||||
|
invoice_date: invoice.invoiceDate.toDateString(),
|
||||||
|
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
|
||||||
|
|
||||||
|
notes: toEmptyString(invoice.notes, (value) => value.toString()),
|
||||||
|
|
||||||
|
language_code: invoice.languageCode.toString(),
|
||||||
|
currency_code: invoice.currencyCode.toString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const quoteItemPresenter = (
|
||||||
|
items: ICollection<CustomerInvoiceItem>,
|
||||||
|
context: ISalesContext
|
||||||
|
): any[] =>
|
||||||
|
items.totalCount > 0
|
||||||
|
? items.items.map((item: CustomerInvoiceItem) => ({
|
||||||
|
id_article: item.idArticle.toString(),
|
||||||
|
description: item.description.toString(),
|
||||||
|
quantity: item.quantity.toFormat(),
|
||||||
|
unit_price: item.unitPrice.toFormat(),
|
||||||
|
subtotal_price: item.subtotalPrice.toFormat(),
|
||||||
|
discount: item.discount.toFormat(),
|
||||||
|
total_price: item.totalPrice.toFormat(),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
2;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-invoice.reporter";
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
<html lang="{{lang_code}}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
||||||
|
referrerpolicy="no-referrer" />
|
||||||
|
<title>Presupuesto #{{id}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
color: #000;
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
thead {
|
||||||
|
display: table-header-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot {
|
||||||
|
display: table-footer-group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header id="header">
|
||||||
|
<aside class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">DISTRIBUIDOR OFICIAL</h3>
|
||||||
|
<div id="logo" class="w-32">
|
||||||
|
<svg viewBox="0 0 336 133" fill="none" xmlns="http://www.w3.org/2000/svg" class="uecko-logo">
|
||||||
|
<path
|
||||||
|
d="M49.7002 83.0001H66.9002V22.5001H49.7002V56.2001C49.7002 64.3001 45.5002 68.5001 39.0002 68.5001C32.5002 68.5001 28.6002 64.3001 28.6002 56.2001V22.5001H0.700195V33.2001H11.4002V61.6001C11.4002 75.5001 19.0002 84.1001 31.9002 84.1001C40.6002 84.1001 45.7002 79.5001 49.6002 74.4001V83.0001H49.7002ZM120.6 48.0001H94.8002C96.2002 40.2001 100.8 35.1001 107.9 35.1001C115.1 35.2001 119.6 40.3001 120.6 48.0001ZM137.1 58.7001C137.2 57.1001 137.3 56.1001 137.3 54.4001V54.2001C137.3 37.0001 128 21.4001 107.8 21.4001C90.2002 21.4001 77.9002 35.6001 77.9002 52.9001V53.1001C77.9002 71.6001 91.3002 84.4001 109.5 84.4001C120.4 84.4001 128.6 80.1001 134.2 73.1001L124.4 64.4001C119.7 68.8001 115.5 70.6001 109.7 70.6001C102 70.6001 96.6002 66.5001 94.9002 58.7001H137.1ZM162.2 52.9001V52.7001C162.2 43.8001 168.3 36.2001 176.9 36.2001C183 36.2001 186.8 38.8001 190.7 42.9001L201.2 31.6001C195.6 25.3001 188.4 21.4001 177 21.4001C158.5 21.4001 145.3 35.6001 145.3 52.9001V53.1001C145.3 70.4001 158.6 84.4001 176.8 84.4001C188.9 84.4001 195.6 79.8001 201.5 73.3001L191.5 63.1001C187.3 67.1001 183.4 69.5001 177.6 69.5001C168.2 69.6001 162.2 62.1001 162.2 52.9001ZM269.1 83.0001L245.3 46.3001L268.3 22.5001H247.8L227.7 44.5001V0.600098H210.5V83.0001H227.7V64.6001L233.7 58.3001L249.5 83.0001H269.1ZM318.5 53.1001C318.5 62.0001 312.6 69.6001 302.8 69.6001C293.3 69.6001 286.9 61.8001 286.9 52.9001V52.7001C286.9 43.8001 292.8 36.2001 302.6 36.2001C312.1 36.2001 318.5 44.0001 318.5 52.9001V53.1001ZM335.4 52.9001V52.7001C335.4 35.3001 321.5 21.4001 302.8 21.4001C284 21.4001 270 35.5001 270 52.9001V53.1001C270 70.5001 283.9 84.4001 302.6 84.4001C321.4 84.4001 335.4 70.3001 335.4 52.9001Z"
|
||||||
|
fill="black" class="uecko-logo"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<section class="flex pb-2 space-x-4">
|
||||||
|
<img id="dealer-logo" src={{dealer.logo}} alt="Logo distribuidor" />
|
||||||
|
<address class="text-base not-italic font-medium whitespace-pre-line" id="from">{{dealer.contact_information}}
|
||||||
|
</address>
|
||||||
|
</section>
|
||||||
|
<section class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
||||||
|
<aside>
|
||||||
|
<p class="text-sm"><strong>Presupuesto nº:</strong> {{reference}}</p>
|
||||||
|
<p class="text-sm"><strong>Fecha:</strong> {{date}}</p>
|
||||||
|
<p class="text-sm"><strong>Validez:</strong> {{validity}}</p>
|
||||||
|
<p class="text-sm"><strong>Vendedor:</strong> {{dealer.name}}</p>
|
||||||
|
<p class="text-sm"><strong>Referencia cliente:</strong> {{customer_reference}}</p>
|
||||||
|
</aside>
|
||||||
|
<address class="text-base not-italic font-semibold whitespace-pre-line" id="to">{{customer_information}}
|
||||||
|
</address>
|
||||||
|
</section>
|
||||||
|
<aside class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-2xl font-normal">PRESUPUESTO</h3>
|
||||||
|
<div id="header-pagination">
|
||||||
|
Página <span class="pageNumber"></span> de <span class="totalPages"></span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</header>
|
||||||
|
<main id="main">
|
||||||
|
<section id="details">
|
||||||
|
<table class="table-header">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 py-2 text-right border">Cant.</th>
|
||||||
|
<th class="px-2 py-2 border">Descripción</th>
|
||||||
|
<th class="px-2 py-2 text-right border">Prec. Unitario</th>
|
||||||
|
<th class="px-2 py-2 text-right border">Subtotal</th>
|
||||||
|
<th class="px-2 py-2 text-right border">Dto (%)</th>
|
||||||
|
<th class="px-2 py-2 text-right border">Importe total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-body">
|
||||||
|
{{#each items}}
|
||||||
|
<tr class="text-sm border-b">
|
||||||
|
<td class="content-start px-2 py-2 text-right">{{quantity}}</td>
|
||||||
|
<td class="px-2 py-2 font-medium">{{description}}</td>
|
||||||
|
<td class="content-start px-2 py-2 text-right">{{unit_price}}</td>
|
||||||
|
<td class="content-start px-2 py-2 text-right">{{subtotal_price}}</td>
|
||||||
|
<td class="content-start px-2 py-2 text-right">{{discount}}</td>
|
||||||
|
<td class="content-start px-2 py-2 text-right">{{total_price}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="resume" class="flex items-center justify-between pb-4 mb-4">
|
||||||
|
|
||||||
|
<div class="grow">
|
||||||
|
<div class="pt-4">
|
||||||
|
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4">
|
||||||
|
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grow">
|
||||||
|
<table class="min-w-full bg-transparent">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="px-4 py-2">Importe neto</td>
|
||||||
|
<td class="px-4 py-2"></td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{subtotal_price}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="px-4 py-2">% Descuento</td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{discount.amount}}</td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{discount_price}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="px-4 py-2">Base imponible</td>
|
||||||
|
<td class="px-4 py-2"></td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{before_tax_price}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="px-4 py-2">% IVA</td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{tax}}</td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{tax_price}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="px-4 py-2">Importe total</td>
|
||||||
|
<td class="px-4 py-2"></td>
|
||||||
|
<td class="px-4 py-2 text-right border">{{total_price}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="legal_terms">
|
||||||
|
<p class="text-xs text-gray-500">{{quote.default_legal_terms}}</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer id="footer" class="mt-4">
|
||||||
|
<aside><img src="https://uecko.com/assets/img/uecko-footer_logos.jpg" class="w-full" /></aside>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 336 133" fill="none" xmlns="http://www.w3.org/2000/svg" class="uecko-logo"><path d="M49.7002 83.0001H66.9002V22.5001H49.7002V56.2001C49.7002 64.3001 45.5002 68.5001 39.0002 68.5001C32.5002 68.5001 28.6002 64.3001 28.6002 56.2001V22.5001H0.700195V33.2001H11.4002V61.6001C11.4002 75.5001 19.0002 84.1001 31.9002 84.1001C40.6002 84.1001 45.7002 79.5001 49.6002 74.4001V83.0001H49.7002ZM120.6 48.0001H94.8002C96.2002 40.2001 100.8 35.1001 107.9 35.1001C115.1 35.2001 119.6 40.3001 120.6 48.0001ZM137.1 58.7001C137.2 57.1001 137.3 56.1001 137.3 54.4001V54.2001C137.3 37.0001 128 21.4001 107.8 21.4001C90.2002 21.4001 77.9002 35.6001 77.9002 52.9001V53.1001C77.9002 71.6001 91.3002 84.4001 109.5 84.4001C120.4 84.4001 128.6 80.1001 134.2 73.1001L124.4 64.4001C119.7 68.8001 115.5 70.6001 109.7 70.6001C102 70.6001 96.6002 66.5001 94.9002 58.7001H137.1ZM162.2 52.9001V52.7001C162.2 43.8001 168.3 36.2001 176.9 36.2001C183 36.2001 186.8 38.8001 190.7 42.9001L201.2 31.6001C195.6 25.3001 188.4 21.4001 177 21.4001C158.5 21.4001 145.3 35.6001 145.3 52.9001V53.1001C145.3 70.4001 158.6 84.4001 176.8 84.4001C188.9 84.4001 195.6 79.8001 201.5 73.3001L191.5 63.1001C187.3 67.1001 183.4 69.5001 177.6 69.5001C168.2 69.6001 162.2 62.1001 162.2 52.9001ZM269.1 83.0001L245.3 46.3001L268.3 22.5001H247.8L227.7 44.5001V0.600098H210.5V83.0001H227.7V64.6001L233.7 58.3001L249.5 83.0001H269.1ZM318.5 53.1001C318.5 62.0001 312.6 69.6001 302.8 69.6001C293.3 69.6001 286.9 61.8001 286.9 52.9001V52.7001C286.9 43.8001 292.8 36.2001 302.6 36.2001C312.1 36.2001 318.5 44.0001 318.5 52.9001V53.1001ZM335.4 52.9001V52.7001C335.4 35.3001 321.5 21.4001 302.8 21.4001C284 21.4001 270 35.5001 270 52.9001V53.1001C270 70.5001 283.9 84.4001 302.6 84.4001C321.4 84.4001 335.4 70.3001 335.4 52.9001Z" fill="black" class="uecko-logo"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,6 +1,10 @@
|
|||||||
import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core";
|
import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core";
|
||||||
import type { IMapperRegistry, ModuleParams } from "@erp/core/api";
|
import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api";
|
||||||
import { InMemoryMapperRegistry, SequelizeTransactionManager } from "@erp/core/api";
|
import {
|
||||||
|
InMemoryMapperRegistry,
|
||||||
|
InMemoryPresenterRegistry,
|
||||||
|
SequelizeTransactionManager,
|
||||||
|
} from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
CreateCustomerInvoiceAssembler,
|
CreateCustomerInvoiceAssembler,
|
||||||
CreateCustomerInvoiceUseCase,
|
CreateCustomerInvoiceUseCase,
|
||||||
@ -9,6 +13,7 @@ import {
|
|||||||
GetCustomerInvoiceUseCase,
|
GetCustomerInvoiceUseCase,
|
||||||
ListCustomerInvoicesAssembler,
|
ListCustomerInvoicesAssembler,
|
||||||
ListCustomerInvoicesUseCase,
|
ListCustomerInvoicesUseCase,
|
||||||
|
ReportCustomerInvoiceUseCase,
|
||||||
UpdateCustomerInvoiceAssembler,
|
UpdateCustomerInvoiceAssembler,
|
||||||
UpdateCustomerInvoiceUseCase,
|
UpdateCustomerInvoiceUseCase,
|
||||||
} from "../application";
|
} from "../application";
|
||||||
@ -24,7 +29,7 @@ type InvoiceDeps = {
|
|||||||
catalogs: {
|
catalogs: {
|
||||||
taxes: JsonTaxCatalogProvider;
|
taxes: JsonTaxCatalogProvider;
|
||||||
};
|
};
|
||||||
assemblers: {
|
presenters: {
|
||||||
list: ListCustomerInvoicesAssembler;
|
list: ListCustomerInvoicesAssembler;
|
||||||
get: GetCustomerInvoiceAssembler;
|
get: GetCustomerInvoiceAssembler;
|
||||||
create: CreateCustomerInvoiceAssembler;
|
create: CreateCustomerInvoiceAssembler;
|
||||||
@ -36,16 +41,16 @@ type InvoiceDeps = {
|
|||||||
create: () => CreateCustomerInvoiceUseCase;
|
create: () => CreateCustomerInvoiceUseCase;
|
||||||
update: () => UpdateCustomerInvoiceUseCase;
|
update: () => UpdateCustomerInvoiceUseCase;
|
||||||
delete: () => DeleteCustomerInvoiceUseCase;
|
delete: () => DeleteCustomerInvoiceUseCase;
|
||||||
};
|
report: () => ReportCustomerInvoiceUseCase;
|
||||||
presenters: {
|
|
||||||
// list: <T>(res: Response) => ListPresenter<T>;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let _repo: CustomerInvoiceRepository | null = null;
|
let _presenterRegistry: IPresenterRegistry | null = null;
|
||||||
let _mapperRegistry: IMapperRegistry | null = null;
|
let _mapperRegistry: IMapperRegistry | null = null;
|
||||||
|
|
||||||
|
let _repo: CustomerInvoiceRepository | null = null;
|
||||||
let _service: CustomerInvoiceService | null = null;
|
let _service: CustomerInvoiceService | null = null;
|
||||||
let _assemblers: InvoiceDeps["assemblers"] | null = null;
|
const _presenters: InvoiceDeps["presenters"] | null = null;
|
||||||
let _catalogs: InvoiceDeps["catalogs"] | null = null;
|
let _catalogs: InvoiceDeps["catalogs"] | null = null;
|
||||||
|
|
||||||
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
||||||
@ -53,26 +58,28 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
|||||||
const transactionManager = new SequelizeTransactionManager(database);
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider };
|
if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider };
|
||||||
|
|
||||||
const fullMapper: CustomerInvoiceFullMapper = new CustomerInvoiceFullMapper({
|
|
||||||
taxCatalog: _catalogs!.taxes,
|
|
||||||
});
|
|
||||||
const listMapper = new CustomerInvoiceListMapper();
|
|
||||||
|
|
||||||
if (!_mapperRegistry) {
|
if (!_mapperRegistry) {
|
||||||
_mapperRegistry = new InMemoryMapperRegistry();
|
_mapperRegistry = new InMemoryMapperRegistry();
|
||||||
_mapperRegistry.registerDomainMapper("FULL", fullMapper);
|
_mapperRegistry.registerDomainMapper(
|
||||||
_mapperRegistry.registerReadModelMapper("LIST", listMapper);
|
"FULL",
|
||||||
|
new CustomerInvoiceFullMapper({
|
||||||
|
taxCatalog: _catalogs!.taxes,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
_mapperRegistry.registerReadModelMapper("LIST", new CustomerInvoiceListMapper());
|
||||||
}
|
}
|
||||||
if (!_repo) _repo = new CustomerInvoiceRepository({ mapperRegistry: _mapperRegistry, database });
|
if (!_repo) _repo = new CustomerInvoiceRepository({ mapperRegistry: _mapperRegistry, database });
|
||||||
if (!_service) _service = new CustomerInvoiceService(_repo);
|
if (!_service) _service = new CustomerInvoiceService(_repo);
|
||||||
|
|
||||||
if (!_assemblers) {
|
if (!_presenterRegistry) {
|
||||||
_assemblers = {
|
_presenterRegistry = new InMemoryPresenterRegistry();
|
||||||
|
_presenterRegistry.registerPresenter(key, mapper);
|
||||||
|
/*_presenters = {
|
||||||
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
|
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
|
||||||
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO
|
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO
|
||||||
create: new CreateCustomerInvoiceAssembler(), // transforma domain → CreatedDTO
|
create: new CreateCustomerInvoiceAssembler(), // transforma domain → CreatedDTO
|
||||||
update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO
|
update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO
|
||||||
};
|
};*/
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -80,31 +87,29 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
|||||||
repo: _repo,
|
repo: _repo,
|
||||||
mapperRegistry: _mapperRegistry,
|
mapperRegistry: _mapperRegistry,
|
||||||
service: _service,
|
service: _service,
|
||||||
assemblers: _assemblers,
|
presenters: _presenters,
|
||||||
catalogs: _catalogs,
|
catalogs: _catalogs,
|
||||||
build: {
|
build: {
|
||||||
list: () =>
|
list: () =>
|
||||||
new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list),
|
new ListCustomerInvoicesUseCase(_service!, transactionManager!, _presenters!.list),
|
||||||
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get),
|
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _presenters!.get),
|
||||||
create: () =>
|
create: () =>
|
||||||
new CreateCustomerInvoiceUseCase(
|
new CreateCustomerInvoiceUseCase(
|
||||||
_service!,
|
_service!,
|
||||||
transactionManager!,
|
transactionManager!,
|
||||||
_assemblers!.create,
|
_presenters!.create,
|
||||||
_catalogs!.taxes
|
_catalogs!.taxes
|
||||||
),
|
),
|
||||||
update: () =>
|
update: () =>
|
||||||
new UpdateCustomerInvoiceUseCase(
|
new UpdateCustomerInvoiceUseCase(
|
||||||
_service!,
|
_service!,
|
||||||
transactionManager!,
|
transactionManager!,
|
||||||
_assemblers!.update,
|
_presenters!.update,
|
||||||
_catalogs!.taxes
|
_catalogs!.taxes
|
||||||
),
|
),
|
||||||
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
|
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
|
||||||
},
|
report: () =>
|
||||||
presenters: {
|
new ReportCustomerInvoiceUseCase(_service!, transactionManager!, _presenters!.get),
|
||||||
//list: <T>(res: Response) => createListPresenter<T>(res),
|
|
||||||
//json: <T>(res: Response, status: number = 200) => createJsonPresenter<T>(res, status),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
|
import { GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
|
||||||
|
export class ReportCustomerInvoiceController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) {
|
||||||
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
|
const { invoice_id } = this.req.params;
|
||||||
|
|
||||||
|
const getUseCase = getUsecaasdasd;
|
||||||
|
const invoiceDto = await
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ invoice_id, companyId, });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.downloadPDF(result.data),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
CustomerInvoiceListRequestSchema,
|
CustomerInvoiceListRequestSchema,
|
||||||
DeleteCustomerInvoiceByIdRequestSchema,
|
DeleteCustomerInvoiceByIdRequestSchema,
|
||||||
GetCustomerInvoiceByIdRequestSchema,
|
GetCustomerInvoiceByIdRequestSchema,
|
||||||
|
ReportCustomerInvoiceByIdRequestSchema,
|
||||||
} from "../../../common/dto";
|
} from "../../../common/dto";
|
||||||
import { getInvoiceDependencies } from "../dependencies";
|
import { getInvoiceDependencies } from "../dependencies";
|
||||||
import {
|
import {
|
||||||
@ -104,5 +105,16 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:invoice_id/report",
|
||||||
|
//checkTabContext,
|
||||||
|
validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.build.report();
|
||||||
|
const controller = new ReportCustomerInvoiceController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
app.use(`${baseRoutePath}/customer-invoices`, router);
|
app.use(`${baseRoutePath}/customer-invoices`, router);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
|
||||||
|
|
||||||
export const generateUUIDv4 = (): string => uuidv4();
|
export const generateUUIDv4 = (): string => uuidv4();
|
||||||
|
export const generateUUIDv7 = (): string => uuidv7();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user