factura tiene en cuenta forma de pago

This commit is contained in:
David Arranz 2025-09-22 19:31:49 +02:00
parent 6cc728fb87
commit 0367c51a97
13 changed files with 199 additions and 29 deletions

View File

@ -0,0 +1,13 @@
export function formatPaymentMethodDTO(paymentMethod: object) {
if (!paymentMethod) {
return null;
}
//Construir objeto paymentMethod para comprobar que existe
//const value = PaymentMethod.create(
// id: paymentMethod.payment_id),
//).data;
//return value.format(locale);
return paymentMethod.payment_description;
}

View File

@ -1,5 +1,6 @@
export * from "./format-date-dto"; export * from "./format-date-dto";
export * from "./format-money-dto"; export * from "./format-money-dto";
export * from "./format-payment_method-dto";
export * from "./format-percentage-dto"; export * from "./format-percentage-dto";
export * from "./format-quantity-dto"; export * from "./format-quantity-dto";
export * from "./map-dto-to-customer-invoice-props"; export * from "./map-dto-to-customer-invoice-props";

View File

@ -24,6 +24,17 @@ export class CustomerInvoiceFullPresenter extends Presenter<
const items = itemsPresenter.toOutput(invoice.items); const items = itemsPresenter.toOutput(invoice.items);
const allAmounts = invoice.getAllAmounts(); const allAmounts = invoice.getAllAmounts();
const payment = invoice.paymentMethod.match(
(payment) => {
const { id, payment_description } = payment.toObjectString();
return {
payment_id: id,
payment_description,
};
},
() => undefined
);
return { return {
id: invoice.id.toString(), id: invoice.id.toString(),
company_id: invoice.companyId.toString(), company_id: invoice.companyId.toString(),
@ -45,6 +56,8 @@ export class CustomerInvoiceFullPresenter extends Presenter<
taxes: invoice.taxes.getCodesToString(), taxes: invoice.taxes.getCodesToString(),
payment_method: payment,
subtotal_amount: allAmounts.subtotalAmount.toObjectString(), subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_percentage: invoice.discountPercentage.toObjectString(), discount_percentage: invoice.discountPercentage.toObjectString(),

View File

@ -6,6 +6,7 @@ import {
formatMoneyDTO, formatMoneyDTO,
formatPercentageDTO, formatPercentageDTO,
} from "../../helpers"; } from "../../helpers";
import { formatPaymentMethodDTO } from "../../helpers/format-payment_method-dto";
export class CustomerInvoiceReportPresenter extends Presenter< export class CustomerInvoiceReportPresenter extends Presenter<
GetCustomerInvoiceByIdResponseDTO, GetCustomerInvoiceByIdResponseDTO,
@ -40,6 +41,8 @@ export class CustomerInvoiceReportPresenter extends Presenter<
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions), taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions),
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions), taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions),
total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions), total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions),
payment_method: formatPaymentMethodDTO(invoiceDTO.payment_method),
}; };
} }
} }

View File

@ -45,14 +45,14 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter<
right: "10mm", right: "10mm",
top: "10mm", top: "10mm",
}, },
// landscape: false, landscape: false,
// preferCSSPageSize: true, preferCSSPageSize: true,
// omitBackground: false, omitBackground: false,
// printBackground: true, printBackground: true,
// displayHeaderFooter: false, displayHeaderFooter: false,
// 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></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></div>',
}); });
await browser.close(); await browser.close();

View File

@ -185,17 +185,26 @@
<section id="resume" class="flex items-center justify-between pb-4 mb-4"> <section id="resume" class="flex items-center justify-between pb-4 mb-4">
<div class="grow"> <div class="grow relative pt-10 self-start">
<div class="pt-4"> {{#if payment_method}}
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p> <div class="">
<p class=" text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
</div> </div>
{{else}}
<!-- Empty payment method-->
{{/if}}
{{#if notes}}
<div class="pt-4"> <div class="pt-4">
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p> <p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
</div> </div>
{{else}}
<!-- Empty notes-->
{{/if}}
</div> </div>
<div class="relative pt-10 grow"> <div class="relative pt-10 grow">
<table class="table-header min-w-full bg-transparent"> <table class=" table-header min-w-full bg-transparent">
<tbody> <tbody>
{{#if discount_percentage}} {{#if discount_percentage}}
<tr> <tr>
@ -235,6 +244,7 @@
</section> </section>
</main> </main>
<footer id="footer" class="mt-4"> <footer id="footer" class="mt-4">
<aside> <aside>
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 <p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212

View File

@ -111,18 +111,17 @@
<body> <body>
<header id="header"> <header id="header">
<aside class="flex items-start mb-4 w-full bg-red-600"> <aside class="flex items-start mb-4 w-full ">
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda --> <!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
<div class="flex flex-col 70% items-start text-left bg-green-700"> <div class="flex flex-col items-start text-left" style="flex:0 0 70%;">
<img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" /> <img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
<div class="flex w-full"> <div class="flex w-full">
<div class="p-1 "> <div class="p-1 ">
<h3 class="text-2xl font-normal">PROFORMA</h3>
<p>Nº:<strong>&nbsp;{{invoice_number}}</strong></p> <p>Nº:<strong>&nbsp;{{invoice_number}}</strong></p>
<p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p> <p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p>
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p> <p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
</div> </div>
<div class="p-1 ml-9"> <div class="p-1 ml-28">
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2> <h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
<p>{{recipient.tin}}</p> <p>{{recipient.tin}}</p>
<p>{{recipient.street}}</p> <p>{{recipient.street}}</p>
@ -132,10 +131,10 @@
</div> </div>
<!-- Bloque DERECHO: logo2 arriba y texto DEBAJO --> <!-- Bloque DERECHO: logo2 arriba y texto DEBAJO -->
<div class="ml-auto flex flex-col items-end text-right"> <div class="ml-auto flex flex-col items-end text-right h-full">
<img src="https://rodax-software.com/images/logo2.jpg" alt="Logo secundario" <img src="https://rodax-software.com/images/logo2.jpg" alt="Logo secundario"
class="block h-5 w-auto md:h-8 mb-1" /> class="block h-5 w-auto md:h-8 mb-1" />
<div class="not-italic text-xs leading-tight"> <div class="not-italic text-xs leading-tight h-full">
<p>Telf: 91 785 02 47 / 686 62 10 59</p> <p>Telf: 91 785 02 47 / 686 62 10 59</p>
<p><a href="mailto:info@rodax-software.com" class="hover:underline">info@rodax-software.com</a></p> <p><a href="mailto:info@rodax-software.com" class="hover:underline">info@rodax-software.com</a></p>
<p><a href="https://www.rodax-software.com" target="_blank" rel="noopener" <p><a href="https://www.rodax-software.com" target="_blank" rel="noopener"
@ -146,6 +145,7 @@
</header> </header>
<main id="main"> <main id="main">
<h1>FACTURA PROFORMA</h1>
<section id="details" class="border-b border-black "> <section id="details" class="border-b border-black ">
<div class="relative pt-0 border-b border-black"> <div class="relative pt-0 border-b border-black">
<!-- Badge TOTAL decorado con imagen --> <!-- Badge TOTAL decorado con imagen -->
@ -186,17 +186,26 @@
<section id="resume" class="flex items-center justify-between pb-4 mb-4"> <section id="resume" class="flex items-center justify-between pb-4 mb-4">
<div class="grow"> <div class="grow relative pt-10 self-start">
<div class="pt-4"> {{#if payment_method}}
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p> <div class="">
<p class=" text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
</div> </div>
{{else}}
<!-- Empty payment method-->
{{/if}}
{{#if notes}}
<div class="pt-4"> <div class="pt-4">
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p> <p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
</div> </div>
{{else}}
<!-- Empty notes-->
{{/if}}
</div> </div>
<div class="relative pt-10 grow"> <div class="relative pt-10 grow">
<table class="table-header min-w-full bg-transparent"> <table class=" table-header min-w-full bg-transparent">
<tbody> <tbody>
{{#if discount_percentage}} {{#if discount_percentage}}
<tr> <tr>
@ -238,9 +247,10 @@
<footer id="footer" class="mt-4"> <footer id="footer" class="mt-4">
<aside> <aside>
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 <p class="text-left"><strong>Factura Proforma.</strong>
| CIF: B83999441 - Este documento es de carácter informativo y no tiene validez contable ni fiscal. Contiene precios y condiciones
Rodax Software S.L.</p> de venta sujetos a confirmación del cliente. Solo adquirirá validez como factura definitiva una vez aceptados
dichos términos.</p>
</aside> </aside>
</footer> </footer>

View File

@ -9,7 +9,7 @@ import {
UtcDate, UtcDate,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems } from "../entities"; import { CustomerInvoiceItems, InvoicePaymentMethod } from "../entities";
import { InvoiceTaxes } from "../entities/invoice-taxes"; import { InvoiceTaxes } from "../entities/invoice-taxes";
import { import {
CustomerInvoiceNumber, CustomerInvoiceNumber,
@ -41,10 +41,15 @@ export interface CustomerInvoiceProps {
taxes: InvoiceTaxes; taxes: InvoiceTaxes;
items: CustomerInvoiceItems; items: CustomerInvoiceItems;
paymentMethod: Maybe<InvoicePaymentMethod>;
discountPercentage: Percentage; discountPercentage: Percentage;
} }
export interface ICustomerInvoice { export interface ICustomerInvoice {
hasRecipient: boolean;
hasPaymentMethod: boolean;
getSubtotalAmount(): InvoiceAmount; getSubtotalAmount(): InvoiceAmount;
getDiscountAmount(): InvoiceAmount; getDiscountAmount(): InvoiceAmount;
@ -140,6 +145,10 @@ export class CustomerInvoice
return this.props.recipient; return this.props.recipient;
} }
public get paymentMethod(): Maybe<InvoicePaymentMethod> {
return this.props.paymentMethod;
}
public get languageCode(): LanguageCode { public get languageCode(): LanguageCode {
return this.props.languageCode; return this.props.languageCode;
} }
@ -165,6 +174,10 @@ export class CustomerInvoice
return this.recipient.isSome(); return this.recipient.isSome();
} }
public get hasPaymentMethod() {
return this.paymentMethod.isSome();
}
private _getDiscountAmount(subtotalAmount: InvoiceAmount): InvoiceAmount { private _getDiscountAmount(subtotalAmount: InvoiceAmount): InvoiceAmount {
return subtotalAmount.percentage(this.discountPercentage); return subtotalAmount.percentage(this.discountPercentage);
} }

View File

@ -1,3 +1,4 @@
export * from "./customer-invoice-items"; export * from "./customer-invoice-items";
export * from "./invoice-payment-method";
export * from "./invoice-taxes"; export * from "./invoice-taxes";
export * from "./item-taxes"; export * from "./item-taxes";

View File

@ -0,0 +1 @@
export * from "./invoice-payment-method";

View File

@ -0,0 +1,32 @@
import { DomainEntity, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
export interface InvoicePaymentMethodProps {
paymentDescription: string;
}
export class InvoicePaymentMethod extends DomainEntity<InvoicePaymentMethodProps> {
public static create(
props: InvoicePaymentMethodProps,
id?: UniqueID
): Result<InvoicePaymentMethod, Error> {
const item = new InvoicePaymentMethod(props, id);
return Result.ok(item);
}
get paymentDescription(): string {
return this.props.paymentDescription;
}
getProps(): InvoicePaymentMethodProps {
return this.props;
}
toObjectString() {
return {
id: String(this.id),
payment_description: String(this.paymentDescription),
};
}
}

View File

@ -11,7 +11,7 @@ import {
extractOrPushError, extractOrPushError,
maybeFromNullableVO, maybeFromNullableVO,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoiceItems, CustomerInvoiceItems,
@ -19,6 +19,7 @@ import {
CustomerInvoiceProps, CustomerInvoiceProps,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
InvoicePaymentMethod,
} from "../../../domain"; } from "../../../domain";
import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes"; import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes";
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize";
@ -53,7 +54,50 @@ export class CustomerInvoiceDomainMapper
this._taxesMapper = new TaxesFullMapper(params); this._taxesMapper = new TaxesFullMapper(params);
} }
private mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { private _mapPaymentMethodToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as {
errors: ValidationErrorDetail[];
};
const paymentId = extractOrPushError(
maybeFromNullableVO(source.payment_method_id, (value) => UniqueID.create(value)),
"payment_method_id",
errors
);
const paymentDescription = extractOrPushError(
maybeFromNullableVO(source.payment_method_description, (value) => Result.ok(String(value))),
"payment_method_description",
errors
);
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Invoice payment mapping failed", errors));
}
if (paymentDescription!.isNone() || paymentId!.isNone()) {
return Result.ok(Maybe.none<InvoicePaymentMethod>());
}
const paymentResult = InvoicePaymentMethod.create(
{
paymentDescription: paymentDescription?.getOrUndefined()!,
},
paymentId?.getOrUndefined()!
);
if (paymentResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice payment method creation failed", [
{ path: "paymentMethod", message: paymentResult.error.message },
])
);
}
return Result.ok(Maybe.some<InvoicePaymentMethod>(paymentResult.data));
}
private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as { const { errors } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
}; };
@ -151,7 +195,7 @@ export class CustomerInvoiceDomainMapper
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
const attributes = this.mapAttributesToDomain(source, { errors, ...params }); const attributes = this._mapAttributesToDomain(source, { errors, ...params });
// 2) Recipient (snapshot en la factura o include) // 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(source, { const recipientResult = this._recipientMapper.mapToDomain(source, {
@ -204,6 +248,16 @@ export class CustomerInvoiceDomainMapper
}); });
} }
// Payment method
const paymentMethodResult = this._mapPaymentMethodToDomain(source, { errors, ...params });
if (paymentMethodResult.isFailure) {
errors.push({
path: "paymentMethod",
message: paymentMethodResult.error.message,
});
}
// 5) Si hubo errores de mapeo, devolvemos colección de validación // 5) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) { if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Customer mapping failed", errors)); return Result.fail(new ValidationErrorCollection("Customer mapping failed", errors));
@ -212,6 +266,7 @@ export class CustomerInvoiceDomainMapper
// 6) Construcción del agregado (Dominio) // 6) Construcción del agregado (Dominio)
const recipient = recipientResult.data; const recipient = recipientResult.data;
const paymentMethod = paymentMethodResult.data;
const taxes = InvoiceTaxes.create({ const taxes = InvoiceTaxes.create({
items: taxesResults.data.getAll(), items: taxesResults.data.getAll(),
@ -243,6 +298,8 @@ export class CustomerInvoiceDomainMapper
discountPercentage: attributes.discountPercentage!, discountPercentage: attributes.discountPercentage!,
paymentMethod: paymentMethod!,
taxes: taxes, taxes: taxes,
items, items,
}; };

View File

@ -78,6 +78,10 @@ export class CustomerInvoiceModel extends Model<
declare customer_postal_code: string; declare customer_postal_code: string;
declare customer_country: string; declare customer_country: string;
// Método de pago
declare payment_method_id: string;
declare payment_method_description: string;
// Relaciones // Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>; declare items: NonAttribute<CustomerInvoiceItemModel[]>;
declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>; declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>;
@ -185,6 +189,18 @@ export default (database: Sequelize) => {
defaultValue: null, defaultValue: null,
}, },
payment_method_id: {
type: DataTypes.UUID,
allowNull: true,
defaultValue: null,
},
payment_method_description: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
subtotal_amount_value: { subtotal_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: false, allowNull: false,