Poder indicar la etiqueta del impuesto

This commit is contained in:
David Arranz 2026-10-05 12:04:43 +02:00
parent 75de474c16
commit 1d7ed6df38
30 changed files with 121 additions and 57 deletions

View File

@ -70,6 +70,7 @@ export const QuotePricesResume = ({ currency }: { currency: CurrencyData }) => {
const { formatNumber } = useLocalization(); const { formatNumber } = useLocalization();
const pricesWatch = useWatch({ control, name: ["subtotal_price", "discount", "tax"] }); const pricesWatch = useWatch({ control, name: ["subtotal_price", "discount", "tax"] });
const taxLabel = useWatch({ control, name: "tax_label" }) || t("quotes.form_fields.tax.label");
const totals = calculateQuoteTotals(pricesWatch); const totals = calculateQuoteTotals(pricesWatch);
@ -124,9 +125,7 @@ export const QuotePricesResume = ({ currency }: { currency: CurrencyData }) => {
<Separator orientation='vertical' className='w-px h-16 mx-2' /> <Separator orientation='vertical' className='w-px h-16 mx-2' />
<div className='grid flex-1 h-16 grid-cols-2 gap-6 auto-rows-max'> <div className='grid flex-1 h-16 grid-cols-2 gap-6 auto-rows-max'>
<div className='grid gap-1 font-medium text-muted-foreground'> <div className='grid gap-1 font-medium text-muted-foreground'>
<CardDescription className='text-sm'> <CardDescription className='text-sm'>{taxLabel}</CardDescription>
{t("quotes.form_fields.tax.label")}
</CardDescription>
<FormPercentageField <FormPercentageField
scale={2} scale={2}
@ -137,7 +136,7 @@ export const QuotePricesResume = ({ currency }: { currency: CurrencyData }) => {
</div> </div>
<div className='grid gap-1 font-semibold text-muted-foreground'> <div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm text-right'> <CardDescription className='text-sm text-right'>
{t("quotes.form_fields.tax_price.label")} {t("quotes.form_fields.tax_price.label", { label: "" })}
</CardDescription> </CardDescription>
<CardTitle className='flex items-baseline justify-end gap-1 text-2xl tabular-nums'> <CardTitle className='flex items-baseline justify-end gap-1 text-2xl tabular-nums'>
{tax_price} {tax_price}

View File

@ -332,7 +332,10 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
<li className='flex items-center justify-between'> <li className='flex items-center justify-between'>
<span className='text-muted-foreground'> <span className='text-muted-foreground'>
{t("quotes.form_fields.tax_value.label", { value: totals.tax })} {t("quotes.form_fields.tax_value.label", {
label: data.tax_label,
value: totals.tax,
})}
</span> </span>
<span>{totals.tax_price}</span> <span>{totals.tax_price}</span>
</li> </li>

View File

@ -86,6 +86,7 @@ export const QuoteEdit = () => {
amount: undefined, amount: undefined,
scale: 0, scale: 0,
}, },
tax_label: "% IVA",
tax_price: { tax_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,

View File

@ -379,12 +379,12 @@
"desc": "Percentage Tax" "desc": "Percentage Tax"
}, },
"tax_value": { "tax_value": {
"label": "Tax ({{value}}%)", "label": "{{label}} ({{value}}%)",
"placeholder": "", "placeholder": "",
"desc": "Percentage Tax" "desc": "Percentage Tax"
}, },
"tax_price": { "tax_price": {
"label": "Tax price", "label": "Tax amount",
"placeholder": "", "placeholder": "",
"desc": "Percentage tax price" "desc": "Percentage tax price"
}, },

View File

@ -376,24 +376,24 @@
"desc": "" "desc": ""
}, },
"tax": { "tax": {
"label": "IVA (%)", "label": "Impuesto (%)",
"placeholder": "", "placeholder": "",
"desc": "Porcentaje de IVA" "desc": "Porcentaje de impuesto"
}, },
"tax_value": { "tax_value": {
"label": "IVA ({{value}}%)", "label": "{{label}} ({{value}}%)",
"placeholder": "", "placeholder": "",
"desc": "Porcentaje de IVA" "desc": "Porcentaje de impuesto"
}, },
"tax_price": { "tax_price": {
"label": "Imp. IVA", "label": "Imp. impuesto",
"placeholder": "", "placeholder": "",
"desc": "Importe del IVA" "desc": "Importe del impuesto"
}, },
"total_price": { "total_price": {
"label": "Total price", "label": "Imp. total",
"placeholder": "", "placeholder": "",
"desc": "Quote total price" "desc": "Importe total"
}, },
"items": { "items": {
"quantity": { "quantity": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" /> <link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>Presupuestador Uecko</title> <title>Presupuestador Uecko</title>
<script type="module" crossorigin src="/assets/index-DzeF1POa.js"></script> <script type="module" crossorigin src="/assets/index-vq9AFyzk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CBtV2-Es.css"> <link rel="stylesheet" crossorigin href="/assets/index-CBtV2-Es.css">
</head> </head>

View File

@ -205,6 +205,13 @@ export class CreateQuoteUseCase
return Result.fail(taxOrError.error); return Result.fail(taxOrError.error);
} }
const taxLabelOrError = TextValueObject.create(
this._dealer?.additionalInfo.get("default_tax_label")?.toString() ?? "% IVA"
);
if (taxLabelOrError.isFailure) {
return Result.fail(taxLabelOrError.error);
}
const dateSentOrError = UTCDateValue.create(null); const dateSentOrError = UTCDateValue.create(null);
if (dateSentOrError.isFailure) { if (dateSentOrError.isFailure) {
return Result.fail(dateSentOrError.error); return Result.fail(dateSentOrError.error);
@ -290,6 +297,7 @@ export class CreateQuoteUseCase
discount: discountOrError.object, discount: discountOrError.object,
tax: taxOrError.object, tax: taxOrError.object,
taxLabel: taxLabelOrError.object,
items, items,

View File

@ -119,6 +119,7 @@ export class DuplicateQuoteUseCase
discount: sourceQuote.discount, discount: sourceQuote.discount,
tax: sourceQuote.tax, tax: sourceQuote.tax,
taxLabel: sourceQuote.taxLabel,
items, items,

View File

@ -91,7 +91,8 @@ export class UpdateQuoteUseCase
quoteDTO, quoteDTO,
id, id,
dealerId, dealerId,
existingQuote.showLineDiscountColumn existingQuote.showLineDiscountColumn,
existingQuote.taxLabel
); );
if (quoteOrError.isFailure) { if (quoteOrError.isFailure) {
@ -141,7 +142,8 @@ export class UpdateQuoteUseCase
quoteDTO: IUpdateQuote_Request_DTO, quoteDTO: IUpdateQuote_Request_DTO,
quoteId: UniqueID, quoteId: UniqueID,
dealerId: UniqueID, dealerId: UniqueID,
existingShowLineDiscountColumn: boolean existingShowLineDiscountColumn: boolean,
existingTaxLabel: Note
): Result<Quote, IDomainError> { ): Result<Quote, IDomainError> {
const statusOrError = QuoteStatus.create(quoteDTO.status); const statusOrError = QuoteStatus.create(quoteDTO.status);
if (statusOrError.isFailure) { if (statusOrError.isFailure) {
@ -286,6 +288,7 @@ export class UpdateQuoteUseCase
discount: discountOrError.object, discount: discountOrError.object,
tax: taxOrError.object, tax: taxOrError.object,
taxLabel: existingTaxLabel,
items, items,

View File

@ -1,8 +0,0 @@
# Cambia "Quote" por "Quote" en todos los archivos y directorios de forma recursiva
find . -depth -name "*Quote*" | while read file; do
# Obtén el nuevo nombre con 'Reseller' reemplazado por 'Quote'
newfile=$(echo "$file" | sed 's/Quote/Quote/g')
# Renombra el archivo/directorio
mv "$file" "$newfile"
done

View File

@ -34,6 +34,7 @@ export interface IQuoteProps {
//subtotalPrice: MoneyValue; //subtotalPrice: MoneyValue;
discount: Percentage; discount: Percentage;
tax: Percentage; tax: Percentage;
taxLabel: Note;
//totalPrice: MoneyValue; //totalPrice: MoneyValue;
dealerId: UniqueID; dealerId: UniqueID;
@ -65,6 +66,7 @@ export interface IQuote {
tax: Percentage; tax: Percentage;
taxPrice: MoneyValue; taxPrice: MoneyValue;
taxLabel: Note;
totalPrice: MoneyValue; totalPrice: MoneyValue;
@ -185,6 +187,10 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
return this.props.tax; return this.props.tax;
} }
get taxLabel(): Note {
return this.props.taxLabel;
}
get taxPrice(): MoneyValue { get taxPrice(): MoneyValue {
return this.beforeTaxPrice.percentage(this.tax.toNumber()); return this.beforeTaxPrice.percentage(this.tax.toNumber());
} }

View File

@ -36,6 +36,7 @@ export const CreateQuotePresenter: ICreateQuotePresenter = {
before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(), before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(),
tax: quote.tax.convertScale(2).toObject(), tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.convertScale(2).toObject(), tax_price: quote.taxPrice.convertScale(2).toObject(),
total_price: quote.totalPrice.convertScale(2).toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),

View File

@ -36,6 +36,7 @@ export const DuplicateQuotePresenter: IDuplicateQuotePresenter = {
before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(), before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(),
tax: quote.tax.convertScale(2).toObject(), tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.convertScale(2).toObject(), tax_price: quote.taxPrice.convertScale(2).toObject(),
total_price: quote.totalPrice.convertScale(2).toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),

View File

@ -36,6 +36,7 @@ export const GetQuotePresenter: IGetQuotePresenter = {
before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(), before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(),
tax: quote.tax.convertScale(2).toObject(), tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.convertScale(2).toObject(), tax_price: quote.taxPrice.convertScale(2).toObject(),
total_price: quote.totalPrice.convertScale(2).toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),

View File

@ -35,6 +35,7 @@ export const ListQuotesPresenter: IListQuotesPresenter = {
before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(), before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(),
tax: quote.tax.convertScale(2).toObject(), tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.convertScale(2).toObject(), tax_price: quote.taxPrice.convertScale(2).toObject(),
total_price: quote.totalPrice.convertScale(2).toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),

View File

@ -2,7 +2,7 @@ import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
import { createQuoteMapper } from "@/contexts/sales/infrastructure/mappers/quote.mapper"; import { createQuoteMapper } from "@/contexts/sales/infrastructure/mappers/quote.mapper";
import { ReportQuotePresenter } from "./ReportQuote.reporter"; import { ReportQuotePresenter } from "./ReportQuote.reporter";
const source = (showLineDiscountColumn: boolean, generalDiscount: number) => const source = (showLineDiscountColumn: boolean, generalDiscount: number, taxLabel = "% IVA") =>
({ ({
show_line_discount_column: showLineDiscountColumn, show_line_discount_column: showLineDiscountColumn,
items: [], items: [],
@ -20,6 +20,7 @@ const source = (showLineDiscountColumn: boolean, generalDiscount: number) =>
notes: "", notes: "",
discount: generalDiscount, discount: generalDiscount,
tax: 21, tax: 21,
tax_label: taxLabel,
dealer_id: "770672b9-5a87-4e8e-a8af-8b44a7432e12", dealer_id: "770672b9-5a87-4e8e-a8af-8b44a7432e12",
date_sent: null, date_sent: null,
}, },
@ -44,4 +45,10 @@ describe("ReportQuotePresenter", () => {
expect(html).not.toContain("Descuentos"); expect(html).not.toContain("Descuentos");
expect(html).not.toContain("% Descuento"); expect(html).not.toContain("% Descuento");
}); });
it("renders the tax label persisted in the quote", async () => {
const html = await ReportQuotePresenter.toHTML(mapper.mapToDomain(source(false, 0, "% IGIC")), context);
expect(html).toContain("% IGIC");
});
}); });

View File

@ -95,6 +95,7 @@ const map = async (quote: Quote, context: ISalesContext) => {
before_tax_price: quote.beforeTaxPrice.toFormat(), before_tax_price: quote.beforeTaxPrice.toFormat(),
tax: quote.tax.toFormat(), tax: quote.tax.toFormat(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.toFormat(), tax_price: quote.taxPrice.toFormat(),
total_price: quote.totalPrice.toFormat(), total_price: quote.totalPrice.toFormat(),

View File

@ -139,7 +139,7 @@
<td class="px-4 py-2 text-right border">{{before_tax_price}}</td> <td class="px-4 py-2 text-right border">{{before_tax_price}}</td>
</tr> </tr>
<tr class="border-b"> <tr class="border-b">
<td class="px-4 py-2">% IVA</td> <td class="px-4 py-2">{{tax_label}}</td>
<td class="px-4 py-2 text-right border">{{tax}}</td> <td class="px-4 py-2 text-right border">{{tax}}</td>
<td class="px-4 py-2 text-right border">{{tax_price}}</td> <td class="px-4 py-2 text-right border">{{tax_price}}</td>
</tr> </tr>

View File

@ -36,6 +36,7 @@ export const UpdateQuotePresenter: IUpdateQuotePresenter = {
before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(), before_tax_price: quote.beforeTaxPrice.convertScale(2).toObject(),
tax: quote.tax.convertScale(2).toObject(), tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.convertScale(2).toObject(), tax_price: quote.taxPrice.convertScale(2).toObject(),
total_price: quote.totalPrice.convertScale(2).toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),

View File

@ -1,7 +1,7 @@
import { ISalesContext } from "../Sales.context"; import { ISalesContext } from "../Sales.context";
import { createQuoteMapper } from "./quote.mapper"; import { createQuoteMapper } from "./quote.mapper";
const source = (showLineDiscountColumn: boolean | null) => const source = (showLineDiscountColumn: boolean | null, taxLabel?: string | null) =>
({ ({
show_line_discount_column: showLineDiscountColumn, show_line_discount_column: showLineDiscountColumn,
items: [], items: [],
@ -19,6 +19,7 @@ const source = (showLineDiscountColumn: boolean | null) =>
notes: "", notes: "",
discount: 0, discount: 0,
tax: 21, tax: 21,
tax_label: taxLabel,
dealer_id: "770672b9-5a87-4e8e-a8af-8b44a7432e12", dealer_id: "770672b9-5a87-4e8e-a8af-8b44a7432e12",
date_sent: null, date_sent: null,
}, },
@ -37,4 +38,15 @@ describe("QuoteMapper show_line_discount_column", () => {
it("uses true for historical null values", () => { it("uses true for historical null values", () => {
expect(mapper.mapToDomain(source(null)).showLineDiscountColumn).toBe(true); expect(mapper.mapToDomain(source(null)).showLineDiscountColumn).toBe(true);
}); });
it("maps a tax label in both persistence directions", () => {
const quote = mapper.mapToDomain(source(true, "% IGIC"));
expect(quote.taxLabel.toString()).toBe("% IGIC");
expect(mapper.mapToPersistence(quote).tax_label).toBe("% IGIC");
});
it("uses the default tax label for historical null values", () => {
expect(mapper.mapToDomain(source(true, null)).taxLabel.toString()).toBe("% IVA");
});
}); });

View File

@ -18,7 +18,7 @@ import {
} from "../../domain"; } from "../../domain";
import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus"; import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus";
import { ISalesContext } from "../Sales.context"; import { ISalesContext } from "../Sales.context";
import { QuoteCreationAttributes, Quote_Model } from "../sequelize"; import { DEFAULT_QUOTE_TAX_LABEL, QuoteCreationAttributes, Quote_Model } from "../sequelize";
import { IQuoteItemMapper, createQuoteItemMapper } from "./quoteItem.mapper"; import { IQuoteItemMapper, createQuoteItemMapper } from "./quoteItem.mapper";
export interface IQuoteMapper export interface IQuoteMapper
@ -86,6 +86,9 @@ class QuoteMapper
scale: 2, scale: 2,
}) })
), ),
taxLabel: this.mapsValue(source, "tax_label", (label) =>
TextValueObject.create(label ?? DEFAULT_QUOTE_TAX_LABEL)
),
/*totalPrice: this.mapsValue(source, "total_price", (total_price) => /*totalPrice: this.mapsValue(source, "total_price", (total_price) =>
MoneyValue.create({ MoneyValue.create({
@ -137,6 +140,7 @@ class QuoteMapper
before_tax_price: source.beforeTaxPrice.convertScale(2).toPrimitive(), before_tax_price: source.beforeTaxPrice.convertScale(2).toPrimitive(),
tax: source.tax.convertScale(2).toPrimitive(), tax: source.tax.convertScale(2).toPrimitive(),
tax_label: source.taxLabel.toPrimitive(),
tax_price: source.taxPrice.convertScale(2).toPrimitive(), tax_price: source.taxPrice.convertScale(2).toPrimitive(),
total_price: source.totalPrice.convertScale(2).toPrimitive(), total_price: source.totalPrice.convertScale(2).toPrimitive(),

View File

@ -19,6 +19,8 @@ export type QuoteCreationAttributes = InferCreationAttributes<
dealer_id: string; dealer_id: string;
}; };
export const DEFAULT_QUOTE_TAX_LABEL = "% IVA";
export class Quote_Model extends Model< export class Quote_Model extends Model<
InferAttributes<Quote_Model, { omit: "items" | "dealer" }>, InferAttributes<Quote_Model, { omit: "items" | "dealer" }>,
InferCreationAttributes<Quote_Model, { omit: "items" | "dealer" }> InferCreationAttributes<Quote_Model, { omit: "items" | "dealer" }>
@ -60,6 +62,7 @@ export class Quote_Model extends Model<
declare before_tax_price: CreationOptional<number | null>; declare before_tax_price: CreationOptional<number | null>;
declare tax: CreationOptional<number | null>; declare tax: CreationOptional<number | null>;
declare tax_label: CreationOptional<string | null>;
declare tax_price: CreationOptional<number | null>; declare tax_price: CreationOptional<number | null>;
declare total_price: CreationOptional<number | null>; declare total_price: CreationOptional<number | null>;
@ -156,6 +159,12 @@ export default (sequelize: Sequelize) => {
allowNull: true, allowNull: true,
}, },
tax_label: {
type: DataTypes.STRING(32),
allowNull: false,
defaultValue: DEFAULT_QUOTE_TAX_LABEL,
},
tax_price: { tax_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,

View File

@ -9,6 +9,10 @@ import {
DEFAULT_TAX_LABEL, DEFAULT_TAX_LABEL,
Dealer_Model, Dealer_Model,
} from "@/contexts/sales/infrastructure/sequelize/dealer.model"; } from "@/contexts/sales/infrastructure/sequelize/dealer.model";
import {
DEFAULT_QUOTE_TAX_LABEL,
Quote_Model,
} from "@/contexts/sales/infrastructure/sequelize/quote.model";
import { trace } from "console"; import { trace } from "console";
import { config } from "../../config"; import { config } from "../../config";
import { app } from "../express/app"; import { app } from "../express/app";
@ -115,6 +119,10 @@ try {
{ default_tax_label: DEFAULT_TAX_LABEL }, { default_tax_label: DEFAULT_TAX_LABEL },
{ where: { default_tax_label: null } } { where: { default_tax_label: null } }
); );
await Quote_Model.update(
{ tax_label: DEFAULT_QUOTE_TAX_LABEL },
{ where: { tax_label: null } }
);
initStructure(sequelizeConn.connection); initStructure(sequelizeConn.connection);
insertUsers(); insertUsers();

View File

@ -20,6 +20,7 @@ export interface ICreateQuote_Response_DTO {
discount_price: IMoney_DTO; discount_price: IMoney_DTO;
before_tax_price: IMoney_DTO; before_tax_price: IMoney_DTO;
tax: IPercentage_DTO; tax: IPercentage_DTO;
tax_label: string;
tax_price: IMoney_DTO; tax_price: IMoney_DTO;
total_price: IMoney_DTO; total_price: IMoney_DTO;

View File

@ -20,6 +20,7 @@ export interface IDuplicateQuote_Response_DTO {
discount_price: IMoney_DTO; discount_price: IMoney_DTO;
before_tax_price: IMoney_DTO; before_tax_price: IMoney_DTO;
tax: IPercentage_DTO; tax: IPercentage_DTO;
tax_label: string;
tax_price: IMoney_DTO; tax_price: IMoney_DTO;
total_price: IMoney_DTO; total_price: IMoney_DTO;

View File

@ -20,6 +20,7 @@ export interface IGetQuote_Response_DTO {
discount_price: IMoney_DTO; discount_price: IMoney_DTO;
before_tax_price: IMoney_DTO; before_tax_price: IMoney_DTO;
tax: IPercentage_DTO; tax: IPercentage_DTO;
tax_label: string;
tax_price: IMoney_DTO; tax_price: IMoney_DTO;
total_price: IMoney_DTO; total_price: IMoney_DTO;

View File

@ -15,6 +15,7 @@ export interface IListQuotes_Response_DTO {
discount_price: IMoney_DTO; discount_price: IMoney_DTO;
before_tax_price: IMoney_DTO; before_tax_price: IMoney_DTO;
tax: IPercentage_DTO; tax: IPercentage_DTO;
tax_label: string;
tax_price: IMoney_DTO; tax_price: IMoney_DTO;
total_price: IMoney_DTO; total_price: IMoney_DTO;

View File

@ -20,6 +20,7 @@ export interface IUpdateQuote_Response_DTO {
discount_price: IMoney_DTO; discount_price: IMoney_DTO;
before_tax_price: IMoney_DTO; before_tax_price: IMoney_DTO;
tax: IPercentage_DTO; tax: IPercentage_DTO;
tax_label: string;
tax_price: IMoney_DTO; tax_price: IMoney_DTO;
total_price: IMoney_DTO; total_price: IMoney_DTO;