Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
David Arranz 2026-05-12 21:42:21 +02:00
parent 5d5ec76ad6
commit 9961383a9f
13 changed files with 119 additions and 199 deletions

View File

@ -127,7 +127,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
proforma: Proforma, proforma: Proforma,
item: ReturnType<Proforma["items"]["getAll"]>[number] item: ReturnType<Proforma["items"]["getAll"]>[number]
): Result<IssuedInvoiceItem, Error> { ): Result<IssuedInvoiceItem, Error> {
const itemTotals = item.totals(); const calculationContext = {
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
globalDiscountPercentage: proforma.globalDiscountPercentage,
};
const itemTotals = item.totals(calculationContext);
return IssuedInvoiceItem.create({ return IssuedInvoiceItem.create({
description: item.description, description: item.description,
@ -139,7 +145,7 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
itemDiscountPercentage: item.itemDiscountPercentage, itemDiscountPercentage: item.itemDiscountPercentage,
itemDiscountAmount: itemTotals.itemDiscountAmount, itemDiscountAmount: itemTotals.itemDiscountAmount,
globalDiscountPercentage: item.globalDiscountPercentage, globalDiscountPercentage: proforma.globalDiscountPercentage,
globalDiscountAmount: itemTotals.globalDiscountAmount, globalDiscountAmount: itemTotals.globalDiscountAmount,
totalDiscountAmount: itemTotals.totalDiscountAmount, totalDiscountAmount: itemTotals.totalDiscountAmount,

View File

@ -22,7 +22,13 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
) {} ) {}
toOutput(proforma: Proforma): ProformaFullSnapshot { toOutput(proforma: Proforma): ProformaFullSnapshot {
const items = this.itemsBuilder.toOutput(proforma.items); const calculationContext = {
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
globalDiscountPercentage: proforma.globalDiscountPercentage,
};
const items = this.itemsBuilder.toOutput(proforma.items, calculationContext);
const recipient = this.recipientBuilder.toOutput(proforma); const recipient = this.recipientBuilder.toOutput(proforma);
const taxes = this.taxesBuilder.toOutput(proforma.taxes()); const taxes = this.taxesBuilder.toOutput(proforma.taxes());
//const paymentMethod = this.paymentMethodBuilder.toOutput(proforma.paymentMethod); //const paymentMethod = this.paymentMethodBuilder.toOutput(proforma.paymentMethod);

View File

@ -1,17 +1,21 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common"; import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common";
import { maybeToNullable } from "@repo/rdx-ddd"; import { maybeToNullable } from "@repo/rdx-ddd";
import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain"; import { ItemAmount, type ProformaCalculationContext, type ProformaItem, type ProformaItems } from "../../../../domain";
export interface IProformaItemsFullSnapshotBuilder export interface IProformaItemsFullSnapshotBuilder {
extends ISnapshotBuilder<ProformaItems, ProformaItemDetailDTO[]> {} toOutput(invoiceItems: ProformaItems, context: ProformaCalculationContext): ProformaItemDetailDTO[];
}
export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder { export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder {
private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO { private mapItem(
const allAmounts = proformaItem.totals(); proformaItem: ProformaItem,
context: ProformaCalculationContext,
index: number
): ProformaItemDetailDTO {
const allAmounts = proformaItem.totals(context);
const isValued = proformaItem.isValued(); const isValued = proformaItem.isValued();
const currencyCode = proformaItem.currencyCode.code; const currencyCode = context.currencyCode.code;
return { return {
id: proformaItem.id.toPrimitive(), id: proformaItem.id.toPrimitive(),
@ -34,7 +38,7 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
? allAmounts.itemDiscountAmount.toObjectString() ? allAmounts.itemDiscountAmount.toObjectString()
: ItemAmount.zero(currencyCode).toObjectString(), : ItemAmount.zero(currencyCode).toObjectString(),
global_discount_percentage: proformaItem.globalDiscountPercentage.toObjectString(), global_discount_percentage: context.globalDiscountPercentage.toObjectString(),
global_discount_amount: isValued global_discount_amount: isValued
? allAmounts.globalDiscountAmount.toObjectString() ? allAmounts.globalDiscountAmount.toObjectString()
@ -81,7 +85,7 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
}; };
} }
toOutput(invoiceItems: ProformaItems): ProformaItemDetailDTO[] { toOutput(invoiceItems: ProformaItems, context: ProformaCalculationContext): ProformaItemDetailDTO[] {
return invoiceItems.map((item, index) => this.mapItem(item, index)); return invoiceItems.map((item, index) => this.mapItem(item, context, index));
} }
} }

View File

@ -27,7 +27,7 @@ import {
ProformaItems, ProformaItems,
} from "../entities"; } from "../entities";
import { ProformaItemMismatch } from "../errors"; import { ProformaItemMismatch } from "../errors";
import type { IProformaTaxTotals } from "../services"; import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
import { ProformaItemTaxes } from "../value-objects"; import { ProformaItemTaxes } from "../value-objects";
export interface IProformaCreateProps { export interface IProformaCreateProps {
@ -128,9 +128,6 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
const internalItems = ProformaItems.create({ const internalItems = ProformaItems.create({
items: [], items: [],
languageCode: props.languageCode,
currencyCode: props.currencyCode,
globalDiscountPercentage: props.globalDiscountPercentage,
}); });
const { items, ...internalProps } = props; const { items, ...internalProps } = props;
@ -201,9 +198,6 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
for (const [index, itemProps] of itemsProps.entries()) { for (const [index, itemProps] of itemsProps.entries()) {
const itemResult = ProformaItem.create({ const itemResult = ProformaItem.create({
...itemProps, ...itemProps,
languageCode: this.languageCode,
currencyCode: this.currencyCode,
globalDiscountPercentage: this.globalDiscountPercentage,
}); });
if (itemResult.isFailure) { if (itemResult.isFailure) {
@ -413,7 +407,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
* La cabecera NO recalcula lógica de porcentaje toda la lógica está en Item/Items. * La cabecera NO recalcula lógica de porcentaje toda la lógica está en Item/Items.
*/ */
public totals(): IProformaTotals { public totals(): IProformaTotals {
const itemsTotals = this.items.totals(); const itemsTotals = this.items.totals(this.calculationContext());
return { return {
subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount), subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount),
@ -434,7 +428,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
} }
public taxes(): Collection<IProformaTaxTotals> { public taxes(): Collection<IProformaTaxTotals> {
return this.items.taxes(); return this.items.taxes(this.calculationContext());
} }
public addItem(props: IProformaItemCreateProps): Result<void, Error> { public addItem(props: IProformaItemCreateProps): Result<void, Error> {
@ -457,6 +451,14 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return Result.ok(); return Result.ok();
} }
private calculationContext(): ProformaCalculationContext {
return {
languageCode: this.languageCode,
currencyCode: this.currencyCode,
globalDiscountPercentage: this.globalDiscountPercentage,
};
}
// Helpers // Helpers
/** /**

View File

@ -1,8 +1,9 @@
import { DiscountPercentage, type Tax, type TaxPercentage } from "@erp/core/api"; import { DiscountPercentage, type Tax, type TaxPercentage } from "@erp/core/api";
import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd"; import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common"; import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common";
import type { ProformaCalculationContext } from "../../services";
import { import {
ProformaItemTaxes, ProformaItemTaxes,
type ProformaItemTaxesProps, type ProformaItemTaxesProps,
@ -34,18 +35,9 @@ export interface IProformaItemCreateProps {
itemDiscountPercentage: Maybe<DiscountPercentage>; // % descuento de línea itemDiscountPercentage: Maybe<DiscountPercentage>; // % descuento de línea
taxes: ProformaItemTaxesProps; taxes: ProformaItemTaxesProps;
// Estos campos vienen de la cabecera,
// pero se necesitan para cálculos y representaciones de la línea.
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
} }
export type ProformaItemPatchProps = Omit< export type ProformaItemPatchProps = IProformaItemCreateProps;
IProformaItemCreateProps,
"globalDiscountPercentage" | "languageCode" | "currencyCode"
>;
export interface IProformaItemTotals { export interface IProformaItemTotals {
subtotalAmount: ItemAmount; subtotalAmount: ItemAmount;
@ -67,16 +59,12 @@ export interface IProformaItemTotals {
export interface IProformaItem { export interface IProformaItem {
description: Maybe<ItemDescription>; description: Maybe<ItemDescription>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
quantity: Maybe<ItemQuantity>; quantity: Maybe<ItemQuantity>;
unitAmount: Maybe<ItemAmount>; unitAmount: Maybe<ItemAmount>;
taxes: ProformaItemTaxes; taxes: ProformaItemTaxes;
itemDiscountPercentage: Maybe<DiscountPercentage>; // Descuento en línea itemDiscountPercentage: Maybe<DiscountPercentage>; // Descuento en línea
globalDiscountPercentage: DiscountPercentage; // Descuento en cabecera
ivaCode(): Maybe<string>; ivaCode(): Maybe<string>;
ivaPercentage(): Maybe<TaxPercentage>; ivaPercentage(): Maybe<TaxPercentage>;
@ -87,7 +75,8 @@ export interface IProformaItem {
retentionCode(): Maybe<string>; retentionCode(): Maybe<string>;
retentionPercentage(): Maybe<TaxPercentage>; retentionPercentage(): Maybe<TaxPercentage>;
totals(): IProformaItemTotals; totals(context: ProformaCalculationContext): IProformaItemTotals;
subtotalAmount(context: ProformaCalculationContext): ItemAmount;
isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado" isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
} }
@ -135,14 +124,6 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return this.props.description; return this.props.description;
} }
get languageCode() {
return this.props.languageCode;
}
get currencyCode() {
return this.props.currencyCode;
}
get quantity() { get quantity() {
return this.props.quantity; return this.props.quantity;
} }
@ -155,10 +136,6 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return this.props.itemDiscountPercentage; return this.props.itemDiscountPercentage;
} }
get globalDiscountPercentage() {
return this.props.globalDiscountPercentage;
}
get taxes() { get taxes() {
return this.props.taxes; return this.props.taxes;
} }
@ -178,9 +155,9 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return this.props.quantity.isSome() && this.props.unitAmount.isSome(); return this.props.quantity.isSome() && this.props.unitAmount.isSome();
} }
public subtotalAmount(): ItemAmount { public subtotalAmount(context: ProformaCalculationContext): ItemAmount {
if (!this.isValued()) { if (!this.isValued()) {
return ItemAmount.zero(this.currencyCode.code); return ItemAmount.zero(context.currencyCode.code);
} }
const quantity = this.quantity.unwrap(); const quantity = this.quantity.unwrap();
@ -240,13 +217,14 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
* - totalAmount * - totalAmount
* *
*/ */
public totals(): IProformaItemTotals { public totals(context: ProformaCalculationContext): IProformaItemTotals {
const subtotalAmount = this._calculateSubtotalAmount(); const subtotalAmount = this._calculateSubtotalAmount(context);
const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount); const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount, context);
const globalDiscountAmount = this._calculateGlobalDiscountAmount( const globalDiscountAmount = this._calculateGlobalDiscountAmount(
subtotalAmount, subtotalAmount,
itemDiscountAmount itemDiscountAmount,
context
); );
const totalDiscountAmount = this._calculateTotalDiscountAmount( const totalDiscountAmount = this._calculateTotalDiscountAmount(
itemDiscountAmount, itemDiscountAmount,
@ -286,9 +264,9 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
/** /**
* @summary Helper puro para calcular el subtotal. * @summary Helper puro para calcular el subtotal.
*/ */
private _calculateSubtotalAmount(): ItemAmount { private _calculateSubtotalAmount(context: ProformaCalculationContext): ItemAmount {
if (!this.isValued()) { if (!this.isValued()) {
return ItemAmount.zero(this.currencyCode.code); return ItemAmount.zero(context.currencyCode.code);
} }
const quantity = this.quantity.unwrap(); const quantity = this.quantity.unwrap();
@ -300,9 +278,12 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
/** /**
* @summary Helper puro para calcular el descuento de línea. * @summary Helper puro para calcular el descuento de línea.
*/ */
private _calculateItemDiscountAmount(subtotal: ItemAmount): ItemAmount { private _calculateItemDiscountAmount(
subtotal: ItemAmount,
context: ProformaCalculationContext
): ItemAmount {
if (!this.isValued() || this.props.itemDiscountPercentage.isNone()) { if (!this.isValued() || this.props.itemDiscountPercentage.isNone()) {
return ItemAmount.zero(this.currencyCode.code); return ItemAmount.zero(context.currencyCode.code);
} }
const discountPercentage = this.props.itemDiscountPercentage.match( const discountPercentage = this.props.itemDiscountPercentage.match(
@ -318,15 +299,16 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
*/ */
private _calculateGlobalDiscountAmount( private _calculateGlobalDiscountAmount(
subtotalAmount: ItemAmount, subtotalAmount: ItemAmount,
discountAmount: ItemAmount discountAmount: ItemAmount,
context: ProformaCalculationContext
): ItemAmount { ): ItemAmount {
if (!this.isValued()) { if (!this.isValued()) {
return ItemAmount.zero(this.currencyCode.code); return ItemAmount.zero(context.currencyCode.code);
} }
const amountAfterLineDiscount = subtotalAmount.subtract(discountAmount); const amountAfterLineDiscount = subtotalAmount.subtract(discountAmount);
const globalDiscount = this.props.globalDiscountPercentage; const globalDiscount = context.globalDiscountPercentage;
return amountAfterLineDiscount.percentage(globalDiscount); return amountAfterLineDiscount.percentage(globalDiscount);
} }

View File

@ -1,126 +1,40 @@
import type { DiscountPercentage } from "@erp/core/api"; import { Collection } from "@repo/rdx-utils";
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { ProformaItemMismatch } from "../../errors"; import {
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../../services"; type IProformaTaxTotals,
import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator"; type ProformaCalculationContext,
ProformaItemsTotalsCalculator,
ProformaTaxesCalculator,
} from "../../services";
import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity"; import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity";
export interface IProformaItemsProps { export interface IProformaItemsProps {
items?: ProformaItem[]; items?: ProformaItem[];
// Estos campos vienen de la cabecera,
// pero se necesitan para cálculos y representaciones de la línea.
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
} }
/*type ProformaItemCreateProps = {
items: IProformaItemCreateProps[];
// Estos campos vienen de la cabecera,
// pero se necesitan para cálculos y representaciones de la línea.
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
};
type InternalProformaProps = Omit<ProformaItemCreateProps, "items"> & {
items: ProformaItem[];
};*/
export interface IProformaItems { export interface IProformaItems {
// OJO, no extendemos de Collection<IProformaItem> para no exponer // OJO, no extendemos de Collection<IProformaItem> para no exponer
// públicamente métodos para manipular la colección. // públicamente métodos para manipular la colección.
valued(): IProformaItem[]; // Devuelve solo las líneas valoradas. valued(): IProformaItem[]; // Devuelve solo las líneas valoradas.
totals(): IProformaItemTotals; totals(context: ProformaCalculationContext): IProformaItemTotals;
taxes(): Collection<IProformaTaxTotals>; taxes(context: ProformaCalculationContext): Collection<IProformaTaxTotals>;
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
} }
export class ProformaItems extends Collection<ProformaItem> implements IProformaItems { export class ProformaItems extends Collection<ProformaItem> implements IProformaItems {
public languageCode!: LanguageCode;
public currencyCode!: CurrencyCode;
public globalDiscountPercentage!: DiscountPercentage;
private constructor(props: IProformaItemsProps) { private constructor(props: IProformaItemsProps) {
super(props.items ?? []); super(props.items ?? []);
this.languageCode = props.languageCode;
this.currencyCode = props.currencyCode;
this.globalDiscountPercentage = props.globalDiscountPercentage;
if (this.items.length > 0) {
this.ensureSameContext(this.items);
}
4;
} }
static create(props: IProformaItemsProps): ProformaItems { static create(props: IProformaItemsProps): ProformaItems {
return new ProformaItems(props); return new ProformaItems(props);
} }
public add(item: ProformaItem): boolean {
console.log("adding item", {
item: {
description: item.description.getOrUndefined()?.toString(),
quantity: item.quantity.getOrUndefined()?.toString(),
unitAmount: item.unitAmount.getOrUndefined()?.toObjectString(),
itemDiscountPercentage: item.itemDiscountPercentage.getOrUndefined()?.toObjectString(),
ivaPercentage: item.ivaPercentage.toString(),
recPercentage: item.recPercentage.toString(),
languageCode: item.languageCode.code,
currencyCode: item.currencyCode.code,
globalDiscountPercentage: item.globalDiscountPercentage.toObjectString(),
},
languageCode: this.languageCode,
currencyCode: this.currencyCode,
globalDiscountPercentage: this.globalDiscountPercentage,
});
const same =
this.languageCode.equals(item.languageCode) &&
this.currencyCode.equals(item.currencyCode) &&
this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
if (!same) return false;
return super.add(item);
}
public valued(): IProformaItem[] { public valued(): IProformaItem[] {
return this.filter((item) => item.isValued()); return this.filter((item) => item.isValued());
} }
/**
* @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 un resultado fallido sin modificar
* la colección.
*/
public addItem(item: ProformaItem): Result<void, Error> {
// 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.
const same =
this.languageCode.equals(item.languageCode) &&
this.currencyCode.equals(item.currencyCode) &&
this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
if (!same) {
return Result.fail(new ProformaItemMismatch(this.size()));
}
super.add(item);
return Result.ok();
}
// Cálculos // Cálculos
/** /**
@ -128,24 +42,11 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
* @remarks * @remarks
* Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos. * Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos.
*/ */
public totals(): IProformaItemTotals { public totals(context: ProformaCalculationContext): IProformaItemTotals {
return new ProformaItemsTotalsCalculator(this).calculate(); return new ProformaItemsTotalsCalculator(this, context).calculate();
} }
public taxes(): Collection<IProformaTaxTotals> { public taxes(context: ProformaCalculationContext): Collection<IProformaTaxTotals> {
return new ProformaTaxesCalculator(this).calculate(); return new ProformaTaxesCalculator(this, context).calculate();
}
private ensureSameContext(items: IProformaItem[]): void {
for (const item of items) {
const same =
item.languageCode.equals(this.languageCode) &&
item.currencyCode.equals(this.currencyCode) &&
item.globalDiscountPercentage.equals(this.globalDiscountPercentage);
if (!same) {
throw new Error("[ProformaItems] All items must share the same context.");
}
}
} }
} }

View File

@ -1 +1,4 @@
export * from "./proforma-compare-tax-totals";
export * from "./proforma-compute-tax-groups";
export * from "./proforma-items-totals-calculator";
export * from "./proforma-taxes-calculator"; export * from "./proforma-taxes-calculator";

View File

@ -4,6 +4,8 @@ import { Maybe } from "@repo/rdx-utils";
import { ItemAmount } from "../../common"; import { ItemAmount } from "../../common";
import type { IProformaItems } from "../entities"; import type { IProformaItems } from "../entities";
import type { ProformaCalculationContext } from "./proforma-items-totals-calculator";
type TaxGroupState = { type TaxGroupState = {
taxableAmount: ItemAmount; taxableAmount: ItemAmount;
@ -30,9 +32,12 @@ type TaxGroupState = {
* - REC y RETENTION pueden ser None. * - REC y RETENTION pueden ser None.
* - No se recalculan porcentajes (se suma lo ya calculado por línea). * - No se recalculan porcentajes (se suma lo ya calculado por línea).
*/ */
export function proformaComputeTaxGroups(items: IProformaItems): Map<string, TaxGroupState> { export function proformaComputeTaxGroups(
items: IProformaItems,
context: ProformaCalculationContext
): Map<string, TaxGroupState> {
const map = new Map<string, TaxGroupState>(); const map = new Map<string, TaxGroupState>();
const currency = items.currencyCode; const currency = context.currencyCode;
for (const item of items.valued()) { for (const item of items.valued()) {
const iva = item.taxes.iva; const iva = item.taxes.iva;
@ -65,7 +70,7 @@ export function proformaComputeTaxGroups(items: IProformaItems): Map<string, Tax
const g = map.get(key)!; const g = map.get(key)!;
const itemTotals = item.totals(); const itemTotals = item.totals(context);
g.taxableAmount = g.taxableAmount.add(itemTotals.taxableAmount); g.taxableAmount = g.taxableAmount.add(itemTotals.taxableAmount);
g.ivaAmount = g.ivaAmount.add(itemTotals.ivaAmount); g.ivaAmount = g.ivaAmount.add(itemTotals.ivaAmount);

View File

@ -1,3 +1,6 @@
import type { DiscountPercentage } from "@erp/core/api";
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { ItemAmount } from "../../common"; import { ItemAmount } from "../../common";
import type { ProformaItems } from "../entities"; import type { ProformaItems } from "../entities";
@ -18,15 +21,24 @@ export interface IProformaItemsTotals {
totalAmount: ItemAmount; totalAmount: ItemAmount;
} }
export interface ProformaCalculationContext {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
globalDiscountPercentage: DiscountPercentage;
}
/** /**
* Acumula los totales (scale 4) a partir de los totales de las líneas valoradas. * Acumula los totales (scale 4) a partir de los totales de las líneas valoradas.
* Aquí no se hace ningúna operación de cálculo. * Aquí no se hace ningúna operación de cálculo.
*/ */
export class ProformaItemsTotalsCalculator { export class ProformaItemsTotalsCalculator {
constructor(private readonly items: ProformaItems) {} constructor(
private readonly items: ProformaItems,
private readonly context: ProformaCalculationContext
) {}
public calculate(): IProformaItemsTotals { public calculate(): IProformaItemsTotals {
const zero = ItemAmount.zero(this.items.currencyCode.code); const zero = ItemAmount.zero(this.context.currencyCode.code);
let subtotalAmount = zero; let subtotalAmount = zero;
@ -44,7 +56,7 @@ export class ProformaItemsTotalsCalculator {
let totalAmount = zero; let totalAmount = zero;
for (const item of this.items.getAll()) { for (const item of this.items.getAll()) {
const amounts = item.totals(); const amounts = item.totals(this.context);
// Subtotales // Subtotales
subtotalAmount = subtotalAmount.add(amounts.subtotalAmount); subtotalAmount = subtotalAmount.add(amounts.subtotalAmount);

View File

@ -6,6 +6,7 @@ import type { IProformaItems } from "../entities";
import { proformaCompareTaxTotals } from "./proforma-compare-tax-totals"; import { proformaCompareTaxTotals } from "./proforma-compare-tax-totals";
import { proformaComputeTaxGroups } from "./proforma-compute-tax-groups"; import { proformaComputeTaxGroups } from "./proforma-compute-tax-groups";
import type { ProformaCalculationContext } from "./proforma-items-totals-calculator";
export interface IProformaTaxTotals { export interface IProformaTaxTotals {
taxableAmount: InvoiceAmount; taxableAmount: InvoiceAmount;
@ -26,10 +27,13 @@ export interface IProformaTaxTotals {
} }
export class ProformaTaxesCalculator { export class ProformaTaxesCalculator {
constructor(private readonly items: IProformaItems) {} constructor(
private readonly items: IProformaItems,
private readonly context: ProformaCalculationContext
) {}
public calculate(): Collection<IProformaTaxTotals> { public calculate(): Collection<IProformaTaxTotals> {
const groups = proformaComputeTaxGroups(this.items); // <- devuelve en escala 4 const groups = proformaComputeTaxGroups(this.items, this.context); // <- devuelve en escala 4
// Vamos acumulando los importes, redondeando previamente a 2 decimales // Vamos acumulando los importes, redondeando previamente a 2 decimales
const rows = Array.from(groups.values()).map((g) => { const rows = Array.from(groups.values()).map((g) => {
@ -65,7 +69,7 @@ export class ProformaTaxesCalculator {
private toInvoiceAmount(amount: ItemAmount): InvoiceAmount { private toInvoiceAmount(amount: ItemAmount): InvoiceAmount {
return InvoiceAmount.create({ return InvoiceAmount.create({
value: amount.convertScale(InvoiceAmount.DEFAULT_SCALE).value, value: amount.convertScale(InvoiceAmount.DEFAULT_SCALE).value,
currency_code: this.items.currencyCode.code, currency_code: this.context.currencyCode.code,
}).data; }).data;
} }
} }

View File

@ -241,9 +241,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
// 6) Construcción del agregado (Dominio) // 6) Construcción del agregado (Dominio)
const items = ProformaItems.create({ const items = ProformaItems.create({
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
items: itemCollectionResults.data.getAll(), items: itemCollectionResults.data.getAll(),
}); });

View File

@ -128,15 +128,10 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
return { return {
itemId, itemId,
languageCode: parent.languageCode,
currencyCode: parent.currencyCode,
description, description,
quantity, quantity,
unitAmount, unitAmount,
itemDiscountPercentage, itemDiscountPercentage,
globalDiscountPercentage: parent.globalDiscountPercentage,
iva, iva,
rec, rec,
retention, retention,
@ -179,11 +174,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
unitAmount: attributes.unitAmount!, unitAmount: attributes.unitAmount!,
itemDiscountPercentage: attributes.itemDiscountPercentage!, itemDiscountPercentage: attributes.itemDiscountPercentage!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
taxes: taxesResult.data, taxes: taxesResult.data,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
}, },
itemId itemId
); );
@ -201,7 +192,11 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
}; };
const allAmounts = source.totals(); const allAmounts = source.totals({
currencyCode: parent.currencyCode,
globalDiscountPercentage: parent.globalDiscountPercentage,
languageCode: parent.languageCode,
});
return Result.ok({ return Result.ok({
item_id: source.id.toPrimitive(), item_id: source.id.toPrimitive(),
@ -236,8 +231,9 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
item_discount_amount_scale: allAmounts.itemDiscountAmount.scale, item_discount_amount_scale: allAmounts.itemDiscountAmount.scale,
// //
global_discount_percentage_value: source.globalDiscountPercentage.value, global_discount_percentage_value: parent.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: source.globalDiscountPercentage.scale, global_discount_percentage_scale:
parent.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE,
global_discount_amount_value: allAmounts.globalDiscountAmount.value, global_discount_amount_value: allAmounts.globalDiscountAmount.value,
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,

View File

@ -28,6 +28,8 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
const defaultRetentionPercentage = const defaultRetentionPercentage =
defaultTaxSummary?.retentionPercentage ?? firstTaxableItem?.retentionPercentage ?? null; defaultTaxSummary?.retentionPercentage ?? firstTaxableItem?.retentionPercentage ?? null;
console.log({ defaultTaxPercentage, defaultRecPercentage, defaultRetentionPercentage, taxMode });
return { return {
series: proforma.series ?? proformaDefaults.series, series: proforma.series ?? proformaDefaults.series,