Facturas de cliente

This commit is contained in:
David Arranz 2025-10-12 11:14:33 +02:00
parent 700ace3b76
commit efcae31500
7 changed files with 196 additions and 33 deletions

View File

@ -17,7 +17,7 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
return { return {
id: invoiceItem.id.toPrimitive(), id: invoiceItem.id.toPrimitive(),
isNonValued: String(invoiceItem.isNonValued), is_non_valued: String(invoiceItem.isNonValued),
position: String(index), position: String(index),
description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()), description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()),

View File

@ -35,6 +35,17 @@ export class CustomerInvoiceFullPresenter extends Presenter<
() => undefined () => undefined
); );
const invoiceTaxes = invoice.getTaxes().map((taxItem) => {
console.log(taxItem);
return {
tax_code: taxItem.tax.code,
taxable_amount: taxItem.taxableAmount.toObjectString(),
taxes_amount: taxItem.taxesAmount.toObjectString(),
};
});
console.log(invoiceTaxes);
return { return {
id: invoice.id.toString(), id: invoice.id.toString(),
company_id: invoice.companyId.toString(), company_id: invoice.companyId.toString(),
@ -56,13 +67,7 @@ export class CustomerInvoiceFullPresenter extends Presenter<
customer_id: invoice.customerId.toString(), customer_id: invoice.customerId.toString(),
recipient, recipient,
taxes: invoice.taxes.map((taxItem) => { taxes: invoiceTaxes,
return {
tax_code: taxItem.tax.code,
taxable_amount: taxItem.taxableAmount.convertScale(2).toObjectString(),
taxes_amount: taxItem.taxesAmount.convertScale(2).toObjectString(),
};
}),
payment_method: payment, payment_method: payment,

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, InvoicePaymentMethod } from "../entities"; import { CustomerInvoiceItems, InvoicePaymentMethod, InvoiceTaxTotal } from "../entities";
import { import {
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceSerie, CustomerInvoiceSerie,
@ -68,6 +68,8 @@ export interface ICustomerInvoice {
getTaxesAmount(): InvoiceAmount; getTaxesAmount(): InvoiceAmount;
getTotalAmount(): InvoiceAmount; getTotalAmount(): InvoiceAmount;
getTaxes(): InvoiceTaxTotal[];
issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result<CustomerInvoice, Error>; issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result<CustomerInvoice, Error>;
} }
@ -198,10 +200,6 @@ export class CustomerInvoice
return this._items; return this._items;
} }
public get taxes() {
return this.items.getTaxesAmountByTaxes();
}
public get hasRecipient() { public get hasRecipient() {
return this.recipient.isSome(); return this.recipient.isSome();
} }
@ -224,7 +222,9 @@ export class CustomerInvoice
private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
let amount = InvoiceAmount.zero(this.currencyCode.code); let amount = InvoiceAmount.zero(this.currencyCode.code);
for (const taxItem of this.taxes) { const itemTaxes = this.items.getTaxesAmountByTaxes();
for (const taxItem of itemTaxes) {
amount = amount.add(taxItem.taxesAmount); amount = amount.add(taxItem.taxesAmount);
} }
return amount; return amount;
@ -262,6 +262,24 @@ export class CustomerInvoice
return this._getTotalAmount(taxableAmount, taxesAmount); return this._getTotalAmount(taxableAmount, taxesAmount);
} }
public getTaxes(): InvoiceTaxTotal[] {
const itemTaxes = this.items.getTaxesAmountByTaxes();
return itemTaxes.map((item) => {
return {
tax: item.tax,
taxableAmount: InvoiceAmount.create({
value: item.taxableAmount.convertScale(2).value,
currency_code: this.currencyCode.code,
}).data,
taxesAmount: InvoiceAmount.create({
value: item.taxesAmount.convertScale(2).value,
currency_code: this.currencyCode.code,
}).data,
};
});
}
public getAllAmounts() { public getAllAmounts() {
const subtotalAmount = this.getSubtotalAmount(); const subtotalAmount = this.getSubtotalAmount();
const discountAmount = this._getDiscountAmount(subtotalAmount); const discountAmount = this._getDiscountAmount(subtotalAmount);

View File

@ -6,7 +6,7 @@ import {
ItemDiscount, ItemDiscount,
ItemQuantity, ItemQuantity,
} from "../../value-objects"; } from "../../value-objects";
import { ItemTaxes } from "../item-taxes"; import { ItemTaxTotal, ItemTaxes } from "../item-taxes";
export interface CustomerInvoiceItemProps { export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>; description: Maybe<CustomerInvoiceItemDescription>;
@ -109,6 +109,12 @@ export class CustomerInvoiceItem
return this.getProps(); return this.getProps();
} }
/**
* @private
* @summary Calcula el importe de descuento a partir del subtotal y el porcentaje.
* @param subtotalAmount - Importe subtotal.
* @returns El importe de descuento calculado.
*/
private _getDiscountAmount(subtotalAmount: ItemAmount): ItemAmount { private _getDiscountAmount(subtotalAmount: ItemAmount): ItemAmount {
const discount = this.discountPercentage.match( const discount = this.discountPercentage.match(
(percentage) => percentage, (percentage) => percentage,
@ -117,18 +123,44 @@ export class CustomerInvoiceItem
return subtotalAmount.percentage(discount); return subtotalAmount.percentage(discount);
} }
/**
* @private
* @summary Calcula el importe imponible restando el descuento al subtotal.
* @param subtotalAmount - Importe subtotal.
* @param discountAmount - Importe de descuento.
* @returns El importe imponible resultante.
*/
private _getTaxableAmount(subtotalAmount: ItemAmount, discountAmount: ItemAmount): ItemAmount { private _getTaxableAmount(subtotalAmount: ItemAmount, discountAmount: ItemAmount): ItemAmount {
return subtotalAmount.subtract(discountAmount); return subtotalAmount.subtract(discountAmount);
} }
/**
* @private
* @summary Calcula el importe total de impuestos sobre la base imponible.
* @param taxableAmount - Importe imponible.
* @returns El importe de impuestos calculado.
*/
private _getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { private _getTaxesAmount(taxableAmount: ItemAmount): ItemAmount {
return this.props.taxes.getTaxesAmount(taxableAmount); return this.props.taxes.getTaxesAmount(taxableAmount);
} }
/**
* @private
* @summary Calcula el importe total sumando base imponible e impuestos.
* @param taxableAmount - Importe imponible.
* @param taxesAmount - Importe de impuestos.
* @returns El importe total del ítem.
*/
private _getTotalAmount(taxableAmount: ItemAmount, taxesAmount: ItemAmount): ItemAmount { private _getTotalAmount(taxableAmount: ItemAmount, taxesAmount: ItemAmount): ItemAmount {
return taxableAmount.add(taxesAmount); return taxableAmount.add(taxesAmount);
} }
/**
* @summary Calcula el subtotal del ítem (cantidad × importe unitario).
* @returns Un `ItemAmount` con el subtotal del ítem.
* @remarks
* Si la cantidad o el importe unitario no están definidos, se asumen valores cero.
*/
public getSubtotalAmount(): ItemAmount { public getSubtotalAmount(): ItemAmount {
const curCode = this.currencyCode.code; const curCode = this.currencyCode.code;
const quantity = this.quantity.match( const quantity = this.quantity.match(
@ -143,18 +175,34 @@ export class CustomerInvoiceItem
return unitAmount.multiply(quantity); return unitAmount.multiply(quantity);
} }
/**
* @summary Calcula el importe total de descuento del ítem.
* @returns Un `ItemAmount` con el importe descontado.
*/
public getDiscountAmount(): ItemAmount { public getDiscountAmount(): ItemAmount {
return this._getDiscountAmount(this.getSubtotalAmount()); return this._getDiscountAmount(this.getSubtotalAmount());
} }
/**
* @summary Calcula el importe imponible (subtotal descuento).
* @returns Un `ItemAmount` con la base imponible del ítem.
*/
public getTaxableAmount(): ItemAmount { public getTaxableAmount(): ItemAmount {
return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount()); return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount());
} }
/**
* @summary Calcula el importe total de impuestos aplicados al ítem.
* @returns Un `ItemAmount` con el total de impuestos.
*/
public getTaxesAmount(): ItemAmount { public getTaxesAmount(): ItemAmount {
return this._getTaxesAmount(this.getTaxableAmount()); return this._getTaxesAmount(this.getTaxableAmount());
} }
/**
* @summary Calcula el importe total final del ítem (base imponible + impuestos).
* @returns Un `ItemAmount` con el importe total.
*/
public getTotalAmount(): ItemAmount { public getTotalAmount(): ItemAmount {
const taxableAmount = this.getTaxableAmount(); const taxableAmount = this.getTaxableAmount();
const taxesAmount = this._getTaxesAmount(taxableAmount); const taxesAmount = this._getTaxesAmount(taxableAmount);
@ -162,10 +210,26 @@ export class CustomerInvoiceItem
return this._getTotalAmount(taxableAmount, taxesAmount); return this._getTotalAmount(taxableAmount, taxesAmount);
} }
public getTaxesAmountByTaxes() { /**
* @summary Obtiene los importes de impuestos agrupados por tipo de impuesto.
* @returns Una colección con la base imponible y el importe de impuestos por cada tipo.
*/
public getTaxesAmountByTaxes(): ItemTaxTotal[] {
return this.taxes.getTaxesAmountByTaxes(this.getTaxableAmount()); return this.taxes.getTaxesAmountByTaxes(this.getTaxableAmount());
} }
/**
* @summary Devuelve todos los importes calculados del ítem en un único objeto.
* @returns Un objeto con las propiedades:
* - `subtotalAmount`
* - `discountAmount`
* - `taxableAmount`
* - `taxesAmount`
* - `totalAmount`
* @remarks
* Este método es útil para mostrar todos los cálculos en la interfaz de usuario
* o serializar el ítem con sus valores calculados.
*/
public getAllAmounts() { public getAllAmounts() {
const subtotalAmount = this.getSubtotalAmount(); const subtotalAmount = this.getSubtotalAmount();
const discountAmount = this._getDiscountAmount(subtotalAmount); const discountAmount = this._getDiscountAmount(subtotalAmount);

View File

@ -2,6 +2,7 @@ import { Tax } from "@erp/core/api";
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects"; import { ItemAmount } from "../../value-objects";
import { ItemTaxes } from "../item-taxes";
import { CustomerInvoiceItem } from "./customer-invoice-item"; import { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps { export interface CustomerInvoiceItemsProps {
@ -25,6 +26,15 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
return new CustomerInvoiceItems(props); return new CustomerInvoiceItems(props);
} }
/**
* @summary Añade un nuevo ítem a la colección.
* @param item - El ítem de factura a añadir.
* @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado.
* @remarks
* Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con
* los de la colección. Si no coinciden, el método devuelve `false` sin modificar
* la colección.
*/
add(item: CustomerInvoiceItem): boolean { add(item: CustomerInvoiceItem): boolean {
// Antes de añadir un nuevo item, debo comprobar que el item a añadir // Antes de añadir un nuevo item, debo comprobar que el item a añadir
// tiene el mismo "currencyCode" y "languageCode" que la colección de items. // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
@ -37,6 +47,10 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
return super.add(item); return super.add(item);
} }
/**
* @summary Calcula el subtotal de todos los ítems sin descuentos ni impuestos.
* @returns Un `ItemAmount` con el subtotal total de la colección en su moneda.
*/
public getSubtotalAmount(): ItemAmount { public getSubtotalAmount(): ItemAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, tax) => total.add(tax.getSubtotalAmount()), (total, tax) => total.add(tax.getSubtotalAmount()),
@ -44,6 +58,10 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
); );
} }
/**
* @summary Calcula el importe total de descuentos aplicados a todos los ítems.
* @returns Un `ItemAmount` con el importe total de descuentos.
*/
public getDiscountAmount(): ItemAmount { public getDiscountAmount(): ItemAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, item) => total.add(item.getDiscountAmount()), (total, item) => total.add(item.getDiscountAmount()),
@ -51,6 +69,10 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
); );
} }
/**
* @summary Calcula el importe imponible total (base antes de impuestos).
* @returns Un `ItemAmount` con el total imponible.
*/
public getTaxableAmount(): ItemAmount { public getTaxableAmount(): ItemAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, item) => total.add(item.getTaxableAmount()), (total, item) => total.add(item.getTaxableAmount()),
@ -58,6 +80,10 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
); );
} }
/**
* @summary Calcula el importe total de impuestos de todos los ítems.
* @returns Un `ItemAmount` con la suma de todos los impuestos aplicados.
*/
public getTaxesAmount(): ItemAmount { public getTaxesAmount(): ItemAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, item) => total.add(item.getTaxesAmount()), (total, item) => total.add(item.getTaxesAmount()),
@ -65,6 +91,10 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
); );
} }
/**
* @summary Calcula el importe total final de todos los ítems (subtotal -descuentos + impuestos).
* @returns Un `ItemAmount` con el total global de la colección.
*/
public getTotalAmount(): ItemAmount { public getTotalAmount(): ItemAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, item) => total.add(item.getTotalAmount()), (total, item) => total.add(item.getTotalAmount()),
@ -72,6 +102,18 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
); );
} }
/**
* @summary Agrupa los importes imponibles e impuestos por tipo de impuesto.
* @returns Un array con objetos que contienen:
* - `tax`: El tipo de impuesto.
* - `taxableAmount`: El total de base imponible asociada a ese impuesto.
* - `taxesAmount`: El importe total de impuestos calculado.
* @remarks
* Los impuestos se agrupan por su `tax.code` (código fiscal).
* Si existen varios impuestos con el mismo código, sus importes se agregan.
* En caso de conflicto de datos en impuestos con mismo código, prevalece
* el primer `Tax` encontrado.
*/
public getTaxesAmountByTaxes(): Array<{ public getTaxesAmountByTaxes(): Array<{
tax: Tax; tax: Tax;
taxableAmount: ItemAmount; taxableAmount: ItemAmount;
@ -105,4 +147,20 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
return Array.from(resultMap.values()); return Array.from(resultMap.values());
} }
/**
* @summary Obtiene la lista de impuestos únicos aplicados en todos los ítems.
* @returns Un objeto `ItemTaxes` que contiene todos los impuestos distintos.
* @remarks
* Los impuestos se deduplican por su código (`tax.code`).
* Si existen varios impuestos con el mismo código, el último encontrado
* sobrescribirá los anteriores sin generar error.
*/
public getTaxes(): ItemTaxes {
const taxes = this.getAll()
.flatMap((item) => item.taxes.getAll())
.reduce((map, tax) => map.set(tax.code, tax), new Map<string, Tax>());
return ItemTaxes.create([...taxes.values()]);
}
} }

View File

@ -1,31 +1,43 @@
import { Collection } from "@repo/rdx-utils"; import { Tax, Taxes } from "@erp/core/api";
import { InvoiceAmount } from "../../value-objects"; import { InvoiceAmount } from "../../value-objects";
import { InvoiceTax } from "./invoice-tax";
export interface InvoiceTaxesProps { export type InvoiceTaxTotal = {
items?: InvoiceTax[]; tax: Tax;
} taxableAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
};
export class InvoiceTaxes extends Collection<InvoiceTax> { export class InvoiceTaxes extends Taxes {
constructor(props: InvoiceTaxesProps) { constructor(items: Tax[] = [], totalItems: number | null = null) {
const { items = [] } = props; super(items, totalItems);
super(items);
}
public static create(props: InvoiceTaxesProps): InvoiceTaxes {
return new InvoiceTaxes(props);
} }
public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, tax) => total.add(tax.getTaxAmount(taxableAmount)), (total, tax) => total.add(taxableAmount.percentage(tax.percentage)),
InvoiceAmount.zero(taxableAmount.currencyCode) InvoiceAmount.zero(taxableAmount.currencyCode)
) as InvoiceAmount; );
}
public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: InvoiceAmount): InvoiceAmount {
const currencyCode = taxableAmount.currencyCode;
return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => {
return taxableAmount.percentage(itemTax.percentage).add(totalAmount);
}, InvoiceAmount.zero(currencyCode));
}
public getTaxesAmountByTaxes(taxableAmount: InvoiceAmount): InvoiceTaxTotal[] {
return this.getAll().map((taxItem) => ({
taxableAmount,
tax: taxItem,
taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount),
}));
} }
public getCodesToString(): string { public getCodesToString(): string {
return this.getAll() return this.getAll()
.map((taxItem) => taxItem.tax.code) .map((taxItem) => taxItem.code)
.join(", "); .join(", ");
} }
} }

View File

@ -1,6 +1,12 @@
import { Tax, Taxes } from "@erp/core/api"; import { Tax, Taxes } from "@erp/core/api";
import { ItemAmount } from "../../value-objects"; import { ItemAmount } from "../../value-objects";
export type ItemTaxTotal = {
tax: Tax;
taxableAmount: ItemAmount;
taxesAmount: ItemAmount;
};
export class ItemTaxes extends Taxes { export class ItemTaxes extends Taxes {
constructor(items: Tax[] = [], totalItems: number | null = null) { constructor(items: Tax[] = [], totalItems: number | null = null) {
super(items, totalItems); super(items, totalItems);
@ -21,7 +27,7 @@ export class ItemTaxes extends Taxes {
}, ItemAmount.zero(currencyCode)); }, ItemAmount.zero(currencyCode));
} }
public getTaxesAmountByTaxes(taxableAmount: ItemAmount) { public getTaxesAmountByTaxes(taxableAmount: ItemAmount): ItemTaxTotal[] {
return this.getAll().map((taxItem) => ({ return this.getAll().map((taxItem) => ({
taxableAmount, taxableAmount,
tax: taxItem, tax: taxItem,