feat: agregar soporte para múltiples idiomas en el informe de presupuesto
This commit is contained in:
parent
c77a3a3af5
commit
e0f52da54f
@ -0,0 +1,57 @@
|
||||
export const reportLabels = {
|
||||
es: {
|
||||
official_distributor: "DISTRIBUIDOR OFICIAL",
|
||||
dealer_logo: "Logo distribuidor",
|
||||
document_title: "PRESUPUESTO",
|
||||
browser_title: "Presupuesto",
|
||||
quote_number: "Presupuesto nº:",
|
||||
date: "Fecha:",
|
||||
validity: "Validez:",
|
||||
salesperson: "Vendedor:",
|
||||
customer_reference: "Referencia cliente:",
|
||||
page: "Página",
|
||||
of: "de",
|
||||
quantity: "Cant.",
|
||||
description: "Descripción",
|
||||
unit_price: "Prec. Unitario",
|
||||
subtotal: "Subtotal",
|
||||
discount: "Dto (%)",
|
||||
total_amount: "Importe total",
|
||||
payment_method: "Forma de pago:",
|
||||
notes: "Notas:",
|
||||
net_amount: "Importe neto",
|
||||
discounts: "Descuentos",
|
||||
percentage_discount: "% Descuento",
|
||||
taxable_amount: "Base imponible",
|
||||
},
|
||||
en: {
|
||||
official_distributor: "OFFICIAL DISTRIBUTOR",
|
||||
dealer_logo: "Dealer logo",
|
||||
document_title: "QUOTE",
|
||||
browser_title: "Quote",
|
||||
quote_number: "Quote no.:",
|
||||
date: "Date:",
|
||||
validity: "Validity:",
|
||||
salesperson: "Salesperson:",
|
||||
customer_reference: "Customer reference:",
|
||||
page: "Page",
|
||||
of: "of",
|
||||
quantity: "Qty.",
|
||||
description: "Description",
|
||||
unit_price: "Unit price",
|
||||
subtotal: "Subtotal",
|
||||
discount: "Discount (%)",
|
||||
total_amount: "Total amount",
|
||||
payment_method: "Payment method:",
|
||||
notes: "Notes:",
|
||||
net_amount: "Net amount",
|
||||
discounts: "Discounts",
|
||||
percentage_discount: "% Discount",
|
||||
taxable_amount: "Taxable amount",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ReportLanguage = keyof typeof reportLabels;
|
||||
|
||||
export const resolveReportLanguage = (language: string): ReportLanguage =>
|
||||
language === "en" ? "en" : "es";
|
||||
@ -2,7 +2,12 @@ import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
|
||||
import { createQuoteMapper } from "@/contexts/sales/infrastructure/mappers/quote.mapper";
|
||||
import { ReportQuotePresenter } from "./ReportQuote.reporter";
|
||||
|
||||
const source = (showLineDiscountColumn: boolean, generalDiscount: number, taxLabel = "% IVA") =>
|
||||
const source = (
|
||||
showLineDiscountColumn: boolean,
|
||||
generalDiscount: number,
|
||||
taxLabel = "% IVA",
|
||||
langCode = "es"
|
||||
) =>
|
||||
({
|
||||
show_line_discount_column: showLineDiscountColumn,
|
||||
items: [],
|
||||
@ -12,7 +17,7 @@ const source = (showLineDiscountColumn: boolean, generalDiscount: number, taxLab
|
||||
date: "2026-10-03",
|
||||
reference: "Q-1",
|
||||
currency_code: "EUR",
|
||||
lang_code: "es",
|
||||
lang_code: langCode,
|
||||
customer_reference: "",
|
||||
customer_information: "Customer",
|
||||
validity: "",
|
||||
@ -51,4 +56,25 @@ describe("ReportQuotePresenter", () => {
|
||||
|
||||
expect(html).toContain("% IGIC");
|
||||
});
|
||||
|
||||
it("renders English report labels while preserving quote item snapshots", async () => {
|
||||
const html = await ReportQuotePresenter.toHTML(
|
||||
mapper.mapToDomain(source(false, 0, "% IVA", "en")),
|
||||
context
|
||||
);
|
||||
|
||||
expect(html).toContain('<html lang="en">');
|
||||
expect(html).toContain("QUOTE");
|
||||
expect(html).toContain("Description");
|
||||
expect(html).not.toContain(">PRESUPUESTO<");
|
||||
});
|
||||
|
||||
it("falls back to Spanish labels and language for unsupported quote languages", async () => {
|
||||
const html = await ReportQuotePresenter.toHTML(mapper.mapToDomain(source(false, 0, "% IGIC", "fr")), context);
|
||||
|
||||
expect(html).toContain('<html lang="es">');
|
||||
expect(html).toContain("PRESUPUESTO");
|
||||
expect(html).toContain("Descripción");
|
||||
expect(html).toContain("% IGIC");
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@ import * as handlebars from "handlebars";
|
||||
import path from "path";
|
||||
import puppeteer from "puppeteer";
|
||||
import report from "puppeteer-report";
|
||||
import { reportLabels, resolveReportLanguage } from "./ReportQuote.labels";
|
||||
|
||||
export interface IReportQuoteReporter {
|
||||
toHTML: (quote: Quote, context: ISalesContext) => Promise<string>;
|
||||
@ -63,6 +64,7 @@ export const ReportQuotePresenter: IReportQuoteReporter = {
|
||||
|
||||
const map = async (quote: Quote, context: ISalesContext) => {
|
||||
const { dealer } = context;
|
||||
const langCode = resolveReportLanguage(quote.language.toString());
|
||||
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
@ -71,7 +73,8 @@ const map = async (quote: Quote, context: ISalesContext) => {
|
||||
reference: quote.reference.toString(),
|
||||
customer_reference: quote.customerReference.toString(),
|
||||
customer_information: quote.customer.toString(),
|
||||
lang_code: quote.language.toString(),
|
||||
lang_code: langCode,
|
||||
labels: reportLabels[langCode],
|
||||
currency_code: quote.currency.toString(),
|
||||
|
||||
payment_method: quote.paymentMethod.toString(),
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<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>
|
||||
<title>{{labels.browser_title}} #{{id}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
<header id="header">
|
||||
<aside class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">DISTRIBUIDOR OFICIAL</h3>
|
||||
<h3 class="text-lg font-semibold">{{labels.official_distributor}}</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
|
||||
@ -46,25 +46,25 @@
|
||||
</div>
|
||||
</aside>
|
||||
<section class="flex pb-2 space-x-4">
|
||||
<img id="dealer-logo" src={{dealer.logo}} alt="Logo distribuidor" />
|
||||
<img id="dealer-logo" src={{dealer.logo}} alt="{{labels.dealer_logo}}" />
|
||||
<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>
|
||||
<p class="text-sm"><strong>{{labels.quote_number}}</strong> {{reference}}</p>
|
||||
<p class="text-sm"><strong>{{labels.date}}</strong> {{date}}</p>
|
||||
<p class="text-sm"><strong>{{labels.validity}}</strong> {{validity}}</p>
|
||||
<p class="text-sm"><strong>{{labels.salesperson}}</strong> {{dealer.name}}</p>
|
||||
<p class="text-sm"><strong>{{labels.customer_reference}}</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>
|
||||
<h3 class="text-2xl font-normal">{{labels.document_title}}</h3>
|
||||
<div id="header-pagination">
|
||||
Página <span class="pageNumber"></span> de <span class="totalPages"></span>
|
||||
{{labels.page}} <span class="pageNumber"></span> {{labels.of}} <span class="totalPages"></span>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
@ -73,14 +73,14 @@
|
||||
<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">{{labels.quantity}}</th>
|
||||
<th class="px-2 py-2 border">{{labels.description}}</th>
|
||||
<th class="px-2 py-2 text-right border">{{labels.unit_price}}</th>
|
||||
<th class="px-2 py-2 text-right border">{{labels.subtotal}}</th>
|
||||
{{#if show_line_discount_column}}
|
||||
<th class="px-2 py-2 text-right border">Dto (%)</th>
|
||||
<th class="px-2 py-2 text-right border">{{labels.discount}}</th>
|
||||
{{/if}}
|
||||
<th class="px-2 py-2 text-right border">Importe total</th>
|
||||
<th class="px-2 py-2 text-right border">{{labels.total_amount}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-body">
|
||||
@ -104,10 +104,10 @@
|
||||
|
||||
<div class="grow">
|
||||
<div class="pt-4">
|
||||
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
|
||||
<p class="text-sm"><strong>{{labels.payment_method}}</strong> {{payment_method}}</p>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
|
||||
<p class="text-sm"><strong>{{labels.notes}}</strong> {{notes}} </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -115,26 +115,26 @@
|
||||
<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">{{labels.net_amount}}</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{subtotal_price}}</td>
|
||||
</tr>
|
||||
{{#if show_line_discount_column}}
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">Descuentos</td>
|
||||
<td class="px-4 py-2">{{labels.discounts}}</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{line_discount_price}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{#if has_general_discount}}
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">% Descuento</td>
|
||||
<td class="px-4 py-2">{{labels.percentage_discount}}</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>
|
||||
{{/if}}
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">Base imponible</td>
|
||||
<td class="px-4 py-2">{{labels.taxable_amount}}</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{before_tax_price}}</td>
|
||||
</tr>
|
||||
@ -144,7 +144,7 @@
|
||||
<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">{{labels.total_amount}}</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{total_price}}</td>
|
||||
</tr>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user