Facturas de cliente

This commit is contained in:
David Arranz 2025-09-18 13:17:18 +02:00
parent c8db3099a4
commit 1919a54dc2
35 changed files with 127 additions and 70 deletions

View File

@ -85,7 +85,7 @@ export class CreateAccountUseCase {
website: dto.website ? Maybe.some(dto.website) : Maybe.none(),
legalRecord: dto.legal_record,
defaultTax: dto.default_tax,
langCode: dto.lang_code,
langCode: dto.language_code,
currencyCode: dto.currency_code,
logo: dto.logo ? Maybe.some(dto.logo) : Maybe.none(),
};

View File

@ -106,8 +106,8 @@ export class UpdateAccountUseCase {
validatedData.defaultTax = dto.default_tax;
}
if (dto.lang_code) {
validatedData.langCode = dto.lang_code;
if (dto.language_code) {
validatedData.langCode = dto.language_code;
}
if (dto.currency_code) {

View File

@ -38,7 +38,7 @@ const sampleAccountPrimitives = {
legal_record: "Registro Mercantil XYZ",
default_tax: 21,
status: "active",
lang_code: "es",
language_code: "es",
currency_code: "EUR",
logo: "https://xyz.com/logo.png",
};
@ -87,7 +87,7 @@ const accountBuilder = (accountData: any) => {
: Maybe.none(),
legalRecord: sampleAccountPrimitives.legal_record,
defaultTax: sampleAccountPrimitives.default_tax,
langCode: sampleAccountPrimitives.lang_code,
langCode: sampleAccountPrimitives.language_code,
currencyCode: sampleAccountPrimitives.currency_code,
logo: sampleAccountPrimitives.logo ? Maybe.some(sampleAccountPrimitives.logo) : Maybe.none(),
};

View File

@ -30,7 +30,7 @@ const sampleAccount = {
legal_record: "Registro Mercantil XYZ",
default_tax: 21,
status: "active",
lang_code: "es",
language_code: "es",
currency_code: "EUR",
logo: "https://xyz.com/logo.png",
};

View File

@ -64,7 +64,7 @@ export class AccountMapper
website: source.website ? Maybe.some(source.website) : Maybe.none(),
legalRecord: source.legal_record,
defaultTax: source.default_tax,
langCode: source.lang_code,
langCode: source.language_code,
currencyCode: source.currency_code,
logo: source.logo ? Maybe.some(source.logo) : Maybe.none(),
},
@ -94,7 +94,7 @@ export class AccountMapper
legal_record: source.legalRecord,
default_tax: source.defaultTax,
status: source.isActive ? "active" : "inactive",
lang_code: source.langCode,
language_code: source.langCode,
currency_code: source.currencyCode,
logo: source.logo.getOrUndefined(),
};

View File

@ -37,7 +37,7 @@ export class AccountModel extends Model<InferAttributes<AccountModel>, AccountCr
declare default_tax: number;
declare status: string;
declare lang_code: string;
declare language_code: string;
declare currency_code: string;
declare logo: CreationOptional<string>;
}
@ -129,7 +129,7 @@ export default (sequelize: Sequelize) => {
defaultValue: null,
},
lang_code: {
language_code: {
type: DataTypes.STRING(2),
allowNull: false,
defaultValue: "es",

View File

@ -30,7 +30,7 @@ export const createAccountPresenter: ICreateAccountPresenter = {
default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode),
language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()),
}),

View File

@ -30,7 +30,7 @@ export const getAccountPresenter: IGetAccountPresenter = {
default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode),
language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()),
}),

View File

@ -31,7 +31,7 @@ export const listAccountsPresenter: IListAccountsPresenter = {
default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode),
language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()),
})),

View File

@ -30,7 +30,7 @@ export const updateAccountPresenter: IUpdateAccountPresenter = {
default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode),
language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()),
}),

View File

@ -21,7 +21,7 @@ export interface ICreateAccountRequestDTO {
legal_record: string;
default_tax: number;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
}
@ -46,7 +46,7 @@ export interface IUpdateAccountRequestDTO {
legal_record: string;
default_tax: number;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
}

View File

@ -21,7 +21,7 @@ export interface IListAccountsResponseDTO {
default_tax: number;
status: string;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
}
@ -49,7 +49,7 @@ export interface IGetAccountResponseDTO {
default_tax: number;
status: string;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
}
@ -77,7 +77,7 @@ export interface ICreateAccountResponseDTO {
default_tax: number;
status: string;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
}
@ -108,7 +108,7 @@ export interface IUpdateAccountResponseDTO {
default_tax: number;
status: string;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
}

View File

@ -27,7 +27,7 @@ export const ICreateAccountRequestSchema = z.object({
default_tax: z.number(),
status: z.string(),
lang_code: z.string(),
language_code: z.string(),
currency_code: z.string(),
logo: z.string(),
});
@ -55,7 +55,7 @@ export const IUpdateAccountRequestSchema = z.object({
default_tax: z.number(),
status: z.string(),
lang_code: z.string(),
language_code: z.string(),
currency_code: z.string(),
logo: z.string(),
});

View File

@ -63,7 +63,7 @@ export class ContactMapper
legalRecord: source.legal_record,
defaultTax: source.default_tax,
status: source.status,
langCode: source.lang_code,
langCode: source.language_code,
currencyCode: source.currency_code,
},
idOrError.data
@ -96,7 +96,7 @@ export class ContactMapper
legal_record: source.legalRecord,
default_tax: source.defaultTax,
status: source.isActive ? "active" : "inactive",
lang_code: source.langCode,
language_code: source.langCode,
currency_code: source.currencyCode,
});
}

View File

@ -41,7 +41,7 @@ export class ContactModel extends Model<
declare default_tax: number;
declare status: string;
declare lang_code: string;
declare language_code: string;
declare currency_code: string;
}
@ -130,7 +130,7 @@ export default (sequelize: Sequelize) => {
defaultValue: 2100,
},
lang_code: {
language_code: {
type: DataTypes.STRING(2),
allowNull: false,
defaultValue: "es",

View File

@ -32,7 +32,7 @@ export const listContactsPresenter: IListContactsPresenter = {
default_tax: ensureNumber(contact.defaultTax),
status: ensureString(contact.isActive ? "active" : "inactive"),
lang_code: ensureString(contact.langCode),
language_code: ensureString(contact.langCode),
currency_code: ensureString(contact.currencyCode),
})),
};

View File

@ -22,6 +22,6 @@ export interface IListContactsResponseDTO {
default_tax: number;
status: string;
lang_code: string;
language_code: string;
currency_code: string;
}

View File

@ -20,7 +20,7 @@ i18n
detection: {
order: ["navigator"],
},
debug: import.meta.env.DEV,
debug: false, //import.meta.env.DEV,
fallbackLng: "es",
interpolation: {
escapeValue: false,

View File

@ -31,8 +31,6 @@ export const getAppRouter = () => {
const grouped = groupModulesByLayout(modules);
console.debug(grouped);
return createBrowserRouter(
createRoutesFromElements(
<Route path='/'>

View File

@ -4,7 +4,7 @@ export interface IGetProfileResponseDTO {
id: string;
name: string;
email: string;
lang_code: string;
language_code: string;
roles: string[];
dealer: {
id: string;
@ -16,7 +16,7 @@ export interface IGetProfileResponseDTO {
default_quote_validity: string;
default_tax: IPercentageDTO;
status: string;
lang_code: string;
language_code: string;
currency_code: string;
logo: string;
};

View File

@ -7,7 +7,7 @@ export interface ILoginResponseDTO {
id: string;
name: string;
email: string;
lang_code: string;
language_code: string;
roles: string[];
token: string;
refresh_token: string;

View File

@ -1,6 +1,8 @@
export type DTO<T = unknown> = T;
export type BinaryOutput = Buffer; // Puedes ampliar a Readable si usas streams
export type IPresenterOutputParams = Record<string, unknown>;
export interface IPresenter<TSource = unknown, TOutput = DTO> {
toOutput(source: TSource): TOutput | Promise<TOutput>;
toOutput(source: TSource, params?: IPresenterOutputParams): TOutput | Promise<TOutput>;
}

View File

@ -1,13 +1,9 @@
import { IPresenterRegistry } from "./presenter-registry.interface";
import { IPresenter } from "./presenter.interface";
export type IPresenterParams = {
presenterRegistry: IPresenterRegistry;
} & Record<string, unknown>;
import { IPresenter, IPresenterOutputParams } from "./presenter.interface";
export abstract class Presenter<TSource = unknown, TOutput = unknown>
implements IPresenter<TSource, TOutput>
{
constructor(protected presenterRegistry: IPresenterRegistry) {}
abstract toOutput(source: TSource): TOutput;
abstract toOutput(source: TSource, params?: IPresenterOutputParams): TOutput;
}

View File

@ -0,0 +1,11 @@
import { QuantityDTO } from "@erp/core";
import { Quantity } from "@repo/rdx-ddd";
export function formatQuantityDTO(quantity_value: QuantityDTO) {
const value = Quantity.create({
value: Number(quantity_value.value),
scale: Number(quantity_value.scale),
}).data;
return value.toNumber;
}

View File

@ -1,3 +1,4 @@
export * from "./format-money-dto";
export * from "./format-percentage-dto";
export * from "./format-quantity-dto";
export * from "./map-dto-to-customer-invoice-props";

View File

@ -1,12 +0,0 @@
import { MoneyDTO } from "@erp/core";
import { MoneyValue } from "@repo/rdx-ddd";
export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
const money = MoneyValue.create({
value: Number(amount.value),
currency_code: amount.currency_code,
scale: Number(amount.scale),
}).data;
return money.format(locale);
}

View File

@ -9,7 +9,7 @@ type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement<
>;
export class CustomerInvoiceItemsFullPresenter extends Presenter {
protected _map(
private _mapItem(
invoiceItem: CustomerInvoiceItem,
index: number
): GetCustomerInvoiceItemByInvoiceIdResponseDTO {
@ -48,6 +48,6 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
}
toOutput(invoiceItems: CustomerInvoiceItems): GetCustomerInvoiceByIdResponseDTO["items"] {
return invoiceItems.map(this._map);
return invoiceItems.map(this._mapItem);
}
}

View File

@ -0,0 +1,41 @@
import { IPresenterOutputParams, Presenter } from "@erp/core/api";
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import { ArrayElement } from "@repo/rdx-utils";
import { formatMoneyDTO, formatPercentageDTO, formatQuantityDTO } from "../../helpers";
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
export class CustomerInvoiceItemsReportPersenter extends Presenter<
CustomerInvoiceItemsDTO,
unknown
> {
private _locale!: string;
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) {
return {
...invoiceItem,
quantity: formatQuantityDTO(invoiceItem.quantity),
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, this._locale),
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, this._locale),
discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage),
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, this._locale),
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, this._locale),
total_amount: formatMoneyDTO(invoiceItem.total_amount, this._locale),
};
}
toOutput(invoiceItems: CustomerInvoiceItemsDTO, params: IPresenterOutputParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
return invoiceItems.map((item, index) => {
return this._mapItem(item, index);
});
}
}

View File

@ -7,12 +7,22 @@ export class CustomerInvoiceReportPresenter extends Presenter<
unknown
> {
toOutput(invoiceDTO: GetCustomerInvoiceByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items",
projection: "REPORT",
format: "JSON",
});
const locale = invoiceDTO.language_code;
const itemsDTO = itemsPresenter.toOutput(invoiceDTO.items, {
locale,
});
return {
...invoiceDTO,
items: itemsDTO,
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, locale),
percentage: formatPercentageDTO(invoiceDTO.discount_percentage),
discount_percetage: formatPercentageDTO(invoiceDTO.discount_percentage),
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale),
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale),
total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale),

View File

@ -1,2 +1,3 @@
export * from "./customer-invoice-items.report.presenter";
export * from "./customer-invoice.report.presenter";
export * from "./list-customer-invoices.presenter";

View File

@ -23,6 +23,7 @@ import {
} from "../application";
import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core";
import { CustomerInvoiceItemsReportPersenter } from "../application/presenters/queries/customer-invoice-items.report.presenter";
import { CustomerInvoiceService } from "../domain";
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize";
@ -108,6 +109,14 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
},
presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
},
{
key: {
resource: "customer-invoice-items",
projection: "REPORT",
format: "JSON",
},
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
},
{
key: {
resource: "customer-invoice",

View File

@ -122,7 +122,7 @@
"placeholder": "Select default tax",
"description": "The default tax rate for the customer"
},
"lang_code": {
"language_code": {
"label": "Language",
"placeholder": "Select language",
"description": "The preferred language of the customer"

View File

@ -124,7 +124,7 @@
"placeholder": "Seleccione el impuesto por defecto",
"description": "La tasa de impuesto por defecto para el cliente"
},
"lang_code": {
"language_code": {
"label": "Idioma",
"placeholder": "Seleccione el idioma",
"description": "El idioma preferido del cliente"

View File

@ -40,10 +40,10 @@ const defaultCustomerData = {
country: "ES",
postal_code: "28080",
province: "Madrid",
lang_code: "es",
language_code: "es",
currency_code: "EUR",
legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456",
default_tax: ["iva_21", "rec_5_2"],
default_taxes: ["iva_21", "rec_5_2"],
};
interface CustomerFormProps {
@ -290,20 +290,20 @@ export const CustomerEditForm = ({
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
<TaxesMultiSelectField
control={form.control}
name='default_tax'
name='default_taxes'
required
label={t("form_fields.default_tax.label")}
placeholder={t("form_fields.default_tax.placeholder")}
description={t("form_fields.default_tax.description")}
label={t("form_fields.default_taxes.label")}
placeholder={t("form_fields.default_taxes.placeholder")}
description={t("form_fields.default_taxes.description")}
/>
<SelectField
control={form.control}
name='lang_code'
name='language_code'
required
label={t("form_fields.lang_code.label")}
placeholder={t("form_fields.lang_code.placeholder")}
description={t("form_fields.lang_code.description")}
label={t("form_fields.language_code.label")}
placeholder={t("form_fields.language_code.placeholder")}
description={t("form_fields.language_code.description")}
items={[
{ value: "es", label: "Español" },
{ value: "en", label: "Inglés" },

View File

@ -33,8 +33,8 @@ export function CustomerBasicInfoFields({ control }: { control: any }) {
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
<FormControl>
<RadioGroup
value={field.value ? "1" : "0"}
onValueChange={(val) => field.onChange(val === "1")}
onValueChange={field.onChange}
defaultValue={field.value ? "1" : "0"}
className='flex gap-6'
>
<FormItem className='flex items-center space-x-2'>