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:
|
En create:
|
||||||
|
|
||||||
- `taxRegimeCode` representa configuracion fiscal inicial
|
- `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
|
- `taxes` representa impuestos realmente calculados desde lineas valoradas
|
||||||
- `totals` representa importes 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_method_id?: string | null;
|
||||||
payment_term_id?: string | null;
|
payment_term_id?: string | null;
|
||||||
tax_regime_code?: 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?: [];
|
items?: [];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -52,6 +60,7 @@ Notas:
|
|||||||
- el contrato actual del modulo puede seguir exigiendo otros campos tecnicos ya existentes
|
- el contrato actual del modulo puede seguir exigiendo otros campos tecnicos ya existentes
|
||||||
- `items` es opcional; si existe, puede ir vacio
|
- `items` es opcional; si existe, puede ir vacio
|
||||||
- no se usan defaults ocultos en Zod para disfrazar la intencion del request
|
- 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
|
## Response esperada al crear sin lineas
|
||||||
|
|
||||||
@ -83,6 +92,7 @@ Adaptando nombres exactos al contrato publico actual:
|
|||||||
## Punto correcto de calculo
|
## Punto correcto de calculo
|
||||||
|
|
||||||
- `create` crea cabecera minima, numera y resuelve defaults
|
- `create` crea cabecera minima, numera y resuelve defaults
|
||||||
|
- `create` persiste tambien el `tax_config` resuelto
|
||||||
- `update` permite anadir o editar lineas
|
- `update` permite anadir o editar lineas
|
||||||
- al existir lineas valoradas, `update` recalcula `taxes` y `totals`
|
- 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
|
- el backend emite `target_invoice_series_code` como nombre preferente
|
||||||
- `series` puede mantenerse solo como alias legacy de salida mientras existan consumidores antiguos
|
- `series` puede mantenerse solo como alias legacy de salida mientras existan consumidores antiguos
|
||||||
- `taxes` representa impuestos calculados a partir de lineas valoradas
|
- `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
|
- `totals` representa importes calculados a partir de lineas valoradas
|
||||||
- create no debe inventar filas fiscales sobre base `0`
|
- 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
|
- create de proforma se consolida como creacion de cabecera/documento inicial
|
||||||
- una proforma sin lineas es valida y debe persistirse con `items = []`
|
- una proforma sin lineas es valida y debe persistirse con `items = []`
|
||||||
- `tax_regime_code` queda como configuracion fiscal inicial de la cabecera
|
- `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`
|
- `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`
|
- 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
|
- 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 { IProformaRepository } from "../repositories";
|
||||||
import type { ProformaPaymentResolver } from "../services";
|
import type { ProformaPaymentResolver } from "../services";
|
||||||
import {
|
import {
|
||||||
@ -16,6 +18,7 @@ export const buildProformaCreator = (params: {
|
|||||||
repository: IProformaRepository;
|
repository: IProformaRepository;
|
||||||
proformaSeriesValidator: ProformaSeriesValidator;
|
proformaSeriesValidator: ProformaSeriesValidator;
|
||||||
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||||
|
customerServices: ICustomerPublicServices;
|
||||||
}): IProformaCreator => {
|
}): IProformaCreator => {
|
||||||
return new ProformaCreator(params);
|
return new ProformaCreator(params);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,11 +20,13 @@ import {
|
|||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
|
type ProformaTaxMode,
|
||||||
type ProformaRecipient,
|
type ProformaRecipient,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import type {
|
import type {
|
||||||
ProformaCreateInputProps,
|
ProformaCreateInputProps,
|
||||||
ProformaItemCreateInputProps,
|
ProformaItemCreateInputProps,
|
||||||
|
ProformaTaxConfigInputProps,
|
||||||
ProformaItemTaxCodesInput,
|
ProformaItemTaxCodesInput,
|
||||||
} from "../models";
|
} from "../models";
|
||||||
|
|
||||||
@ -132,6 +134,8 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const taxConfig = this.mapTaxConfig(dto.tax_config, errors);
|
||||||
|
|
||||||
const items = this.mapItemsProps(dto.items ?? [], {
|
const items = this.mapItemsProps(dto.items ?? [], {
|
||||||
languageCode: languageCode!,
|
languageCode: languageCode!,
|
||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
@ -164,6 +168,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
linkedInvoiceId: Maybe.none(),
|
linkedInvoiceId: Maybe.none(),
|
||||||
|
|
||||||
taxRegimeCode: taxRegimeCode!,
|
taxRegimeCode: taxRegimeCode!,
|
||||||
|
taxConfig,
|
||||||
paymentMethodId: paymentMethodId!,
|
paymentMethodId: paymentMethodId!,
|
||||||
|
|
||||||
globalDiscountPercentage: globalDiscountPercentage!,
|
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 {
|
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||||
|
|||||||
@ -15,11 +15,18 @@ import {
|
|||||||
import { Maybe, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
import { Maybe, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
|
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
|
||||||
import { InvoiceSerie, ItemAmount, ItemDescription, ItemQuantity } from "../../../domain";
|
import {
|
||||||
|
InvoiceSerie,
|
||||||
|
ItemAmount,
|
||||||
|
ItemDescription,
|
||||||
|
ItemQuantity,
|
||||||
|
type ProformaTaxMode,
|
||||||
|
} from "../../../domain";
|
||||||
import type {
|
import type {
|
||||||
ProformaItemPatchInputProps,
|
ProformaItemPatchInputProps,
|
||||||
ProformaItemTaxCodesInput,
|
ProformaItemTaxCodesInput,
|
||||||
ProformaPatchInputProps,
|
ProformaPatchInputProps,
|
||||||
|
ProformaTaxConfigPatchInputProps,
|
||||||
} from "../models";
|
} from "../models";
|
||||||
|
|
||||||
export interface IUpdateProformaInputMapper {
|
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) {
|
if (dto.items !== undefined) {
|
||||||
proformaPatchProps.items = this.mapItemsProps(dto.items, { errors });
|
proformaPatchProps.items = this.mapItemsProps(dto.items, { errors });
|
||||||
}
|
}
|
||||||
@ -257,6 +268,73 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
return Maybe.some(String(code).trim());
|
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 {
|
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
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 { Maybe } from "@repo/rdx-utils";
|
||||||
import type { InvoiceSerie } from "../../../domain";
|
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.
|
* - 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<
|
export type ProformaCreateInputProps = Omit<
|
||||||
IProformaCreateProps,
|
IProformaCreateProps,
|
||||||
"items" | "proformaReference" | "proformaNumber" | "documentSeriesId" | "series"
|
"items" | "proformaReference" | "proformaNumber" | "documentSeriesId" | "series" | "taxConfig"
|
||||||
> & {
|
> & {
|
||||||
proformaSeriesCode: Maybe<InvoiceSerie>;
|
proformaSeriesCode: Maybe<InvoiceSerie>;
|
||||||
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||||
|
taxConfig?: ProformaTaxConfigInputProps;
|
||||||
items: ProformaItemCreateInputProps[];
|
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 { Maybe } from "@repo/rdx-utils";
|
||||||
import type { InvoiceSerie } from "../../../domain";
|
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.
|
* - 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"> & {
|
export type ProformaPatchInputProps = Omit<ProformaPatchProps, "items" | "series"> & {
|
||||||
targetInvoiceSeriesCode?: Maybe<InvoiceSerie>;
|
targetInvoiceSeriesCode?: Maybe<InvoiceSerie>;
|
||||||
|
taxConfig?: ProformaTaxConfigPatchInputProps;
|
||||||
items?: ProformaItemPatchInputProps[];
|
items?: ProformaItemPatchInputProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,12 @@ export interface EnsureProformaItemTaxesParams {
|
|||||||
taxCodes: ProformaItemTaxCodesInput;
|
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`.
|
* 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> {
|
private collectCodes(taxCodes: ProformaItemTaxCodesInput): Collection<string> {
|
||||||
const codes = [taxCodes.ivaCode, taxCodes.recCode, taxCodes.retentionCode]
|
const codes = [taxCodes.ivaCode, taxCodes.recCode, taxCodes.retentionCode]
|
||||||
.filter((code) => code.isSome())
|
.filter((code) => code.isSome())
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
@ -6,8 +7,10 @@ import {
|
|||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
Proforma,
|
Proforma,
|
||||||
|
ProformaTaxConfig,
|
||||||
|
type ProformaTaxMode,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import type { ProformaCreateInputProps } from "../models";
|
import type { ProformaCreateInputProps, ProformaTaxConfigInputProps } from "../models";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
|
||||||
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
||||||
@ -36,13 +39,14 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
paymentResolver: ProformaPaymentResolver;
|
paymentResolver: ProformaPaymentResolver;
|
||||||
proformaSeriesValidator: ProformaSeriesValidator;
|
proformaSeriesValidator: ProformaSeriesValidator;
|
||||||
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||||
|
customerServices: ICustomerPublicServices;
|
||||||
}
|
}
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>> {
|
async create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>> {
|
||||||
const { companyId, id, props, transaction } = params;
|
const { companyId, id, props, transaction } = params;
|
||||||
|
|
||||||
const resolvedProps = await this.resolveCreateProps(props);
|
const resolvedProps = await this.resolveCreateProps(props, transaction);
|
||||||
|
|
||||||
if (resolvedProps.isFailure) {
|
if (resolvedProps.isFailure) {
|
||||||
return Result.fail(resolvedProps.error);
|
return Result.fail(resolvedProps.error);
|
||||||
@ -132,14 +136,30 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async resolveCreateProps(
|
private async resolveCreateProps(
|
||||||
props: ProformaCreateInputProps
|
props: ProformaCreateInputProps,
|
||||||
|
transaction?: unknown
|
||||||
): Promise<
|
): Promise<
|
||||||
Result<Omit<IProformaCreateProps, "proformaReference" | "proformaNumber" | "documentSeriesId">, Error>
|
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 = {
|
const _newProps = {
|
||||||
...props,
|
...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
|
// Tax Regime => comprobar que existe
|
||||||
@ -162,6 +182,17 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
return Result.fail(paymentMethodResult.error);
|
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
|
// En create una proforma puede nacer sin líneas: en ese caso solo persistimos
|
||||||
// cabecera + defaults iniciales. Los impuestos y totales materializados salen
|
// cabecera + defaults iniciales. Los impuestos y totales materializados salen
|
||||||
// de `items`, así que con `items = []` no se generan filas fiscales.
|
// de `items`, así que con `items = []` no se generan filas fiscales.
|
||||||
@ -186,7 +217,177 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
|
|
||||||
return Result.ok({
|
return Result.ok({
|
||||||
..._newProps,
|
..._newProps,
|
||||||
|
taxConfig: taxConfigResult.data,
|
||||||
items: resolvedItems,
|
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 { 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 { ProformaPatchInputProps } from "../models";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
|
||||||
@ -72,6 +77,7 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
const resolvedPatch = await this.resolvePatchProps({
|
const resolvedPatch = await this.resolvePatchProps({
|
||||||
companyId,
|
companyId,
|
||||||
currentInvoiceDate: proforma.invoiceDate,
|
currentInvoiceDate: proforma.invoiceDate,
|
||||||
|
currentTaxConfig: proforma.taxConfig,
|
||||||
patch: patchProps,
|
patch: patchProps,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
@ -107,10 +113,11 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
private async resolvePatchProps(params: {
|
private async resolvePatchProps(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
currentInvoiceDate: UtcDate;
|
currentInvoiceDate: UtcDate;
|
||||||
|
currentTaxConfig: Proforma["taxConfig"];
|
||||||
patch: ProformaPatchInputProps;
|
patch: ProformaPatchInputProps;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}): Promise<Result<ProformaPatchProps, Error>> {
|
}): Promise<Result<ProformaPatchProps, Error>> {
|
||||||
const { patch, companyId, currentInvoiceDate, transaction } = params;
|
const { patch, companyId, currentInvoiceDate, transaction, currentTaxConfig } = params;
|
||||||
|
|
||||||
if (patch.taxRegimeCode !== undefined) {
|
if (patch.taxRegimeCode !== undefined) {
|
||||||
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
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) {
|
if (patch.targetInvoiceSeriesCode !== undefined) {
|
||||||
const targetInvoiceSeriesValidationResult =
|
const targetInvoiceSeriesValidationResult =
|
||||||
await this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
|
await this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
|
||||||
@ -148,10 +166,16 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (patch.items === undefined) {
|
if (patch.items === undefined) {
|
||||||
return Result.ok({
|
const resolvedPatch: ProformaPatchProps = {
|
||||||
...patch,
|
...patch,
|
||||||
series: patch.targetInvoiceSeriesCode,
|
series: patch.targetInvoiceSeriesCode,
|
||||||
} as ProformaPatchProps);
|
};
|
||||||
|
|
||||||
|
if (taxConfigResult.data !== undefined) {
|
||||||
|
resolvedPatch.taxConfig = taxConfigResult.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(resolvedPatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveInvoiceDate = patch.proformaDate ?? currentInvoiceDate;
|
const effectiveInvoiceDate = patch.proformaDate ?? currentInvoiceDate;
|
||||||
@ -174,11 +198,175 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok({
|
const resolvedPatch: ProformaPatchProps = {
|
||||||
...patch,
|
...patch,
|
||||||
series: patch.targetInvoiceSeriesCode,
|
series: patch.targetInvoiceSeriesCode,
|
||||||
items: resolvedItems,
|
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 {
|
private isApprovedPatchAllowed(patch: ProformaPatchInputProps): boolean {
|
||||||
|
|||||||
@ -69,6 +69,17 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
tax_regime: maybeToNullable(taxRegime, (taxRegime) => taxRegime),
|
tax_regime: maybeToNullable(taxRegime, (taxRegime) => taxRegime),
|
||||||
|
|
||||||
payment_term: null,
|
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(),
|
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
||||||
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import {
|
|||||||
import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors";
|
import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors";
|
||||||
import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
|
import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
|
||||||
import { canManuallyTransitionProformaStatus } from "../services";
|
import { canManuallyTransitionProformaStatus } from "../services";
|
||||||
import { ProformaItemTaxes, type ProformaRecipient } from "../value-objects";
|
import { ProformaItemTaxes, ProformaTaxConfig, type ProformaRecipient } from "../value-objects";
|
||||||
|
|
||||||
export interface IProformaCreateProps {
|
export interface IProformaCreateProps {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
@ -57,6 +57,7 @@ export interface IProformaCreateProps {
|
|||||||
|
|
||||||
paymentMethodId: Maybe<UniqueID>;
|
paymentMethodId: Maybe<UniqueID>;
|
||||||
taxRegimeCode: Maybe<string>;
|
taxRegimeCode: Maybe<string>;
|
||||||
|
taxConfig: ProformaTaxConfig;
|
||||||
|
|
||||||
items: IProformaItemCreateProps[];
|
items: IProformaItemCreateProps[];
|
||||||
globalDiscountPercentage: DiscountPercentage;
|
globalDiscountPercentage: DiscountPercentage;
|
||||||
@ -107,6 +108,7 @@ export interface IProforma {
|
|||||||
|
|
||||||
paymentMethodId: Maybe<UniqueID>;
|
paymentMethodId: Maybe<UniqueID>;
|
||||||
taxRegimeCode: Maybe<string>;
|
taxRegimeCode: Maybe<string>;
|
||||||
|
taxConfig: ProformaTaxConfig;
|
||||||
|
|
||||||
linkedInvoiceId: Maybe<UniqueID>;
|
linkedInvoiceId: Maybe<UniqueID>;
|
||||||
|
|
||||||
@ -169,10 +171,13 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
|||||||
// Mutabilidad
|
// Mutabilidad
|
||||||
public update(patchProps: ProformaPatchProps): Result<Proforma, Error> {
|
public update(patchProps: ProformaPatchProps): Result<Proforma, Error> {
|
||||||
const { items, ...otherProps } = patchProps;
|
const { items, ...otherProps } = patchProps;
|
||||||
|
const definedProps = Object.fromEntries(
|
||||||
|
Object.entries(otherProps).filter(([, value]) => value !== undefined)
|
||||||
|
) as Partial<ProformaInternalProps>;
|
||||||
|
|
||||||
const candidateProps: ProformaInternalProps = {
|
const candidateProps: ProformaInternalProps = {
|
||||||
...this.props,
|
...this.props,
|
||||||
...otherProps,
|
...definedProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validacciones
|
// Validacciones
|
||||||
@ -281,6 +286,10 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
|||||||
return this.props.taxRegimeCode;
|
return this.props.taxRegimeCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get taxConfig(): ProformaTaxConfig {
|
||||||
|
return this.props.taxConfig;
|
||||||
|
}
|
||||||
|
|
||||||
public get linkedInvoiceId(): Maybe<UniqueID> {
|
public get linkedInvoiceId(): Maybe<UniqueID> {
|
||||||
return this.props.linkedInvoiceId;
|
return this.props.linkedInvoiceId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./proforma-item-taxes.vo";
|
export * from "./proforma-item-taxes.vo";
|
||||||
export * from "./proforma-recipient.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 payment_term_id: CreationOptional<string | null>;
|
||||||
|
|
||||||
declare tax_regime_code: 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_value: number;
|
||||||
declare subtotal_amount_scale: number;
|
declare subtotal_amount_scale: number;
|
||||||
@ -249,6 +255,36 @@ export default (database: Sequelize) => {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: null,
|
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: {
|
subtotal_amount_value: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { SetupParams } from "@erp/core/api";
|
import type { SetupParams } from "@erp/core/api";
|
||||||
|
import type { ICustomerPublicServices } from "@erp/customers/api";
|
||||||
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
@ -51,6 +52,7 @@ export function buildProformaPublicServices(
|
|||||||
const { database } = params;
|
const { database } = params;
|
||||||
|
|
||||||
const catalogs = resolveProformaCatalogsDeps(params);
|
const catalogs = resolveProformaCatalogsDeps(params);
|
||||||
|
const customerServices = params.getService<ICustomerPublicServices>("customers:general");
|
||||||
const documentSeriesServices =
|
const documentSeriesServices =
|
||||||
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||||
|
|
||||||
@ -92,6 +94,7 @@ export function buildProformaPublicServices(
|
|||||||
paymentResolver: catalogResolvers.paymentResolver,
|
paymentResolver: catalogResolvers.paymentResolver,
|
||||||
proformaSeriesValidator,
|
proformaSeriesValidator,
|
||||||
targetInvoiceSeriesValidator,
|
targetInvoiceSeriesValidator,
|
||||||
|
customerServices,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
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 { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
|
|
||||||
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
||||||
@ -65,6 +66,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
|
|
||||||
const catalogs = resolveProformaCatalogsDeps(params);
|
const catalogs = resolveProformaCatalogsDeps(params);
|
||||||
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
||||||
|
const customerServices = params.getService<ICustomerPublicServices>("customers:general");
|
||||||
const documentSeriesServices =
|
const documentSeriesServices =
|
||||||
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||||
|
|
||||||
@ -109,6 +111,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
repository,
|
repository,
|
||||||
proformaSeriesValidator,
|
proformaSeriesValidator,
|
||||||
targetInvoiceSeriesValidator,
|
targetInvoiceSeriesValidator,
|
||||||
|
customerServices,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updater = buildProformaUpdater({
|
const updater = buildProformaUpdater({
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
Proforma,
|
Proforma,
|
||||||
|
ProformaTaxConfig,
|
||||||
type ProformaInternalProps,
|
type ProformaInternalProps,
|
||||||
ProformaItems,
|
ProformaItems,
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
@ -177,6 +178,25 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
errors
|
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 {
|
return {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
companyId,
|
companyId,
|
||||||
@ -193,6 +213,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
paymentMethodId,
|
paymentMethodId,
|
||||||
taxRegimeCode,
|
taxRegimeCode,
|
||||||
|
taxConfig,
|
||||||
|
|
||||||
globalDiscountPercentage,
|
globalDiscountPercentage,
|
||||||
linkedInvoiceId,
|
linkedInvoiceId,
|
||||||
@ -265,6 +286,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
|
|
||||||
paymentMethodId: attributes.paymentMethodId!,
|
paymentMethodId: attributes.paymentMethodId!,
|
||||||
taxRegimeCode: attributes.taxRegimeCode!,
|
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.
|
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_code: maybeToNullable(source.taxRegimeCode, (value) => value),
|
||||||
tax_regime_description: null,
|
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_value: allAmounts.subtotalAmount.value,
|
||||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
Proforma,
|
Proforma,
|
||||||
|
ProformaTaxConfig,
|
||||||
type ProformaInternalProps,
|
type ProformaInternalProps,
|
||||||
ProformaItems,
|
ProformaItems,
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
@ -117,6 +118,30 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
"tax_regime_code",
|
"tax_regime_code",
|
||||||
errors
|
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(
|
const globalDiscountPercentage = extractOrPushError(
|
||||||
DiscountPercentage.create({
|
DiscountPercentage.create({
|
||||||
value: Number(raw.global_discount_percentage_value ?? 0),
|
value: Number(raw.global_discount_percentage_value ?? 0),
|
||||||
@ -148,6 +173,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
paymentMethodId,
|
paymentMethodId,
|
||||||
taxRegimeCode,
|
taxRegimeCode,
|
||||||
|
taxConfig,
|
||||||
globalDiscountPercentage,
|
globalDiscountPercentage,
|
||||||
linkedInvoiceId,
|
linkedInvoiceId,
|
||||||
};
|
};
|
||||||
@ -201,6 +227,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
||||||
paymentMethodId: attributes.paymentMethodId!,
|
paymentMethodId: attributes.paymentMethodId!,
|
||||||
taxRegimeCode: attributes.taxRegimeCode!,
|
taxRegimeCode: attributes.taxRegimeCode!,
|
||||||
|
taxConfig: attributes.taxConfig!,
|
||||||
linkedInvoiceId: attributes.linkedInvoiceId!,
|
linkedInvoiceId: attributes.linkedInvoiceId!,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -264,6 +291,15 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
|
payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
|
||||||
payment_term_id: null,
|
payment_term_id: null,
|
||||||
tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
|
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_value: allAmounts.subtotalAmount.value,
|
||||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@erp/core";
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared";
|
import { ItemPositionSchema, ProformaTaxConfigPatchSchema, TaxCombinationCodeSchema } from "../../shared";
|
||||||
|
|
||||||
export const CreateProformaItemRequestSchema = z.object({
|
export const CreateProformaItemRequestSchema = z.object({
|
||||||
position: ItemPositionSchema,
|
position: ItemPositionSchema,
|
||||||
@ -49,6 +49,7 @@ export const CreateProformaRequestSchema = z.object({
|
|||||||
payment_method_id: z.uuid().nullable().optional(),
|
payment_method_id: z.uuid().nullable().optional(),
|
||||||
payment_term_id: z.uuid().nullable().optional(),
|
payment_term_id: z.uuid().nullable().optional(),
|
||||||
tax_regime_code: z.string().nullable().optional(),
|
tax_regime_code: z.string().nullable().optional(),
|
||||||
|
tax_config: ProformaTaxConfigPatchSchema.optional(),
|
||||||
|
|
||||||
items: z.array(CreateProformaItemRequestSchema).optional(),
|
items: z.array(CreateProformaItemRequestSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from "@erp/core";
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { ItemPositionSchema } from "../../shared";
|
import { ItemPositionSchema, ProformaTaxConfigPatchSchema } from "../../shared";
|
||||||
|
|
||||||
export const UpdateProformaItemRequestSchema = z.object({
|
export const UpdateProformaItemRequestSchema = z.object({
|
||||||
position: ItemPositionSchema,
|
position: ItemPositionSchema,
|
||||||
@ -54,6 +54,7 @@ export const UpdateProformaByIdRequestSchema = z.object({
|
|||||||
payment_method_id: z.uuid().nullable().optional(),
|
payment_method_id: z.uuid().nullable().optional(),
|
||||||
payment_term_id: z.uuid().nullable().optional(),
|
payment_term_id: z.uuid().nullable().optional(),
|
||||||
tax_regime_code: z.string().nullable().optional(),
|
tax_regime_code: z.string().nullable().optional(),
|
||||||
|
tax_config: ProformaTaxConfigPatchSchema.optional(),
|
||||||
|
|
||||||
// retención como código??? retencion_15
|
// retención como código??? retencion_15
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { z } from "zod/v4";
|
|||||||
import {
|
import {
|
||||||
PaymentMethodRefSchema,
|
PaymentMethodRefSchema,
|
||||||
PaymentTermRefSchema,
|
PaymentTermRefSchema,
|
||||||
|
ProformaTaxConfigSchema,
|
||||||
TaxRegimeRefSchema,
|
TaxRegimeRefSchema,
|
||||||
TaxesBreakdownSchema,
|
TaxesBreakdownSchema,
|
||||||
} from "../../shared";
|
} from "../../shared";
|
||||||
@ -50,6 +51,7 @@ export const GetProformaByIdResponseSchema = z.object({
|
|||||||
payment_term: PaymentTermRefSchema.nullable(),
|
payment_term: PaymentTermRefSchema.nullable(),
|
||||||
|
|
||||||
tax_regime: TaxRegimeRefSchema.nullable(),
|
tax_regime: TaxRegimeRefSchema.nullable(),
|
||||||
|
tax_config: ProformaTaxConfigSchema,
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
items_discount_amount: MoneySchema,
|
items_discount_amount: MoneySchema,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export * from "./item-position.dto";
|
|||||||
export * from "./payment-method-ref.dto";
|
export * from "./payment-method-ref.dto";
|
||||||
export * from "./payment-term-ref.dto";
|
export * from "./payment-term-ref.dto";
|
||||||
export * from "./proforma";
|
export * from "./proforma";
|
||||||
|
export * from "./proforma-tax-config.dto";
|
||||||
export * from "./tax-combination-code.dto";
|
export * from "./tax-combination-code.dto";
|
||||||
export * from "./tax-regime-ref.dto";
|
export * from "./tax-regime-ref.dto";
|
||||||
export * from "./taxes-breakdown.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,
|
customerId: dto.customer_id,
|
||||||
recipient: mapRecipient(dto),
|
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),
|
taxes: dto.taxes.map(mapTaxSummary),
|
||||||
|
|
||||||
paymentMethodId: dto.payment_method?.id ?? null,
|
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 { ProformaStatus } from "./proforma-status.entity";
|
||||||
import type { ProformaTaxSummary } from "./proforma-tax-summary.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,
|
* Interface que representa una proforma en el sistema,
|
||||||
* adaptada desde la respuesta de la API.
|
* adaptada desde la respuesta de la API.
|
||||||
@ -33,6 +42,7 @@ export interface Proforma {
|
|||||||
recipient: ProformaRecipient;
|
recipient: ProformaRecipient;
|
||||||
|
|
||||||
taxRegimeCode: string | null;
|
taxRegimeCode: string | null;
|
||||||
|
taxConfig: ProformaTaxConfig;
|
||||||
taxes: ProformaTaxSummary[];
|
taxes: ProformaTaxSummary[];
|
||||||
|
|
||||||
paymentMethodId: string | null;
|
paymentMethodId: string | null;
|
||||||
|
|||||||
@ -79,6 +79,30 @@ export const getProformaRecCode = (
|
|||||||
return definition?.recCode ?? null;
|
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 = (
|
export const getProformaRetentionCode = (
|
||||||
retentionPercentage: number | string | null | undefined
|
retentionPercentage: number | string | null | undefined
|
||||||
): string | null => {
|
): string | null => {
|
||||||
@ -89,6 +113,20 @@ export const getProformaRetentionCode = (
|
|||||||
return PROFORMA_RETENTION_DEFINITIONS[parsedRetentionPercentage].retentionCode;
|
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 getProformaTaxOptions = () => PROFORMA_TAX_OPTIONS;
|
||||||
|
|
||||||
export const getProformaRecOptions = () => PROFORMA_REC_OPTIONS;
|
export const getProformaRecOptions = () => PROFORMA_REC_OPTIONS;
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import type { Proforma } from "../../shared";
|
import type { Proforma } from "../../shared";
|
||||||
|
import {
|
||||||
|
getProformaRecPercentageFromCode,
|
||||||
|
getProformaRetentionPercentageFromCode,
|
||||||
|
getProformaTaxPercentageFromCode,
|
||||||
|
} from "../../shared";
|
||||||
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
||||||
import { buildProformaUpdateDefault } from "../utils";
|
import { buildProformaUpdateDefault } from "../utils";
|
||||||
|
|
||||||
@ -68,49 +73,19 @@ const resolveFiscalState = (
|
|||||||
proforma: Proforma,
|
proforma: Proforma,
|
||||||
proformaDefaults: ProformaUpdateForm
|
proformaDefaults: ProformaUpdateForm
|
||||||
): FiscalState => {
|
): FiscalState => {
|
||||||
// Caso habitual: una sola combinación de impuestos
|
const defaultTaxPercentage = getProformaTaxPercentageFromCode(proforma.taxConfig.defaultIvaCode);
|
||||||
if (proforma.taxes.length === 1) {
|
const defaultRecPercentage = getProformaRecPercentageFromCode(proforma.taxConfig.defaultRecCode);
|
||||||
const taxSummary = proforma.taxes[0];
|
const defaultRetentionPercentage = getProformaRetentionPercentageFromCode(
|
||||||
|
proforma.taxConfig.defaultRetentionCode
|
||||||
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;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
taxMode: "perLine",
|
taxMode: proforma.taxConfig.taxMode ?? proformaDefaults.taxMode,
|
||||||
defaultTaxPercentage: null,
|
defaultTaxPercentage,
|
||||||
defaultRecPercentage: hasRecPercentage ? taxSummary.recPercentage : null,
|
defaultRecPercentage,
|
||||||
defaultRetentionPercentage: null,
|
defaultRetentionPercentage,
|
||||||
hasTaxPercentage: false,
|
hasTaxPercentage: proforma.taxConfig.defaultIvaCode !== null,
|
||||||
hasRecPercentage,
|
hasRecPercentage: proforma.taxConfig.usesEquivalenceSurcharge,
|
||||||
hasRetentionPercentage: false,
|
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
|
// Si se han tocado los detalles, se envían todos
|
||||||
if (shouldReplaceItems(patch)) {
|
if (shouldReplaceItems(patch)) {
|
||||||
const context = buildProformaItemUpdateDTOContext(formData);
|
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 => {
|
const resolveSingleIvaCode = (formData: ProformaUpdateForm): string | null => {
|
||||||
if (!formData.hasTaxPercentage) {
|
if (!formData.hasTaxPercentage) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user