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 {
id: invoiceItem.id.toPrimitive(),
isNonValued: String(invoiceItem.isNonValued),
is_non_valued: String(invoiceItem.isNonValued),
position: String(index),
description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()),

View File

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

View File

@ -9,7 +9,7 @@ import {
UtcDate,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems, InvoicePaymentMethod } from "../entities";
import { CustomerInvoiceItems, InvoicePaymentMethod, InvoiceTaxTotal } from "../entities";
import {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
@ -68,6 +68,8 @@ export interface ICustomerInvoice {
getTaxesAmount(): InvoiceAmount;
getTotalAmount(): InvoiceAmount;
getTaxes(): InvoiceTaxTotal[];
issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result<CustomerInvoice, Error>;
}
@ -198,10 +200,6 @@ export class CustomerInvoice
return this._items;
}
public get taxes() {
return this.items.getTaxesAmountByTaxes();
}
public get hasRecipient() {
return this.recipient.isSome();
}
@ -224,7 +222,9 @@ export class CustomerInvoice
private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
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);
}
return amount;
@ -262,6 +262,24 @@ export class CustomerInvoice
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() {
const subtotalAmount = this.getSubtotalAmount();
const discountAmount = this._getDiscountAmount(subtotalAmount);

View File

@ -6,7 +6,7 @@ import {
ItemDiscount,
ItemQuantity,
} from "../../value-objects";
import { ItemTaxes } from "../item-taxes";
import { ItemTaxTotal, ItemTaxes } from "../item-taxes";
export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>;
@ -109,6 +109,12 @@ export class CustomerInvoiceItem
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 {
const discount = this.discountPercentage.match(
(percentage) => percentage,
@ -117,18 +123,44 @@ export class CustomerInvoiceItem
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 {
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 {
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 {
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 {
const curCode = this.currencyCode.code;
const quantity = this.quantity.match(
@ -143,18 +175,34 @@ export class CustomerInvoiceItem
return unitAmount.multiply(quantity);
}
/**
* @summary Calcula el importe total de descuento del ítem.
* @returns Un `ItemAmount` con el importe descontado.
*/
public getDiscountAmount(): ItemAmount {
return this._getDiscountAmount(this.getSubtotalAmount());
}
/**
* @summary Calcula el importe imponible (subtotal descuento).
* @returns Un `ItemAmount` con la base imponible del ítem.
*/
public getTaxableAmount(): ItemAmount {
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 {
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 {
const taxableAmount = this.getTaxableAmount();
const taxesAmount = this._getTaxesAmount(taxableAmount);
@ -162,10 +210,26 @@ export class CustomerInvoiceItem
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());
}
/**
* @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() {
const subtotalAmount = this.getSubtotalAmount();
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 { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects";
import { ItemTaxes } from "../item-taxes";
import { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps {
@ -25,6 +26,15 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
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 {
// 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.
@ -37,6 +47,10 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
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 {
return this.getAll().reduce(
(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 {
return this.getAll().reduce(
(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 {
return this.getAll().reduce(
(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 {
return this.getAll().reduce(
(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 {
return this.getAll().reduce(
(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<{
tax: Tax;
taxableAmount: ItemAmount;
@ -105,4 +147,20 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
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 { InvoiceTax } from "./invoice-tax";
export interface InvoiceTaxesProps {
items?: InvoiceTax[];
}
export type InvoiceTaxTotal = {
tax: Tax;
taxableAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
};
export class InvoiceTaxes extends Collection<InvoiceTax> {
constructor(props: InvoiceTaxesProps) {
const { items = [] } = props;
super(items);
}
public static create(props: InvoiceTaxesProps): InvoiceTaxes {
return new InvoiceTaxes(props);
export class InvoiceTaxes extends Taxes {
constructor(items: Tax[] = [], totalItems: number | null = null) {
super(items, totalItems);
}
public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
return this.getAll().reduce(
(total, tax) => total.add(tax.getTaxAmount(taxableAmount)),
(total, tax) => total.add(taxableAmount.percentage(tax.percentage)),
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 {
return this.getAll()
.map((taxItem) => taxItem.tax.code)
.map((taxItem) => taxItem.code)
.join(", ");
}
}

View File

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