.
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
5d5ec76ad6
commit
9961383a9f
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user