.
This commit is contained in:
parent
ebb2d06903
commit
e73aeaeefe
@ -31,7 +31,9 @@ export const QuotesList = () => {
|
|||||||
<TabsTrigger value='all'>
|
<TabsTrigger value='all'>
|
||||||
<Trans i18nKey='quotes.list.tabs.all' />
|
<Trans i18nKey='quotes.list.tabs.all' />
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value='active'>Active</TabsTrigger>
|
<TabsTrigger value='emitted'>
|
||||||
|
<Trans i18nKey='quotes.list.tabs.emitted' />
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value='draft'>
|
<TabsTrigger value='draft'>
|
||||||
<Trans i18nKey='quotes.list.tabs.draft' />
|
<Trans i18nKey='quotes.list.tabs.draft' />
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -50,8 +52,8 @@ export const QuotesList = () => {
|
|||||||
<TabsContent value='archived'>
|
<TabsContent value='archived'>
|
||||||
<QuotesDataTable status='archived' />
|
<QuotesDataTable status='archived' />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='active'>
|
<TabsContent value='emitted'>
|
||||||
<QuotesDataTable status='active' />
|
<QuotesDataTable status='emitted' />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DataTableProvider>
|
</DataTableProvider>
|
||||||
|
|||||||
@ -81,7 +81,8 @@
|
|||||||
"subtitle": "",
|
"subtitle": "",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Todas",
|
"all": "Todas",
|
||||||
"draft": "Borrador",
|
"draft": "Borradores",
|
||||||
|
"emitted": "Emitidas",
|
||||||
"archived": "Archivadas"
|
"archived": "Archivadas"
|
||||||
},
|
},
|
||||||
"columns": {
|
"columns": {
|
||||||
|
|||||||
@ -1,24 +1,52 @@
|
|||||||
import * as handlebars from "handlebars";
|
import { ICollection } from "@shared/contexts";
|
||||||
import * as puppeteer from "puppeteer";
|
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||||
import { Quote } from "../../../../../../domain";
|
|
||||||
import { ISalesContext } from "../../../../../Sales.context";
|
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 {
|
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 = {
|
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
|
// Obtener y compilar la plantilla HTML
|
||||||
const templateHtml = obtenerPlantillaHTML();
|
const templateHtml = obtenerPlantillaHTML();
|
||||||
const template = handlebars.compile(templateHtml);
|
const template = handlebars.compile(templateHtml, {});
|
||||||
const html = template(quote);
|
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
|
// Generar el PDF con Puppeteer
|
||||||
const browser = await puppeteer.launch();
|
const browser = await puppeteer.launch();
|
||||||
const page = await browser.newPage();
|
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();
|
await browser.close();
|
||||||
|
|
||||||
return pdfBuffer;
|
return pdfBuffer;
|
||||||
@ -26,15 +54,64 @@ export const ReportQuotePresenter: IReportQuoteReporter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const obtenerPlantillaHTML = (): string => {
|
const obtenerPlantillaHTML = (): string => {
|
||||||
// Implementar la lógica para obtener la plantilla HTML
|
return readFileSync(path.join(__dirname, "./templates/quote.hbs")).toString();
|
||||||
return `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Factura</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Factura: </h1>
|
|
||||||
<p>Cliente:</p>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 { configurePassportAuth } from "@/contexts/auth";
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
|
import path from "path";
|
||||||
import { v1Routes } from "./api/v1";
|
import { v1Routes } from "./api/v1";
|
||||||
|
|
||||||
//const logger = initLogger(rTracer);
|
//const logger = initLogger(rTracer);
|
||||||
@ -56,6 +57,10 @@ configurePassportAuth(passport);
|
|||||||
// Express configuration
|
// Express configuration
|
||||||
app.set("port", process.env.PORT ?? 3000);
|
app.set("port", process.env.PORT ?? 3000);
|
||||||
|
|
||||||
|
// Public assets
|
||||||
|
app.use("/assets", express.static(path.join(__dirname, "/public")));
|
||||||
|
|
||||||
|
// API
|
||||||
app.use("/api/v1", v1Routes());
|
app.use("/api/v1", v1Routes());
|
||||||
|
|
||||||
export { app };
|
export { app };
|
||||||
|
|||||||
@ -383,7 +383,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toFormat(format?: string, roundingMode?: RoundingMode): string {
|
public toFormat(format?: string, roundingMode?: RoundingMode): string {
|
||||||
return this.props.toFormat(format, roundingMode);
|
return this._isNull ? "" : this.props.toFormat(format, roundingMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toUnit(): number {
|
public toUnit(): number {
|
||||||
|
|||||||
@ -245,6 +245,14 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
|||||||
return this.isNull() ? 0 : Number(this.toString());
|
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> {
|
public toPrimitive(): NullOr<number> {
|
||||||
if (this.scale !== Percentage.DEFAULT_SCALE) {
|
if (this.scale !== Percentage.DEFAULT_SCALE) {
|
||||||
return this.convertScale(Percentage.DEFAULT_SCALE).toPrimitive();
|
return this.convertScale(Percentage.DEFAULT_SCALE).toPrimitive();
|
||||||
|
|||||||
@ -222,6 +222,14 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
|||||||
return this.isNull() ? 0 : Number(this.toString());
|
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> {
|
public toPrimitive(): NullOr<number> {
|
||||||
if (this.scale !== Quantity.DEFAULT_SCALE) {
|
if (this.scale !== Quantity.DEFAULT_SCALE) {
|
||||||
return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();
|
return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user