.
This commit is contained in:
parent
ebb2d06903
commit
e73aeaeefe
@ -31,7 +31,9 @@ export const QuotesList = () => {
|
||||
<TabsTrigger value='all'>
|
||||
<Trans i18nKey='quotes.list.tabs.all' />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='active'>Active</TabsTrigger>
|
||||
<TabsTrigger value='emitted'>
|
||||
<Trans i18nKey='quotes.list.tabs.emitted' />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='draft'>
|
||||
<Trans i18nKey='quotes.list.tabs.draft' />
|
||||
</TabsTrigger>
|
||||
@ -50,8 +52,8 @@ export const QuotesList = () => {
|
||||
<TabsContent value='archived'>
|
||||
<QuotesDataTable status='archived' />
|
||||
</TabsContent>
|
||||
<TabsContent value='active'>
|
||||
<QuotesDataTable status='active' />
|
||||
<TabsContent value='emitted'>
|
||||
<QuotesDataTable status='emitted' />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DataTableProvider>
|
||||
|
||||
@ -81,7 +81,8 @@
|
||||
"subtitle": "",
|
||||
"tabs": {
|
||||
"all": "Todas",
|
||||
"draft": "Borrador",
|
||||
"draft": "Borradores",
|
||||
"emitted": "Emitidas",
|
||||
"archived": "Archivadas"
|
||||
},
|
||||
"columns": {
|
||||
|
||||
@ -1,24 +1,52 @@
|
||||
import * as handlebars from "handlebars";
|
||||
import * as puppeteer from "puppeteer";
|
||||
import { Quote } from "../../../../../../domain";
|
||||
import { ICollection } from "@shared/contexts";
|
||||
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||
import { ISalesContext } from "../../../../../Sales.context";
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import * as handlebars from "handlebars";
|
||||
import path from "path";
|
||||
import * as puppeteer from "puppeteer";
|
||||
|
||||
export interface IReportQuoteReporter {
|
||||
toPDF: (quote: Quote, context: ISalesContext) => any;
|
||||
toHTML: (quote: Quote, context: ISalesContext) => string;
|
||||
toPDF: (quote: Quote, context: ISalesContext) => Promise<Buffer>;
|
||||
}
|
||||
|
||||
export const ReportQuotePresenter: IReportQuoteReporter = {
|
||||
toPDF: async (quote: Quote, context: ISalesContext): Promise<Buffer> => {
|
||||
toHTML: (quote: Quote, context: ISalesContext): string => {
|
||||
const quote_dto = map(quote, context);
|
||||
|
||||
// Obtener y compilar la plantilla HTML
|
||||
const templateHtml = obtenerPlantillaHTML();
|
||||
const template = handlebars.compile(templateHtml);
|
||||
const html = template(quote);
|
||||
const template = handlebars.compile(templateHtml, {});
|
||||
const html = template(quote_dto);
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
toPDF: async (quote: Quote, context: ISalesContext): Promise<Buffer> => {
|
||||
const html = ReportQuotePresenter.toHTML(quote, context);
|
||||
|
||||
// Generar el PDF con Puppeteer
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html);
|
||||
const pdfBuffer = await page.pdf({ format: "A4" });
|
||||
|
||||
await page.setContent(html, { waitUntil: "networkidle2" });
|
||||
const pdfBuffer = await page.pdf({
|
||||
//path: "quote.pdf",
|
||||
format: "A4",
|
||||
printBackground: false,
|
||||
displayHeaderFooter: false,
|
||||
//headerTemplate: `<div style="font-size:12px; text-align:center; width:100%; margin:0 auto;">Quote #${quote_dto.id}</div>`,
|
||||
//footerTemplate: `<div style="font-size:12px; text-align:center; width:100%; margin:0 auto;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>`,
|
||||
margin: {
|
||||
top: "0mm",
|
||||
bottom: "0mm",
|
||||
left: "0mm",
|
||||
right: "0mm",
|
||||
},
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
return pdfBuffer;
|
||||
@ -26,15 +54,64 @@ export const ReportQuotePresenter: IReportQuoteReporter = {
|
||||
};
|
||||
|
||||
const obtenerPlantillaHTML = (): string => {
|
||||
// Implementar la lógica para obtener la plantilla HTML
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
<title>Factura</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Factura: </h1>
|
||||
<p>Cliente:</p>
|
||||
</body>
|
||||
</html>`;
|
||||
return readFileSync(path.join(__dirname, "./templates/quote.hbs")).toString();
|
||||
};
|
||||
|
||||
const map = (quote: Quote, context: ISalesContext) => {
|
||||
const { dealer } = context;
|
||||
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.toString(),
|
||||
date: quote.date.toISO8601(),
|
||||
reference: quote.reference.toString(),
|
||||
customer_information: quote.customer.toString(),
|
||||
lang_code: quote.language.toString(),
|
||||
currency_code: quote.currency.toString(),
|
||||
|
||||
payment_method: quote.paymentMethod.toString(),
|
||||
validity: quote.validity.toString(),
|
||||
notes: quote.notes.toString(),
|
||||
|
||||
subtotal_price: quote.subtotalPrice.toFormat(),
|
||||
|
||||
discount: quote.discount.toFormat(),
|
||||
discount_price: quote.discountPrice.toFormat(),
|
||||
|
||||
before_tax_price: quote.beforeTaxPrice.toFormat(),
|
||||
|
||||
tax: quote.tax.toFormat(),
|
||||
tax_price: quote.taxPrice.toFormat(),
|
||||
|
||||
total_price: quote.totalPrice.toFormat(),
|
||||
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
|
||||
dealer: {
|
||||
id: dealer?.id.toString(),
|
||||
name: dealer?.name.toString(),
|
||||
currency_code: dealer?.currency.code,
|
||||
lang_code: dealer?.language.code,
|
||||
contact_information: dealer?.additionalInfo.get("contact_information"),
|
||||
default_payment_method: dealer?.additionalInfo.get("contact_information"),
|
||||
default_notes: dealer?.additionalInfo.get("contact_information"),
|
||||
default_legal_terms: dealer?.additionalInfo.get("contact_information"),
|
||||
default_quote_validity: dealer?.additionalInfo.get("contact_information"),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContext): any[] =>
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: QuoteItem) => ({
|
||||
article_id: item.articleId.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,129 @@
|
||||
<html lang="{{lang_code}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Presupuesto #{{id}}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body class="px-20 py-12 bg-transparent border">
|
||||
<div>
|
||||
<div class="flex items-center justify-between pb-4 mb-4 border-b">
|
||||
<div class="bg-red-400 border border-green-500">
|
||||
<h1 class="text-2xl font-bold">DISTRIBUIDOR OFICIAL</h1>
|
||||
<div class="flex space-x-4 bg-blue-500 border">
|
||||
<div class="mt-2">
|
||||
<img src="https://via.placeholder.com/100x50" alt="Logo distribuidor" />
|
||||
</div>
|
||||
<div class="mt-2 text-sm">
|
||||
{{dealer.contact_information}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<img src="https://via.placeholder.com/150x50" alt="Uecko Logo" />
|
||||
<p class="text-xs text-gray-500">Essential Furniture</p>
|
||||
<p class="text-xs text-gray-500">PREMIO AMBARRO DEL AÑO 2021</p>
|
||||
<p class="text-xs text-gray-500">LUXURY SPAIN</p>
|
||||
<p class="text-xs text-gray-500">ELLE 2021</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
||||
<div>
|
||||
<p class="text-sm"><strong>Presupuesto nº:</strong> {{id}}</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> {{reference}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm">{{customer_information}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2 class="mb-2 text-xl font-semibold">Cotización</h2>
|
||||
|
||||
<table class="min-w-full bg-white">
|
||||
<thead class="bg-gray-200">
|
||||
<tr class="text-xs">
|
||||
<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>
|
||||
{{#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>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pb-4 mb-4 border-b">
|
||||
<div>
|
||||
<div class="pt-4 border-t">
|
||||
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
|
||||
</div>
|
||||
<div class="pt-4 border-t">
|
||||
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<table class="min-w-full bg-transparent">
|
||||
<tbody>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2 border">Importe neto</td>
|
||||
<td class="px-4 py-2 border"></td>
|
||||
<td class="px-4 py-2 text-right border">{{subtotal_price}}</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2 border">% 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 border">Base imponible</td>
|
||||
<td class="px-4 py-2 border"></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 border">% 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 border">Importe total</td>
|
||||
<td class="px-4 py-2 border"></td>
|
||||
<td class="px-4 py-2 text-right border">{{total_price}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 mt-4 border-t">
|
||||
<p class="text-xs text-gray-500">Información básica sobre protección de datos</p>
|
||||
<p class="text-xs text-gray-500">{{quote.default_legal_terms}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -6,6 +6,7 @@ import responseTime from "response-time";
|
||||
import { configurePassportAuth } from "@/contexts/auth";
|
||||
import morgan from "morgan";
|
||||
import passport from "passport";
|
||||
import path from "path";
|
||||
import { v1Routes } from "./api/v1";
|
||||
|
||||
//const logger = initLogger(rTracer);
|
||||
@ -56,6 +57,10 @@ configurePassportAuth(passport);
|
||||
// Express configuration
|
||||
app.set("port", process.env.PORT ?? 3000);
|
||||
|
||||
// Public assets
|
||||
app.use("/assets", express.static(path.join(__dirname, "/public")));
|
||||
|
||||
// API
|
||||
app.use("/api/v1", v1Routes());
|
||||
|
||||
export { app };
|
||||
|
||||
@ -383,7 +383,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
}
|
||||
|
||||
public toFormat(format?: string, roundingMode?: RoundingMode): string {
|
||||
return this.props.toFormat(format, roundingMode);
|
||||
return this._isNull ? "" : this.props.toFormat(format, roundingMode);
|
||||
}
|
||||
|
||||
public toUnit(): number {
|
||||
|
||||
@ -245,6 +245,14 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
||||
return this.isNull() ? 0 : Number(this.toString());
|
||||
}
|
||||
|
||||
public toFormat(): string {
|
||||
return this._isNull
|
||||
? ""
|
||||
: Intl.NumberFormat("es-ES", {
|
||||
maximumFractionDigits: 2,
|
||||
}).format(this.toNumber());
|
||||
}
|
||||
|
||||
public toPrimitive(): NullOr<number> {
|
||||
if (this.scale !== Percentage.DEFAULT_SCALE) {
|
||||
return this.convertScale(Percentage.DEFAULT_SCALE).toPrimitive();
|
||||
|
||||
@ -222,6 +222,14 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
||||
return this.isNull() ? 0 : Number(this.toString());
|
||||
}
|
||||
|
||||
public toFormat(): string {
|
||||
return this._isNull
|
||||
? ""
|
||||
: Intl.NumberFormat("es-ES", {
|
||||
maximumFractionDigits: 2,
|
||||
}).format(this.toNumber());
|
||||
}
|
||||
|
||||
public toPrimitive(): NullOr<number> {
|
||||
if (this.scale !== Quantity.DEFAULT_SCALE) {
|
||||
return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user