Configuración fiscal para proformas
This commit is contained in:
parent
659df116a2
commit
eeb285f8b2
@ -11,7 +11,7 @@ Definir la semantica correcta de `create proforma` como creacion de cabecera ini
|
||||
En create:
|
||||
|
||||
- `taxRegimeCode` representa configuracion fiscal inicial
|
||||
- `taxDefaults` sigue siendo un concepto de UI o resolucion interna, no un desglose fiscal persistido
|
||||
- `tax_config` representa la configuracion fiscal persistida de cabecera
|
||||
- `taxes` representa impuestos realmente calculados desde lineas valoradas
|
||||
- `totals` representa importes realmente calculados desde lineas valoradas
|
||||
|
||||
@ -43,6 +43,14 @@ Si `items` se omite o se envia como `[]`:
|
||||
payment_method_id?: string | null;
|
||||
payment_term_id?: string | null;
|
||||
tax_regime_code?: string | null;
|
||||
tax_config?: {
|
||||
tax_mode?: "single" | "per_line";
|
||||
default_iva_code?: string | null;
|
||||
uses_equivalence_surcharge?: boolean;
|
||||
default_rec_code?: string | null;
|
||||
uses_retention?: boolean;
|
||||
default_retention_code?: string | null;
|
||||
};
|
||||
items?: [];
|
||||
}
|
||||
```
|
||||
@ -52,6 +60,7 @@ Notas:
|
||||
- el contrato actual del modulo puede seguir exigiendo otros campos tecnicos ya existentes
|
||||
- `items` es opcional; si existe, puede ir vacio
|
||||
- no se usan defaults ocultos en Zod para disfrazar la intencion del request
|
||||
- si no se informa `tax_config`, el backend resuelve defaults iniciales y los persiste
|
||||
|
||||
## Response esperada al crear sin lineas
|
||||
|
||||
@ -83,6 +92,7 @@ Adaptando nombres exactos al contrato publico actual:
|
||||
## Punto correcto de calculo
|
||||
|
||||
- `create` crea cabecera minima, numera y resuelve defaults
|
||||
- `create` persiste tambien el `tax_config` resuelto
|
||||
- `update` permite anadir o editar lineas
|
||||
- al existir lineas valoradas, `update` recalcula `taxes` y `totals`
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ En proformas:
|
||||
- el backend emite `target_invoice_series_code` como nombre preferente
|
||||
- `series` puede mantenerse solo como alias legacy de salida mientras existan consumidores antiguos
|
||||
- `taxes` representa impuestos calculados a partir de lineas valoradas
|
||||
- `tax_config` representa la configuracion fiscal persistida de cabecera
|
||||
- `totals` representa importes calculados a partir de lineas valoradas
|
||||
- create no debe inventar filas fiscales sobre base `0`
|
||||
|
||||
|
||||
81
docs/customer-invoices/proforma-tax-config.md
Normal file
81
docs/customer-invoices/proforma-tax-config.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Proforma Tax Config
|
||||
|
||||
## Objetivo
|
||||
|
||||
Persistir la configuracion fiscal de cabecera de una proforma sin confundirla con los impuestos realmente calculados desde lineas valoradas.
|
||||
|
||||
## Alcance
|
||||
|
||||
- `tax_regime_code` sigue representando el regimen fiscal general
|
||||
- `tax_config` representa los controles fiscales de cabecera usados por la UI
|
||||
- `taxes` sigue representando solo desglose fiscal materializado
|
||||
- `totals` sigue representando solo importes calculados desde lineas valoradas
|
||||
|
||||
## Campos persistidos en `proformas`
|
||||
|
||||
- `tax_mode`
|
||||
- `default_iva_code`
|
||||
- `uses_equivalence_surcharge`
|
||||
- `default_rec_code`
|
||||
- `uses_retention`
|
||||
- `default_retention_code`
|
||||
|
||||
## Contrato publico
|
||||
|
||||
`GET /proformas/:id` debe devolver:
|
||||
|
||||
```ts
|
||||
{
|
||||
tax_regime: {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
} | null;
|
||||
tax_config: {
|
||||
tax_mode: "single" | "per_line";
|
||||
default_iva_code: string | null;
|
||||
uses_equivalence_surcharge: boolean;
|
||||
default_rec_code: string | null;
|
||||
uses_retention: boolean;
|
||||
default_retention_code: string | null;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`POST /proformas` y `PATCH /proformas/:id` pueden aceptar `tax_config` como bloque independiente de `items`.
|
||||
|
||||
## Regla funcional
|
||||
|
||||
- una proforma puede existir con `items = []`
|
||||
- en ese caso `taxes = []`
|
||||
- en ese caso todos los totales monetarios deben quedar a `0`
|
||||
- la UI debe hidratar los controles de fiscalidad desde `tax_config`, no desde `taxes`
|
||||
|
||||
## Resolucion de defaults en create
|
||||
|
||||
Los defaults del cliente solo se usan al crear la proforma y el resultado se persiste.
|
||||
|
||||
Fallback actual:
|
||||
|
||||
- IVA por defecto: `iva_21`
|
||||
- recargo de equivalencia: desactivado salvo indicacion explicita
|
||||
- retencion: desactivada salvo indicacion explicita
|
||||
- codigo de retencion por defecto si `uses_retention = true`: `retention_15`
|
||||
|
||||
## Regla de update
|
||||
|
||||
`tax_config` se actualiza de forma parcial y se mergea sobre la configuracion persistida actual.
|
||||
|
||||
No debe reconstruirse desde:
|
||||
|
||||
- `taxes`
|
||||
- el cliente actual
|
||||
- defaults implícitos de frontend
|
||||
|
||||
## No incluido
|
||||
|
||||
- nueva tabla para configuracion fiscal
|
||||
- recalculo fiscal fuera del flujo normal de lineas
|
||||
- cambios de Verifacti
|
||||
- cobros o vencimientos vivos
|
||||
- rectificativas
|
||||
@ -151,6 +151,7 @@ Preparar el esquema fisico para separar:
|
||||
- create de proforma se consolida como creacion de cabecera/documento inicial
|
||||
- una proforma sin lineas es valida y debe persistirse con `items = []`
|
||||
- `tax_regime_code` queda como configuracion fiscal inicial de la cabecera
|
||||
- `tax_config` queda persistido en `proformas` como configuracion fiscal de cabecera
|
||||
- `proforma_taxes` solo representa impuestos calculados, no defaults fiscales ni placeholders sobre base `0`
|
||||
- si una proforma nace sin lineas, la response debe devolver `taxes = []` y totales monetarios a `0`
|
||||
- update sigue siendo el punto donde se anaden lineas y se materializan impuestos/totales reales
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
ALTER TABLE proformas
|
||||
ADD COLUMN IF NOT EXISTS tax_mode VARCHAR(20) NOT NULL DEFAULT 'single',
|
||||
ADD COLUMN IF NOT EXISTS default_iva_code VARCHAR(64) NULL,
|
||||
ADD COLUMN IF NOT EXISTS uses_equivalence_surcharge TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS default_rec_code VARCHAR(64) NULL,
|
||||
ADD COLUMN IF NOT EXISTS uses_retention TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS default_retention_code VARCHAR(64) NULL;
|
||||
|
||||
UPDATE proformas
|
||||
SET
|
||||
tax_mode = COALESCE(NULLIF(tax_mode, ''), 'single'),
|
||||
default_iva_code = COALESCE(default_iva_code, 'iva_21'),
|
||||
uses_equivalence_surcharge = COALESCE(uses_equivalence_surcharge, 0),
|
||||
uses_retention = COALESCE(uses_retention, 0)
|
||||
WHERE 1 = 1;
|
||||
28
docs/customer-invoices/sql/validate-proforma-tax-config.sql
Normal file
28
docs/customer-invoices/sql/validate-proforma-tax-config.sql
Normal file
@ -0,0 +1,28 @@
|
||||
SELECT
|
||||
p.id,
|
||||
p.proforma_reference,
|
||||
p.tax_mode,
|
||||
p.default_iva_code,
|
||||
p.uses_equivalence_surcharge,
|
||||
p.default_rec_code,
|
||||
p.uses_retention,
|
||||
p.default_retention_code
|
||||
FROM proformas p
|
||||
WHERE p.tax_mode NOT IN ('single', 'per_line')
|
||||
OR (p.uses_equivalence_surcharge = 1 AND p.default_iva_code IS NULL)
|
||||
OR (p.uses_equivalence_surcharge = 0 AND p.default_rec_code IS NOT NULL)
|
||||
OR (p.uses_retention = 0 AND p.default_retention_code IS NOT NULL);
|
||||
|
||||
SELECT
|
||||
p.id,
|
||||
p.proforma_reference,
|
||||
COUNT(DISTINCT pi.item_id) AS item_rows,
|
||||
COUNT(DISTINCT pt.tax_id) AS tax_rows
|
||||
FROM proformas p
|
||||
LEFT JOIN proforma_items pi
|
||||
ON pi.proforma_id = p.id
|
||||
LEFT JOIN proforma_taxes pt
|
||||
ON pt.proforma_id = p.id
|
||||
GROUP BY p.id, p.proforma_reference
|
||||
HAVING COUNT(DISTINCT pi.item_id) = 0
|
||||
AND COUNT(DISTINCT pt.tax_id) > 0;
|
||||
@ -1,3 +1,5 @@
|
||||
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type { ProformaPaymentResolver } from "../services";
|
||||
import {
|
||||
@ -16,6 +18,7 @@ export const buildProformaCreator = (params: {
|
||||
repository: IProformaRepository;
|
||||
proformaSeriesValidator: ProformaSeriesValidator;
|
||||
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||
customerServices: ICustomerPublicServices;
|
||||
}): IProformaCreator => {
|
||||
return new ProformaCreator(params);
|
||||
};
|
||||
|
||||
@ -20,11 +20,13 @@ import {
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemQuantity,
|
||||
type ProformaTaxMode,
|
||||
type ProformaRecipient,
|
||||
} from "../../../domain";
|
||||
import type {
|
||||
ProformaCreateInputProps,
|
||||
ProformaItemCreateInputProps,
|
||||
ProformaTaxConfigInputProps,
|
||||
ProformaItemTaxCodesInput,
|
||||
} from "../models";
|
||||
|
||||
@ -132,6 +134,8 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
errors
|
||||
);
|
||||
|
||||
const taxConfig = this.mapTaxConfig(dto.tax_config, errors);
|
||||
|
||||
const items = this.mapItemsProps(dto.items ?? [], {
|
||||
languageCode: languageCode!,
|
||||
currencyCode: currencyCode!,
|
||||
@ -164,6 +168,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
linkedInvoiceId: Maybe.none(),
|
||||
|
||||
taxRegimeCode: taxRegimeCode!,
|
||||
taxConfig,
|
||||
paymentMethodId: paymentMethodId!,
|
||||
|
||||
globalDiscountPercentage: globalDiscountPercentage!,
|
||||
@ -284,6 +289,77 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private mapTaxConfig(
|
||||
taxConfigDTO: CreateProformaRequestDTO["tax_config"],
|
||||
errors: ValidationErrorDetail[]
|
||||
): ProformaTaxConfigInputProps | undefined {
|
||||
if (!taxConfigDTO) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
taxMode: this.mapTaxMode(taxConfigDTO.tax_mode, "tax_config.tax_mode", errors),
|
||||
defaultIvaCode: this.mapOptionalString(
|
||||
taxConfigDTO.default_iva_code,
|
||||
"tax_config.default_iva_code",
|
||||
errors
|
||||
),
|
||||
usesEquivalenceSurcharge: taxConfigDTO.uses_equivalence_surcharge,
|
||||
defaultRecCode: this.mapOptionalString(
|
||||
taxConfigDTO.default_rec_code,
|
||||
"tax_config.default_rec_code",
|
||||
errors
|
||||
),
|
||||
usesRetention: taxConfigDTO.uses_retention,
|
||||
defaultRetentionCode: this.mapOptionalString(
|
||||
taxConfigDTO.default_retention_code,
|
||||
"tax_config.default_retention_code",
|
||||
errors
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private mapTaxMode(
|
||||
taxMode: "single" | "per_line" | undefined,
|
||||
path: string,
|
||||
errors: ValidationErrorDetail[]
|
||||
): ProformaTaxMode | undefined {
|
||||
if (taxMode === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (taxMode === "single") {
|
||||
return "single";
|
||||
}
|
||||
|
||||
if (taxMode === "per_line") {
|
||||
return "perLine";
|
||||
}
|
||||
|
||||
errors.push({
|
||||
path,
|
||||
message: "Invalid tax mode",
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private mapOptionalString(
|
||||
value: string | null | undefined,
|
||||
path: string,
|
||||
errors: ValidationErrorDetail[]
|
||||
): Maybe<string> | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return extractOrPushError(
|
||||
maybeFromNullableResult(value, (currentValue) => Result.ok(String(currentValue))),
|
||||
path,
|
||||
errors
|
||||
);
|
||||
}
|
||||
|
||||
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
||||
if (errors.length > 0) {
|
||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||
|
||||
@ -15,11 +15,18 @@ import {
|
||||
import { Maybe, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
|
||||
import { InvoiceSerie, ItemAmount, ItemDescription, ItemQuantity } from "../../../domain";
|
||||
import {
|
||||
InvoiceSerie,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemQuantity,
|
||||
type ProformaTaxMode,
|
||||
} from "../../../domain";
|
||||
import type {
|
||||
ProformaItemPatchInputProps,
|
||||
ProformaItemTaxCodesInput,
|
||||
ProformaPatchInputProps,
|
||||
ProformaTaxConfigPatchInputProps,
|
||||
} from "../models";
|
||||
|
||||
export interface IUpdateProformaInputMapper {
|
||||
@ -183,6 +190,10 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
if (dto.tax_config !== undefined) {
|
||||
proformaPatchProps.taxConfig = this.mapTaxConfigPatch(dto.tax_config, errors);
|
||||
}
|
||||
|
||||
if (dto.items !== undefined) {
|
||||
proformaPatchProps.items = this.mapItemsProps(dto.items, { errors });
|
||||
}
|
||||
@ -257,6 +268,73 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
||||
return Maybe.some(String(code).trim());
|
||||
}
|
||||
|
||||
private mapTaxConfigPatch(
|
||||
taxConfigDTO: NonNullable<UpdateProformaByIdRequestDTO["tax_config"]>,
|
||||
errors: ValidationErrorDetail[]
|
||||
): ProformaTaxConfigPatchInputProps {
|
||||
return {
|
||||
taxMode: this.mapTaxMode(taxConfigDTO.tax_mode, "tax_config.tax_mode", errors),
|
||||
defaultIvaCode: this.mapOptionalString(
|
||||
taxConfigDTO.default_iva_code,
|
||||
"tax_config.default_iva_code",
|
||||
errors
|
||||
),
|
||||
usesEquivalenceSurcharge: taxConfigDTO.uses_equivalence_surcharge,
|
||||
defaultRecCode: this.mapOptionalString(
|
||||
taxConfigDTO.default_rec_code,
|
||||
"tax_config.default_rec_code",
|
||||
errors
|
||||
),
|
||||
usesRetention: taxConfigDTO.uses_retention,
|
||||
defaultRetentionCode: this.mapOptionalString(
|
||||
taxConfigDTO.default_retention_code,
|
||||
"tax_config.default_retention_code",
|
||||
errors
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private mapTaxMode(
|
||||
taxMode: "single" | "per_line" | undefined,
|
||||
path: string,
|
||||
errors: ValidationErrorDetail[]
|
||||
): ProformaTaxMode | undefined {
|
||||
if (taxMode === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (taxMode === "single") {
|
||||
return "single";
|
||||
}
|
||||
|
||||
if (taxMode === "per_line") {
|
||||
return "perLine";
|
||||
}
|
||||
|
||||
errors.push({
|
||||
path,
|
||||
message: "Invalid tax mode",
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private mapOptionalString(
|
||||
value: string | null | undefined,
|
||||
path: string,
|
||||
errors: ValidationErrorDetail[]
|
||||
): Maybe<string> | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return extractOrPushError(
|
||||
maybeFromNullableResult(value, (currentValue) => Result.ok(String(currentValue))),
|
||||
path,
|
||||
errors
|
||||
);
|
||||
}
|
||||
|
||||
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
||||
if (errors.length > 0) {
|
||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IProformaCreateProps, IProformaItemCreateProps } from "../../../domain";
|
||||
import type { IProformaCreateProps, IProformaItemCreateProps, ProformaTaxMode } from "../../../domain";
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
import type { InvoiceSerie } from "../../../domain";
|
||||
|
||||
@ -14,12 +14,22 @@ import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.
|
||||
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
||||
*/
|
||||
|
||||
export interface ProformaTaxConfigInputProps {
|
||||
taxMode?: ProformaTaxMode;
|
||||
defaultIvaCode?: Maybe<string>;
|
||||
usesEquivalenceSurcharge?: boolean;
|
||||
defaultRecCode?: Maybe<string>;
|
||||
usesRetention?: boolean;
|
||||
defaultRetentionCode?: Maybe<string>;
|
||||
}
|
||||
|
||||
export type ProformaCreateInputProps = Omit<
|
||||
IProformaCreateProps,
|
||||
"items" | "proformaReference" | "proformaNumber" | "documentSeriesId" | "series"
|
||||
"items" | "proformaReference" | "proformaNumber" | "documentSeriesId" | "series" | "taxConfig"
|
||||
> & {
|
||||
proformaSeriesCode: Maybe<InvoiceSerie>;
|
||||
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||
taxConfig?: ProformaTaxConfigInputProps;
|
||||
items: ProformaItemCreateInputProps[];
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import type { ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
|
||||
import type {
|
||||
ProformaItemPatchProps,
|
||||
ProformaPatchProps,
|
||||
ProformaTaxMode,
|
||||
} from "../../../domain";
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
import type { InvoiceSerie } from "../../../domain";
|
||||
|
||||
@ -14,8 +18,18 @@ import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.
|
||||
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
||||
*/
|
||||
|
||||
export interface ProformaTaxConfigPatchInputProps {
|
||||
taxMode?: ProformaTaxMode;
|
||||
defaultIvaCode?: Maybe<string>;
|
||||
usesEquivalenceSurcharge?: boolean;
|
||||
defaultRecCode?: Maybe<string>;
|
||||
usesRetention?: boolean;
|
||||
defaultRetentionCode?: Maybe<string>;
|
||||
}
|
||||
|
||||
export type ProformaPatchInputProps = Omit<ProformaPatchProps, "items" | "series"> & {
|
||||
targetInvoiceSeriesCode?: Maybe<InvoiceSerie>;
|
||||
taxConfig?: ProformaTaxConfigPatchInputProps;
|
||||
items?: ProformaItemPatchInputProps[];
|
||||
};
|
||||
|
||||
|
||||
@ -22,6 +22,12 @@ export interface EnsureProformaItemTaxesParams {
|
||||
taxCodes: ProformaItemTaxCodesInput;
|
||||
}
|
||||
|
||||
export interface FindActiveProformaTaxDefinitionByCodeParams {
|
||||
companyId: UniqueID;
|
||||
atDate: UtcDate;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resuelve datos fiscales externos al agregado de proforma usando servicios públicos de `catalogs`.
|
||||
*
|
||||
@ -115,6 +121,16 @@ export class ProformaTaxResolver {
|
||||
});
|
||||
}
|
||||
|
||||
public async findActiveTaxDefinitionByCode(
|
||||
params: FindActiveProformaTaxDefinitionByCodeParams
|
||||
): Promise<Result<Maybe<TaxDefinitionPublicModel>, Error>> {
|
||||
return this.deps.taxDefinitionFinder.findActiveByCode({
|
||||
companyId: params.companyId,
|
||||
code: params.code,
|
||||
atDate: params.atDate,
|
||||
});
|
||||
}
|
||||
|
||||
private collectCodes(taxCodes: ProformaItemTaxCodesInput): Collection<string> {
|
||||
const codes = [taxCodes.ivaCode, taxCodes.recCode, taxCodes.retentionCode]
|
||||
.filter((code) => code.isSome())
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
@ -6,8 +7,10 @@ import {
|
||||
type IProformaItemCreateProps,
|
||||
InvoiceNumber,
|
||||
Proforma,
|
||||
ProformaTaxConfig,
|
||||
type ProformaTaxMode,
|
||||
} from "../../../domain";
|
||||
import type { ProformaCreateInputProps } from "../models";
|
||||
import type { ProformaCreateInputProps, ProformaTaxConfigInputProps } from "../models";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
||||
@ -36,13 +39,14 @@ export class ProformaCreator implements IProformaCreator {
|
||||
paymentResolver: ProformaPaymentResolver;
|
||||
proformaSeriesValidator: ProformaSeriesValidator;
|
||||
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||
customerServices: ICustomerPublicServices;
|
||||
}
|
||||
) {}
|
||||
|
||||
async create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
const resolvedProps = await this.resolveCreateProps(props);
|
||||
const resolvedProps = await this.resolveCreateProps(props, transaction);
|
||||
|
||||
if (resolvedProps.isFailure) {
|
||||
return Result.fail(resolvedProps.error);
|
||||
@ -132,14 +136,30 @@ export class ProformaCreator implements IProformaCreator {
|
||||
}
|
||||
|
||||
private async resolveCreateProps(
|
||||
props: ProformaCreateInputProps
|
||||
props: ProformaCreateInputProps,
|
||||
transaction?: unknown
|
||||
): Promise<
|
||||
Result<Omit<IProformaCreateProps, "proformaReference" | "proformaNumber" | "documentSeriesId">, Error>
|
||||
> {
|
||||
// TODO: Esto hay que cambiarlo en el futuro para buscar valores por defecto, desde la empresa o desde el cliente
|
||||
const customerResult = await this.deps.customerServices.getCustomerById(props.customerId, {
|
||||
companyId: props.companyId,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (customerResult.isFailure) {
|
||||
return Result.fail(customerResult.error);
|
||||
}
|
||||
|
||||
const customer = customerResult.data;
|
||||
|
||||
const _newProps = {
|
||||
...props,
|
||||
taxRegimeCode: props.taxRegimeCode.isNone() ? Maybe.some("01") : props.taxRegimeCode,
|
||||
taxRegimeCode:
|
||||
props.taxRegimeCode.isNone()
|
||||
? customer.taxRegimeCode.isSome()
|
||||
? customer.taxRegimeCode
|
||||
: Maybe.some("01")
|
||||
: props.taxRegimeCode,
|
||||
};
|
||||
|
||||
// Tax Regime => comprobar que existe
|
||||
@ -162,6 +182,17 @@ export class ProformaCreator implements IProformaCreator {
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
const taxConfigResult = await this.resolveTaxConfig({
|
||||
companyId: _newProps.companyId,
|
||||
atDate: _newProps.proformaDate,
|
||||
customer,
|
||||
override: props.taxConfig,
|
||||
});
|
||||
|
||||
if (taxConfigResult.isFailure) {
|
||||
return Result.fail(taxConfigResult.error);
|
||||
}
|
||||
|
||||
// En create una proforma puede nacer sin líneas: en ese caso solo persistimos
|
||||
// cabecera + defaults iniciales. Los impuestos y totales materializados salen
|
||||
// de `items`, así que con `items = []` no se generan filas fiscales.
|
||||
@ -186,7 +217,177 @@ export class ProformaCreator implements IProformaCreator {
|
||||
|
||||
return Result.ok({
|
||||
..._newProps,
|
||||
taxConfig: taxConfigResult.data,
|
||||
items: resolvedItems,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveTaxConfig(params: {
|
||||
companyId: UniqueID;
|
||||
atDate: IProformaCreateProps["proformaDate"];
|
||||
customer: Awaited<ReturnType<ICustomerPublicServices["getCustomerById"]>> extends Result<
|
||||
infer T,
|
||||
Error
|
||||
>
|
||||
? T
|
||||
: never;
|
||||
override?: ProformaTaxConfigInputProps;
|
||||
}): Promise<Result<ProformaTaxConfig, Error>> {
|
||||
const taxMode = params.override?.taxMode ?? "single";
|
||||
const defaultIvaCode =
|
||||
params.override?.defaultIvaCode ??
|
||||
Maybe.some(this.resolveDefaultIvaCodeFromCustomer(params.customer) ?? "iva_21");
|
||||
|
||||
const usesEquivalenceSurcharge =
|
||||
params.override?.usesEquivalenceSurcharge ?? params.customer.usesEquivalenceSurcharge;
|
||||
const usesRetention = params.override?.usesRetention ?? params.customer.usesRetention;
|
||||
|
||||
const defaultRecCode = await this.resolveDefaultRecCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.atDate,
|
||||
defaultIvaCode,
|
||||
usesEquivalenceSurcharge,
|
||||
requestedCode: params.override?.defaultRecCode,
|
||||
});
|
||||
|
||||
if (defaultRecCode.isFailure) {
|
||||
return Result.fail(defaultRecCode.error);
|
||||
}
|
||||
|
||||
const defaultRetentionCode = await this.resolveRetentionCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.atDate,
|
||||
usesRetention,
|
||||
requestedCode: params.override?.defaultRetentionCode,
|
||||
});
|
||||
|
||||
if (defaultRetentionCode.isFailure) {
|
||||
return Result.fail(defaultRetentionCode.error);
|
||||
}
|
||||
|
||||
const createdTaxConfig = ProformaTaxConfig.create({
|
||||
taxMode,
|
||||
defaultIvaCode,
|
||||
usesEquivalenceSurcharge,
|
||||
defaultRecCode: defaultRecCode.data,
|
||||
usesRetention,
|
||||
defaultRetentionCode: defaultRetentionCode.data,
|
||||
});
|
||||
|
||||
if (createdTaxConfig.isFailure) {
|
||||
return Result.fail(createdTaxConfig.error);
|
||||
}
|
||||
|
||||
return Result.ok(createdTaxConfig.data);
|
||||
}
|
||||
|
||||
private resolveDefaultIvaCodeFromCustomer(
|
||||
_customer: Awaited<ReturnType<ICustomerPublicServices["getCustomerById"]>> extends Result<
|
||||
infer T,
|
||||
Error
|
||||
>
|
||||
? T
|
||||
: never
|
||||
): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async resolveDefaultRecCode(params: {
|
||||
companyId: UniqueID;
|
||||
atDate: IProformaCreateProps["proformaDate"];
|
||||
defaultIvaCode: Maybe<string>;
|
||||
usesEquivalenceSurcharge: boolean;
|
||||
requestedCode?: Maybe<string>;
|
||||
}): Promise<Result<Maybe<string>, Error>> {
|
||||
if (!params.usesEquivalenceSurcharge) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const ivaCode = this.maybeValue(params.defaultIvaCode);
|
||||
|
||||
if (!ivaCode) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const ivaDefinition = await this.deps.taxResolver.findActiveTaxDefinitionByCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.atDate,
|
||||
code: ivaCode,
|
||||
});
|
||||
|
||||
if (ivaDefinition.isFailure) {
|
||||
return Result.fail(ivaDefinition.error);
|
||||
}
|
||||
|
||||
if (ivaDefinition.data.isNone()) {
|
||||
return Result.fail(new Error(`Tax definition not found: ${ivaCode}`));
|
||||
}
|
||||
|
||||
const allowedSurchargeCodes = ivaDefinition.data.unwrap().allowedSurchargeCodes.getAll();
|
||||
const requestedCode = this.maybeValue(params.requestedCode) ?? this.defaultRecForIva(ivaCode);
|
||||
|
||||
if (requestedCode && allowedSurchargeCodes.includes(requestedCode)) {
|
||||
return Result.ok(Maybe.some(requestedCode));
|
||||
}
|
||||
|
||||
const fallbackCode = allowedSurchargeCodes[0] ?? null;
|
||||
|
||||
return Result.ok(fallbackCode ? Maybe.some(fallbackCode) : Maybe.none());
|
||||
}
|
||||
|
||||
private async resolveRetentionCode(params: {
|
||||
companyId: UniqueID;
|
||||
atDate: IProformaCreateProps["proformaDate"];
|
||||
usesRetention: boolean;
|
||||
requestedCode?: Maybe<string>;
|
||||
}): Promise<Result<Maybe<string>, Error>> {
|
||||
if (!params.usesRetention) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const retentionCode = this.maybeValue(params.requestedCode) ?? "retention_15";
|
||||
|
||||
const definition = await this.deps.taxResolver.findActiveTaxDefinitionByCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.atDate,
|
||||
code: retentionCode,
|
||||
});
|
||||
|
||||
if (definition.isFailure) {
|
||||
return Result.fail(definition.error);
|
||||
}
|
||||
|
||||
if (definition.data.isNone()) {
|
||||
return Result.fail(new Error(`Tax definition not found: ${retentionCode}`));
|
||||
}
|
||||
|
||||
return Result.ok(Maybe.some(retentionCode));
|
||||
}
|
||||
|
||||
private defaultRecForIva(ivaCode: string): string | null {
|
||||
const defaults: Record<string, string> = {
|
||||
iva_21: "rec_5_2",
|
||||
iva_10: "rec_1_4",
|
||||
iva_4: "rec_0_5",
|
||||
iva_0: "rec_0",
|
||||
};
|
||||
|
||||
return defaults[ivaCode] ?? null;
|
||||
}
|
||||
|
||||
private maybeValue(value: Maybe<string> | string | null | undefined): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && "isSome" in value && typeof value.isSome === "function") {
|
||||
return value.isSome() ? value.unwrap() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { DomainValidationError, type UniqueID, type UtcDate } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Proforma, ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
|
||||
import {
|
||||
ProformaTaxConfig,
|
||||
type Proforma,
|
||||
type ProformaItemPatchProps,
|
||||
type ProformaPatchProps,
|
||||
} from "../../../domain";
|
||||
import type { ProformaPatchInputProps } from "../models";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
@ -72,6 +77,7 @@ export class ProformaUpdater implements IProformaUpdater {
|
||||
const resolvedPatch = await this.resolvePatchProps({
|
||||
companyId,
|
||||
currentInvoiceDate: proforma.invoiceDate,
|
||||
currentTaxConfig: proforma.taxConfig,
|
||||
patch: patchProps,
|
||||
transaction,
|
||||
});
|
||||
@ -107,10 +113,11 @@ export class ProformaUpdater implements IProformaUpdater {
|
||||
private async resolvePatchProps(params: {
|
||||
companyId: UniqueID;
|
||||
currentInvoiceDate: UtcDate;
|
||||
currentTaxConfig: Proforma["taxConfig"];
|
||||
patch: ProformaPatchInputProps;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<ProformaPatchProps, Error>> {
|
||||
const { patch, companyId, currentInvoiceDate, transaction } = params;
|
||||
const { patch, companyId, currentInvoiceDate, transaction, currentTaxConfig } = params;
|
||||
|
||||
if (patch.taxRegimeCode !== undefined) {
|
||||
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
||||
@ -134,6 +141,17 @@ export class ProformaUpdater implements IProformaUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
const taxConfigResult = await this.resolveTaxConfig({
|
||||
companyId,
|
||||
currentInvoiceDate,
|
||||
currentTaxConfig,
|
||||
patchTaxConfig: patch.taxConfig,
|
||||
});
|
||||
|
||||
if (taxConfigResult.isFailure) {
|
||||
return Result.fail(taxConfigResult.error);
|
||||
}
|
||||
|
||||
if (patch.targetInvoiceSeriesCode !== undefined) {
|
||||
const targetInvoiceSeriesValidationResult =
|
||||
await this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
|
||||
@ -148,10 +166,16 @@ export class ProformaUpdater implements IProformaUpdater {
|
||||
}
|
||||
|
||||
if (patch.items === undefined) {
|
||||
return Result.ok({
|
||||
const resolvedPatch: ProformaPatchProps = {
|
||||
...patch,
|
||||
series: patch.targetInvoiceSeriesCode,
|
||||
} as ProformaPatchProps);
|
||||
};
|
||||
|
||||
if (taxConfigResult.data !== undefined) {
|
||||
resolvedPatch.taxConfig = taxConfigResult.data;
|
||||
}
|
||||
|
||||
return Result.ok(resolvedPatch);
|
||||
}
|
||||
|
||||
const effectiveInvoiceDate = patch.proformaDate ?? currentInvoiceDate;
|
||||
@ -174,11 +198,175 @@ export class ProformaUpdater implements IProformaUpdater {
|
||||
});
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
const resolvedPatch: ProformaPatchProps = {
|
||||
...patch,
|
||||
series: patch.targetInvoiceSeriesCode,
|
||||
items: resolvedItems,
|
||||
};
|
||||
|
||||
if (taxConfigResult.data !== undefined) {
|
||||
resolvedPatch.taxConfig = taxConfigResult.data;
|
||||
}
|
||||
|
||||
return Result.ok(resolvedPatch);
|
||||
}
|
||||
|
||||
private async resolveTaxConfig(params: {
|
||||
companyId: UniqueID;
|
||||
currentInvoiceDate: UtcDate;
|
||||
currentTaxConfig: Proforma["taxConfig"];
|
||||
patchTaxConfig: ProformaPatchInputProps["taxConfig"];
|
||||
}): Promise<Result<ProformaTaxConfig | undefined, Error>> {
|
||||
if (params.patchTaxConfig === undefined) {
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
const currentTaxConfig = params.currentTaxConfig;
|
||||
const patchTaxConfig = params.patchTaxConfig;
|
||||
|
||||
const taxMode = patchTaxConfig.taxMode ?? currentTaxConfig.taxMode;
|
||||
const defaultIvaCode = patchTaxConfig.defaultIvaCode ?? currentTaxConfig.defaultIvaCode;
|
||||
const usesEquivalenceSurcharge =
|
||||
patchTaxConfig.usesEquivalenceSurcharge ?? currentTaxConfig.usesEquivalenceSurcharge;
|
||||
const usesRetention = patchTaxConfig.usesRetention ?? currentTaxConfig.usesRetention;
|
||||
|
||||
const defaultRecCode = await this.resolveDefaultRecCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.currentInvoiceDate,
|
||||
defaultIvaCode,
|
||||
usesEquivalenceSurcharge,
|
||||
requestedCode: patchTaxConfig.defaultRecCode ?? currentTaxConfig.defaultRecCode,
|
||||
});
|
||||
|
||||
if (defaultRecCode.isFailure) {
|
||||
return Result.fail(defaultRecCode.error);
|
||||
}
|
||||
|
||||
const defaultRetentionCode = await this.resolveRetentionCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.currentInvoiceDate,
|
||||
usesRetention,
|
||||
requestedCode:
|
||||
patchTaxConfig.defaultRetentionCode ?? currentTaxConfig.defaultRetentionCode,
|
||||
});
|
||||
|
||||
if (defaultRetentionCode.isFailure) {
|
||||
return Result.fail(defaultRetentionCode.error);
|
||||
}
|
||||
|
||||
const taxConfigResult = ProformaTaxConfig.create({
|
||||
taxMode,
|
||||
defaultIvaCode,
|
||||
usesEquivalenceSurcharge,
|
||||
defaultRecCode: defaultRecCode.data,
|
||||
usesRetention,
|
||||
defaultRetentionCode: defaultRetentionCode.data,
|
||||
});
|
||||
|
||||
if (taxConfigResult.isFailure) {
|
||||
return Result.fail(taxConfigResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(taxConfigResult.data);
|
||||
}
|
||||
|
||||
private async resolveDefaultRecCode(params: {
|
||||
companyId: UniqueID;
|
||||
atDate: UtcDate;
|
||||
defaultIvaCode: Maybe<string>;
|
||||
usesEquivalenceSurcharge: boolean;
|
||||
requestedCode?: Maybe<string>;
|
||||
}): Promise<Result<Maybe<string>, Error>> {
|
||||
if (!params.usesEquivalenceSurcharge) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const ivaCode = this.maybeValue(params.defaultIvaCode);
|
||||
|
||||
if (!ivaCode) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const ivaDefinition = await this.deps.taxResolver.findActiveTaxDefinitionByCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.atDate,
|
||||
code: ivaCode,
|
||||
});
|
||||
|
||||
if (ivaDefinition.isFailure) {
|
||||
return Result.fail(ivaDefinition.error);
|
||||
}
|
||||
|
||||
if (ivaDefinition.data.isNone()) {
|
||||
return Result.fail(new Error(`Tax definition not found: ${ivaCode}`));
|
||||
}
|
||||
|
||||
const allowedSurchargeCodes = ivaDefinition.data.unwrap().allowedSurchargeCodes.getAll();
|
||||
const requestedCode = this.maybeValue(params.requestedCode) ?? this.defaultRecForIva(ivaCode);
|
||||
|
||||
if (requestedCode && allowedSurchargeCodes.includes(requestedCode)) {
|
||||
return Result.ok(Maybe.some(requestedCode));
|
||||
}
|
||||
|
||||
const fallbackCode = allowedSurchargeCodes[0] ?? null;
|
||||
|
||||
return Result.ok(fallbackCode ? Maybe.some(fallbackCode) : Maybe.none());
|
||||
}
|
||||
|
||||
private async resolveRetentionCode(params: {
|
||||
companyId: UniqueID;
|
||||
atDate: UtcDate;
|
||||
usesRetention: boolean;
|
||||
requestedCode?: Maybe<string>;
|
||||
}): Promise<Result<Maybe<string>, Error>> {
|
||||
if (!params.usesRetention) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const retentionCode = this.maybeValue(params.requestedCode) ?? "retention_15";
|
||||
|
||||
const definition = await this.deps.taxResolver.findActiveTaxDefinitionByCode({
|
||||
companyId: params.companyId,
|
||||
atDate: params.atDate,
|
||||
code: retentionCode,
|
||||
});
|
||||
|
||||
if (definition.isFailure) {
|
||||
return Result.fail(definition.error);
|
||||
}
|
||||
|
||||
if (definition.data.isNone()) {
|
||||
return Result.fail(new Error(`Tax definition not found: ${retentionCode}`));
|
||||
}
|
||||
|
||||
return Result.ok(Maybe.some(retentionCode));
|
||||
}
|
||||
|
||||
private defaultRecForIva(ivaCode: string): string | null {
|
||||
const defaults: Record<string, string> = {
|
||||
iva_21: "rec_5_2",
|
||||
iva_10: "rec_1_4",
|
||||
iva_4: "rec_0_5",
|
||||
iva_0: "rec_0",
|
||||
};
|
||||
|
||||
return defaults[ivaCode] ?? null;
|
||||
}
|
||||
|
||||
private maybeValue(value: Maybe<string> | string | null | undefined): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && "isSome" in value && typeof value.isSome === "function") {
|
||||
return value.isSome() ? value.unwrap() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isApprovedPatchAllowed(patch: ProformaPatchInputProps): boolean {
|
||||
|
||||
@ -69,6 +69,17 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
||||
tax_regime: maybeToNullable(taxRegime, (taxRegime) => taxRegime),
|
||||
|
||||
payment_term: null,
|
||||
tax_config: {
|
||||
tax_mode: proforma.taxConfig.taxMode === "perLine" ? "per_line" : "single",
|
||||
default_iva_code: maybeToNullable(proforma.taxConfig.defaultIvaCode, (value) => value),
|
||||
uses_equivalence_surcharge: proforma.taxConfig.usesEquivalenceSurcharge,
|
||||
default_rec_code: maybeToNullable(proforma.taxConfig.defaultRecCode, (value) => value),
|
||||
uses_retention: proforma.taxConfig.usesRetention,
|
||||
default_retention_code: maybeToNullable(
|
||||
proforma.taxConfig.defaultRetentionCode,
|
||||
(value) => value
|
||||
),
|
||||
},
|
||||
|
||||
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
||||
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
||||
|
||||
@ -29,7 +29,7 @@ import {
|
||||
import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors";
|
||||
import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
|
||||
import { canManuallyTransitionProformaStatus } from "../services";
|
||||
import { ProformaItemTaxes, type ProformaRecipient } from "../value-objects";
|
||||
import { ProformaItemTaxes, ProformaTaxConfig, type ProformaRecipient } from "../value-objects";
|
||||
|
||||
export interface IProformaCreateProps {
|
||||
companyId: UniqueID;
|
||||
@ -57,6 +57,7 @@ export interface IProformaCreateProps {
|
||||
|
||||
paymentMethodId: Maybe<UniqueID>;
|
||||
taxRegimeCode: Maybe<string>;
|
||||
taxConfig: ProformaTaxConfig;
|
||||
|
||||
items: IProformaItemCreateProps[];
|
||||
globalDiscountPercentage: DiscountPercentage;
|
||||
@ -107,6 +108,7 @@ export interface IProforma {
|
||||
|
||||
paymentMethodId: Maybe<UniqueID>;
|
||||
taxRegimeCode: Maybe<string>;
|
||||
taxConfig: ProformaTaxConfig;
|
||||
|
||||
linkedInvoiceId: Maybe<UniqueID>;
|
||||
|
||||
@ -169,10 +171,13 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
// Mutabilidad
|
||||
public update(patchProps: ProformaPatchProps): Result<Proforma, Error> {
|
||||
const { items, ...otherProps } = patchProps;
|
||||
const definedProps = Object.fromEntries(
|
||||
Object.entries(otherProps).filter(([, value]) => value !== undefined)
|
||||
) as Partial<ProformaInternalProps>;
|
||||
|
||||
const candidateProps: ProformaInternalProps = {
|
||||
...this.props,
|
||||
...otherProps,
|
||||
...definedProps,
|
||||
};
|
||||
|
||||
// Validacciones
|
||||
@ -281,6 +286,10 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
return this.props.taxRegimeCode;
|
||||
}
|
||||
|
||||
public get taxConfig(): ProformaTaxConfig {
|
||||
return this.props.taxConfig;
|
||||
}
|
||||
|
||||
public get linkedInvoiceId(): Maybe<UniqueID> {
|
||||
return this.props.linkedInvoiceId;
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./proforma-item-taxes.vo";
|
||||
export * from "./proforma-recipient.vo";
|
||||
export * from "./proforma-tax-config.vo";
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import { DomainValidationError } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
export type ProformaTaxMode = "single" | "perLine";
|
||||
|
||||
export interface ProformaTaxConfigProps {
|
||||
taxMode: ProformaTaxMode;
|
||||
defaultIvaCode: Maybe<string>;
|
||||
usesEquivalenceSurcharge: boolean;
|
||||
defaultRecCode: Maybe<string>;
|
||||
usesRetention: boolean;
|
||||
defaultRetentionCode: Maybe<string>;
|
||||
}
|
||||
|
||||
export class ProformaTaxConfig {
|
||||
private constructor(private readonly props: ProformaTaxConfigProps) {}
|
||||
|
||||
public static create(props: ProformaTaxConfigProps): Result<ProformaTaxConfig, Error> {
|
||||
if (props.taxMode !== "single" && props.taxMode !== "perLine") {
|
||||
return Result.fail(
|
||||
new DomainValidationError("INVALID_TAX_MODE", "taxConfig.taxMode", "Invalid tax mode")
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(
|
||||
new ProformaTaxConfig({
|
||||
taxMode: props.taxMode,
|
||||
defaultIvaCode: props.defaultIvaCode,
|
||||
usesEquivalenceSurcharge: props.usesEquivalenceSurcharge,
|
||||
defaultRecCode: props.usesEquivalenceSurcharge ? props.defaultRecCode : Maybe.none(),
|
||||
usesRetention: props.usesRetention,
|
||||
defaultRetentionCode: props.usesRetention ? props.defaultRetentionCode : Maybe.none(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public get taxMode(): ProformaTaxMode {
|
||||
return this.props.taxMode;
|
||||
}
|
||||
|
||||
public get defaultIvaCode(): Maybe<string> {
|
||||
return this.props.defaultIvaCode;
|
||||
}
|
||||
|
||||
public get usesEquivalenceSurcharge(): boolean {
|
||||
return this.props.usesEquivalenceSurcharge;
|
||||
}
|
||||
|
||||
public get defaultRecCode(): Maybe<string> {
|
||||
return this.props.defaultRecCode;
|
||||
}
|
||||
|
||||
public get usesRetention(): boolean {
|
||||
return this.props.usesRetention;
|
||||
}
|
||||
|
||||
public get defaultRetentionCode(): Maybe<string> {
|
||||
return this.props.defaultRetentionCode;
|
||||
}
|
||||
|
||||
public getProps(): ProformaTaxConfigProps {
|
||||
return this.props;
|
||||
}
|
||||
}
|
||||
@ -53,6 +53,12 @@ export class ProformaModel extends Model<
|
||||
declare payment_term_id: CreationOptional<string | null>;
|
||||
|
||||
declare tax_regime_code: CreationOptional<string | null>;
|
||||
declare tax_mode: CreationOptional<string>;
|
||||
declare default_iva_code: CreationOptional<string | null>;
|
||||
declare uses_equivalence_surcharge: CreationOptional<boolean>;
|
||||
declare default_rec_code: CreationOptional<string | null>;
|
||||
declare uses_retention: CreationOptional<boolean>;
|
||||
declare default_retention_code: CreationOptional<string | null>;
|
||||
|
||||
declare subtotal_amount_value: number;
|
||||
declare subtotal_amount_scale: number;
|
||||
@ -249,6 +255,36 @@ export default (database: Sequelize) => {
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
tax_mode: {
|
||||
type: DataTypes.STRING(16),
|
||||
allowNull: false,
|
||||
defaultValue: "single",
|
||||
},
|
||||
default_iva_code: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
uses_equivalence_surcharge: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
default_rec_code: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
uses_retention: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
default_retention_code: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
subtotal_amount_value: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: false,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { SetupParams } from "@erp/core/api";
|
||||
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
@ -51,6 +52,7 @@ export function buildProformaPublicServices(
|
||||
const { database } = params;
|
||||
|
||||
const catalogs = resolveProformaCatalogsDeps(params);
|
||||
const customerServices = params.getService<ICustomerPublicServices>("customers:general");
|
||||
const documentSeriesServices =
|
||||
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||
|
||||
@ -92,6 +94,7 @@ export function buildProformaPublicServices(
|
||||
paymentResolver: catalogResolvers.paymentResolver,
|
||||
proformaSeriesValidator,
|
||||
targetInvoiceSeriesValidator,
|
||||
customerServices,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||
|
||||
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
||||
@ -65,6 +66,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
|
||||
const catalogs = resolveProformaCatalogsDeps(params);
|
||||
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
||||
const customerServices = params.getService<ICustomerPublicServices>("customers:general");
|
||||
const documentSeriesServices =
|
||||
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||
|
||||
@ -109,6 +111,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
repository,
|
||||
proformaSeriesValidator,
|
||||
targetInvoiceSeriesValidator,
|
||||
customerServices,
|
||||
});
|
||||
|
||||
const updater = buildProformaUpdater({
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
Proforma,
|
||||
ProformaTaxConfig,
|
||||
type ProformaInternalProps,
|
||||
ProformaItems,
|
||||
} from "../../../../../../domain";
|
||||
@ -177,6 +178,25 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
errors
|
||||
);
|
||||
|
||||
const taxConfig = extractOrPushError(
|
||||
ProformaTaxConfig.create({
|
||||
taxMode: raw.tax_mode === "per_line" ? "perLine" : "single",
|
||||
defaultIvaCode: maybeFromNullableResult(raw.default_iva_code, (value) =>
|
||||
Result.ok(String(value))
|
||||
),
|
||||
usesEquivalenceSurcharge: Boolean(raw.uses_equivalence_surcharge),
|
||||
defaultRecCode: maybeFromNullableResult(raw.default_rec_code, (value) =>
|
||||
Result.ok(String(value))
|
||||
),
|
||||
usesRetention: Boolean(raw.uses_retention),
|
||||
defaultRetentionCode: maybeFromNullableResult(raw.default_retention_code, (value) =>
|
||||
Result.ok(String(value))
|
||||
),
|
||||
}),
|
||||
"tax_config",
|
||||
errors
|
||||
);
|
||||
|
||||
return {
|
||||
invoiceId,
|
||||
companyId,
|
||||
@ -193,6 +213,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
currencyCode,
|
||||
paymentMethodId,
|
||||
taxRegimeCode,
|
||||
taxConfig,
|
||||
|
||||
globalDiscountPercentage,
|
||||
linkedInvoiceId,
|
||||
@ -265,6 +286,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
paymentMethodId: attributes.paymentMethodId!,
|
||||
taxRegimeCode: attributes.taxRegimeCode!,
|
||||
taxConfig: attributes.taxConfig!,
|
||||
|
||||
linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia.
|
||||
};
|
||||
@ -357,6 +379,12 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
|
||||
tax_regime_description: null,
|
||||
tax_mode: source.taxConfig.taxMode === "perLine" ? "per_line" : "single",
|
||||
default_iva_code: maybeToNullable(source.taxConfig.defaultIvaCode, (value) => value),
|
||||
uses_equivalence_surcharge: source.taxConfig.usesEquivalenceSurcharge,
|
||||
default_rec_code: maybeToNullable(source.taxConfig.defaultRecCode, (value) => value),
|
||||
uses_retention: source.taxConfig.usesRetention,
|
||||
default_retention_code: maybeToNullable(source.taxConfig.defaultRetentionCode, (value) => value),
|
||||
|
||||
subtotal_amount_value: allAmounts.subtotalAmount.value,
|
||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
Proforma,
|
||||
ProformaTaxConfig,
|
||||
type ProformaInternalProps,
|
||||
ProformaItems,
|
||||
} from "../../../../../../domain";
|
||||
@ -117,6 +118,30 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
||||
"tax_regime_code",
|
||||
errors
|
||||
);
|
||||
const taxConfig = extractOrPushError(
|
||||
ProformaTaxConfig.create({
|
||||
taxMode: raw.tax_mode === "per_line" ? "perLine" : "single",
|
||||
defaultIvaCode: extractOrPushError(
|
||||
maybeFromNullableResult(raw.default_iva_code, (value) => Result.ok(String(value))),
|
||||
"default_iva_code",
|
||||
errors
|
||||
)!,
|
||||
usesEquivalenceSurcharge: Boolean(raw.uses_equivalence_surcharge),
|
||||
defaultRecCode: extractOrPushError(
|
||||
maybeFromNullableResult(raw.default_rec_code, (value) => Result.ok(String(value))),
|
||||
"default_rec_code",
|
||||
errors
|
||||
)!,
|
||||
usesRetention: Boolean(raw.uses_retention),
|
||||
defaultRetentionCode: extractOrPushError(
|
||||
maybeFromNullableResult(raw.default_retention_code, (value) => Result.ok(String(value))),
|
||||
"default_retention_code",
|
||||
errors
|
||||
)!,
|
||||
}),
|
||||
"tax_config",
|
||||
errors
|
||||
);
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
DiscountPercentage.create({
|
||||
value: Number(raw.global_discount_percentage_value ?? 0),
|
||||
@ -148,6 +173,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
||||
currencyCode,
|
||||
paymentMethodId,
|
||||
taxRegimeCode,
|
||||
taxConfig,
|
||||
globalDiscountPercentage,
|
||||
linkedInvoiceId,
|
||||
};
|
||||
@ -201,6 +227,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
||||
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
||||
paymentMethodId: attributes.paymentMethodId!,
|
||||
taxRegimeCode: attributes.taxRegimeCode!,
|
||||
taxConfig: attributes.taxConfig!,
|
||||
linkedInvoiceId: attributes.linkedInvoiceId!,
|
||||
};
|
||||
|
||||
@ -264,6 +291,15 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
||||
payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
|
||||
payment_term_id: null,
|
||||
tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
|
||||
tax_mode: source.taxConfig.taxMode === "perLine" ? "per_line" : "single",
|
||||
default_iva_code: maybeToNullable(source.taxConfig.defaultIvaCode, (value) => value),
|
||||
uses_equivalence_surcharge: source.taxConfig.usesEquivalenceSurcharge,
|
||||
default_rec_code: maybeToNullable(source.taxConfig.defaultRecCode, (value) => value),
|
||||
uses_retention: source.taxConfig.usesRetention,
|
||||
default_retention_code: maybeToNullable(
|
||||
source.taxConfig.defaultRetentionCode,
|
||||
(value) => value
|
||||
),
|
||||
|
||||
subtotal_amount_value: allAmounts.subtotalAmount.value,
|
||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared";
|
||||
import { ItemPositionSchema, ProformaTaxConfigPatchSchema, TaxCombinationCodeSchema } from "../../shared";
|
||||
|
||||
export const CreateProformaItemRequestSchema = z.object({
|
||||
position: ItemPositionSchema,
|
||||
@ -49,6 +49,7 @@ export const CreateProformaRequestSchema = z.object({
|
||||
payment_method_id: z.uuid().nullable().optional(),
|
||||
payment_term_id: z.uuid().nullable().optional(),
|
||||
tax_regime_code: z.string().nullable().optional(),
|
||||
tax_config: ProformaTaxConfigPatchSchema.optional(),
|
||||
|
||||
items: z.array(CreateProformaItemRequestSchema).optional(),
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { ItemPositionSchema } from "../../shared";
|
||||
import { ItemPositionSchema, ProformaTaxConfigPatchSchema } from "../../shared";
|
||||
|
||||
export const UpdateProformaItemRequestSchema = z.object({
|
||||
position: ItemPositionSchema,
|
||||
@ -54,6 +54,7 @@ export const UpdateProformaByIdRequestSchema = z.object({
|
||||
payment_method_id: z.uuid().nullable().optional(),
|
||||
payment_term_id: z.uuid().nullable().optional(),
|
||||
tax_regime_code: z.string().nullable().optional(),
|
||||
tax_config: ProformaTaxConfigPatchSchema.optional(),
|
||||
|
||||
// retención como código??? retencion_15
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import { z } from "zod/v4";
|
||||
import {
|
||||
PaymentMethodRefSchema,
|
||||
PaymentTermRefSchema,
|
||||
ProformaTaxConfigSchema,
|
||||
TaxRegimeRefSchema,
|
||||
TaxesBreakdownSchema,
|
||||
} from "../../shared";
|
||||
@ -50,6 +51,7 @@ export const GetProformaByIdResponseSchema = z.object({
|
||||
payment_term: PaymentTermRefSchema.nullable(),
|
||||
|
||||
tax_regime: TaxRegimeRefSchema.nullable(),
|
||||
tax_config: ProformaTaxConfigSchema,
|
||||
|
||||
subtotal_amount: MoneySchema,
|
||||
items_discount_amount: MoneySchema,
|
||||
|
||||
@ -3,6 +3,7 @@ export * from "./item-position.dto";
|
||||
export * from "./payment-method-ref.dto";
|
||||
export * from "./payment-term-ref.dto";
|
||||
export * from "./proforma";
|
||||
export * from "./proforma-tax-config.dto";
|
||||
export * from "./tax-combination-code.dto";
|
||||
export * from "./tax-regime-ref.dto";
|
||||
export * from "./taxes-breakdown.dto";
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const ProformaTaxModeSchema = z.enum(["single", "per_line"]);
|
||||
|
||||
export const ProformaTaxConfigSchema = z.object({
|
||||
tax_mode: ProformaTaxModeSchema,
|
||||
default_iva_code: z.string().nullable(),
|
||||
uses_equivalence_surcharge: z.boolean(),
|
||||
default_rec_code: z.string().nullable(),
|
||||
uses_retention: z.boolean(),
|
||||
default_retention_code: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const ProformaTaxConfigPatchSchema = ProformaTaxConfigSchema.partial();
|
||||
|
||||
export type ProformaTaxConfigDTO = z.infer<typeof ProformaTaxConfigSchema>;
|
||||
export type ProformaTaxConfigPatchDTO = z.infer<typeof ProformaTaxConfigPatchSchema>;
|
||||
@ -42,6 +42,14 @@ export const GetProformaByIdAdapter = {
|
||||
|
||||
customerId: dto.customer_id,
|
||||
recipient: mapRecipient(dto),
|
||||
taxConfig: {
|
||||
taxMode: dto.tax_config.tax_mode === "per_line" ? "perLine" : "single",
|
||||
defaultIvaCode: dto.tax_config.default_iva_code,
|
||||
usesEquivalenceSurcharge: dto.tax_config.uses_equivalence_surcharge,
|
||||
defaultRecCode: dto.tax_config.default_rec_code,
|
||||
usesRetention: dto.tax_config.uses_retention,
|
||||
defaultRetentionCode: dto.tax_config.default_retention_code,
|
||||
},
|
||||
taxes: dto.taxes.map(mapTaxSummary),
|
||||
|
||||
paymentMethodId: dto.payment_method?.id ?? null,
|
||||
|
||||
@ -3,6 +3,15 @@ import type { ProformaRecipient } from "./proforma-recipient.entity";
|
||||
import type { ProformaStatus } from "./proforma-status.entity";
|
||||
import type { ProformaTaxSummary } from "./proforma-tax-summary.entity";
|
||||
|
||||
export interface ProformaTaxConfig {
|
||||
taxMode: "single" | "perLine";
|
||||
defaultIvaCode: string | null;
|
||||
usesEquivalenceSurcharge: boolean;
|
||||
defaultRecCode: string | null;
|
||||
usesRetention: boolean;
|
||||
defaultRetentionCode: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface que representa una proforma en el sistema,
|
||||
* adaptada desde la respuesta de la API.
|
||||
@ -33,6 +42,7 @@ export interface Proforma {
|
||||
recipient: ProformaRecipient;
|
||||
|
||||
taxRegimeCode: string | null;
|
||||
taxConfig: ProformaTaxConfig;
|
||||
taxes: ProformaTaxSummary[];
|
||||
|
||||
paymentMethodId: string | null;
|
||||
|
||||
@ -79,6 +79,30 @@ export const getProformaRecCode = (
|
||||
return definition?.recCode ?? null;
|
||||
};
|
||||
|
||||
export const getProformaTaxPercentageFromCode = (taxCode: string | null | undefined): number | null => {
|
||||
if (!taxCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = Object.values(PROFORMA_TAX_DEFINITIONS).find(
|
||||
(candidate) => candidate.taxCode === taxCode
|
||||
);
|
||||
|
||||
return definition?.taxPercentage ?? null;
|
||||
};
|
||||
|
||||
export const getProformaRecPercentageFromCode = (recCode: string | null | undefined): number | null => {
|
||||
if (!recCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = Object.values(PROFORMA_TAX_DEFINITIONS).find(
|
||||
(candidate) => candidate.recCode === recCode
|
||||
);
|
||||
|
||||
return definition?.recPercentage ?? null;
|
||||
};
|
||||
|
||||
export const getProformaRetentionCode = (
|
||||
retentionPercentage: number | string | null | undefined
|
||||
): string | null => {
|
||||
@ -89,6 +113,20 @@ export const getProformaRetentionCode = (
|
||||
return PROFORMA_RETENTION_DEFINITIONS[parsedRetentionPercentage].retentionCode;
|
||||
};
|
||||
|
||||
export const getProformaRetentionPercentageFromCode = (
|
||||
retentionCode: string | null | undefined
|
||||
): number | null => {
|
||||
if (!retentionCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = Object.values(PROFORMA_RETENTION_DEFINITIONS).find(
|
||||
(candidate) => candidate.retentionCode === retentionCode
|
||||
);
|
||||
|
||||
return definition?.retentionPercentage ?? null;
|
||||
};
|
||||
|
||||
export const getProformaTaxOptions = () => PROFORMA_TAX_OPTIONS;
|
||||
|
||||
export const getProformaRecOptions = () => PROFORMA_REC_OPTIONS;
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import type { Proforma } from "../../shared";
|
||||
import {
|
||||
getProformaRecPercentageFromCode,
|
||||
getProformaRetentionPercentageFromCode,
|
||||
getProformaTaxPercentageFromCode,
|
||||
} from "../../shared";
|
||||
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
||||
import { buildProformaUpdateDefault } from "../utils";
|
||||
|
||||
@ -68,49 +73,19 @@ const resolveFiscalState = (
|
||||
proforma: Proforma,
|
||||
proformaDefaults: ProformaUpdateForm
|
||||
): FiscalState => {
|
||||
// Caso habitual: una sola combinación de impuestos
|
||||
if (proforma.taxes.length === 1) {
|
||||
const taxSummary = proforma.taxes[0];
|
||||
|
||||
const hasTaxPercentage = taxSummary.ivaCode !== null;
|
||||
const hasRecPercentage = taxSummary.recCode !== null;
|
||||
const hasRetentionPercentage = taxSummary.retentionCode !== null;
|
||||
|
||||
return {
|
||||
taxMode: "single",
|
||||
defaultTaxPercentage: hasTaxPercentage ? taxSummary.ivaPercentage : null,
|
||||
defaultRecPercentage: hasRecPercentage ? taxSummary.recPercentage : null,
|
||||
defaultRetentionPercentage: hasRetentionPercentage ? taxSummary.retentionPercentage : null,
|
||||
hasTaxPercentage,
|
||||
hasRecPercentage,
|
||||
hasRetentionPercentage,
|
||||
};
|
||||
}
|
||||
|
||||
// Caso excepcional: proforma sin impuestos
|
||||
if (proforma.taxes.length === 0) {
|
||||
return {
|
||||
taxMode: "single",
|
||||
defaultTaxPercentage: null,
|
||||
defaultRecPercentage: null,
|
||||
defaultRetentionPercentage: null,
|
||||
hasTaxPercentage: false,
|
||||
hasRecPercentage: false,
|
||||
hasRetentionPercentage: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Proforma con varias combinaciones de impuestos
|
||||
const taxSummary = proforma.taxes[0];
|
||||
const hasRecPercentage = taxSummary.recCode !== null;
|
||||
const defaultTaxPercentage = getProformaTaxPercentageFromCode(proforma.taxConfig.defaultIvaCode);
|
||||
const defaultRecPercentage = getProformaRecPercentageFromCode(proforma.taxConfig.defaultRecCode);
|
||||
const defaultRetentionPercentage = getProformaRetentionPercentageFromCode(
|
||||
proforma.taxConfig.defaultRetentionCode
|
||||
);
|
||||
|
||||
return {
|
||||
taxMode: "perLine",
|
||||
defaultTaxPercentage: null,
|
||||
defaultRecPercentage: hasRecPercentage ? taxSummary.recPercentage : null,
|
||||
defaultRetentionPercentage: null,
|
||||
hasTaxPercentage: false,
|
||||
hasRecPercentage,
|
||||
hasRetentionPercentage: false,
|
||||
taxMode: proforma.taxConfig.taxMode ?? proformaDefaults.taxMode,
|
||||
defaultTaxPercentage,
|
||||
defaultRecPercentage,
|
||||
defaultRetentionPercentage,
|
||||
hasTaxPercentage: proforma.taxConfig.defaultIvaCode !== null,
|
||||
hasRecPercentage: proforma.taxConfig.usesEquivalenceSurcharge,
|
||||
hasRetentionPercentage: proforma.taxConfig.usesRetention,
|
||||
};
|
||||
};
|
||||
|
||||
@ -95,6 +95,17 @@ export const buildUpdateProformaByIdParams = (
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldSendTaxConfig(patch)) {
|
||||
data.tax_config = {
|
||||
tax_mode: formData.taxMode === "perLine" ? "per_line" : "single",
|
||||
default_iva_code: resolveSingleIvaCode(formData),
|
||||
uses_equivalence_surcharge: formData.hasRecPercentage,
|
||||
default_rec_code: resolveSingleRecCode(formData),
|
||||
uses_retention: formData.hasRetentionPercentage,
|
||||
default_retention_code: resolveRetentionCode(formData),
|
||||
};
|
||||
}
|
||||
|
||||
// Si se han tocado los detalles, se envían todos
|
||||
if (shouldReplaceItems(patch)) {
|
||||
const context = buildProformaItemUpdateDTOContext(formData);
|
||||
@ -158,6 +169,18 @@ const shouldReplaceItems = (patch: ProformaUpdatePatch): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
const shouldSendTaxConfig = (patch: ProformaUpdatePatch): boolean => {
|
||||
return (
|
||||
ObjectHelper.hasOwn(patch, "taxMode") ||
|
||||
ObjectHelper.hasOwn(patch, "hasTaxPercentage") ||
|
||||
ObjectHelper.hasOwn(patch, "taxPercentage") ||
|
||||
ObjectHelper.hasOwn(patch, "hasRecPercentage") ||
|
||||
ObjectHelper.hasOwn(patch, "recPercentage") ||
|
||||
ObjectHelper.hasOwn(patch, "hasRetentionPercentage") ||
|
||||
ObjectHelper.hasOwn(patch, "retentionPercentage")
|
||||
);
|
||||
};
|
||||
|
||||
const resolveSingleIvaCode = (formData: ProformaUpdateForm): string | null => {
|
||||
if (!formData.hasTaxPercentage) {
|
||||
return null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user