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 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);
@ -124,9 +125,7 @@ export const QuotePricesResume = ({ currency }: { currency: CurrencyData }) => {
<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 gap-1 font-medium text-muted-foreground'>
<CardDescription className='text-sm'>
{t("quotes.form_fields.tax.label")}
</CardDescription>
<CardDescription className='text-sm'>{taxLabel}</CardDescription>
<FormPercentageField
scale={2}
@ -137,7 +136,7 @@ export const QuotePricesResume = ({ currency }: { currency: CurrencyData }) => {
</div>
<div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm text-right'>
{t("quotes.form_fields.tax_price.label")}
{t("quotes.form_fields.tax_price.label", { label: "" })}
</CardDescription>
<CardTitle className='flex items-baseline justify-end gap-1 text-2xl tabular-nums'>
{tax_price}

View File

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

View File

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

View File

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

View File

@ -376,24 +376,24 @@
"desc": ""
},
"tax": {
"label": "IVA (%)",
"label": "Impuesto (%)",
"placeholder": "",
"desc": "Porcentaje de IVA"
"desc": "Porcentaje de impuesto"
},
"tax_value": {
"label": "IVA ({{value}}%)",
"label": "{{label}} ({{value}}%)",
"placeholder": "",
"desc": "Porcentaje de IVA"
"desc": "Porcentaje de impuesto"
},
"tax_price": {
"label": "Imp. IVA",
"label": "Imp. impuesto",
"placeholder": "",
"desc": "Importe del IVA"
"desc": "Importe del impuesto"
},
"total_price": {
"label": "Total price",
"label": "Imp. total",
"placeholder": "",
"desc": "Quote total price"
"desc": "Importe total"
},
"items": {
"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" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<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">
</head>

View File

@ -205,6 +205,13 @@ export class CreateQuoteUseCase
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);
if (dateSentOrError.isFailure) {
return Result.fail(dateSentOrError.error);
@ -290,6 +297,7 @@ export class CreateQuoteUseCase
discount: discountOrError.object,
tax: taxOrError.object,
taxLabel: taxLabelOrError.object,
items,

View File

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

View File

@ -91,7 +91,8 @@ export class UpdateQuoteUseCase
quoteDTO,
id,
dealerId,
existingQuote.showLineDiscountColumn
existingQuote.showLineDiscountColumn,
existingQuote.taxLabel
);
if (quoteOrError.isFailure) {
@ -141,7 +142,8 @@ export class UpdateQuoteUseCase
quoteDTO: IUpdateQuote_Request_DTO,
quoteId: UniqueID,
dealerId: UniqueID,
existingShowLineDiscountColumn: boolean
existingShowLineDiscountColumn: boolean,
existingTaxLabel: Note
): Result<Quote, IDomainError> {
const statusOrError = QuoteStatus.create(quoteDTO.status);
if (statusOrError.isFailure) {
@ -286,6 +288,7 @@ export class UpdateQuoteUseCase
discount: discountOrError.object,
tax: taxOrError.object,
taxLabel: existingTaxLabel,
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;
discount: Percentage;
tax: Percentage;
taxLabel: Note;
//totalPrice: MoneyValue;
dealerId: UniqueID;
@ -65,6 +66,7 @@ export interface IQuote {
tax: Percentage;
taxPrice: MoneyValue;
taxLabel: Note;
totalPrice: MoneyValue;
@ -185,6 +187,10 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
return this.props.tax;
}
get taxLabel(): Note {
return this.props.taxLabel;
}
get taxPrice(): MoneyValue {
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(),
tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.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(),
tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.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(),
tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.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(),
tax: quote.tax.convertScale(2).toObject(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.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 { ReportQuotePresenter } from "./ReportQuote.reporter";
const source = (showLineDiscountColumn: boolean, generalDiscount: number) =>
const source = (showLineDiscountColumn: boolean, generalDiscount: number, taxLabel = "% IVA") =>
({
show_line_discount_column: showLineDiscountColumn,
items: [],
@ -20,6 +20,7 @@ const source = (showLineDiscountColumn: boolean, generalDiscount: number) =>
notes: "",
discount: generalDiscount,
tax: 21,
tax_label: taxLabel,
dealer_id: "770672b9-5a87-4e8e-a8af-8b44a7432e12",
date_sent: null,
},
@ -44,4 +45,10 @@ describe("ReportQuotePresenter", () => {
expect(html).not.toContain("Descuentos");
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(),
tax: quote.tax.toFormat(),
tax_label: quote.taxLabel.toString(),
tax_price: quote.taxPrice.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>
</tr>
<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_price}}</td>
</tr>

View File

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

View File

@ -1,7 +1,7 @@
import { ISalesContext } from "../Sales.context";
import { createQuoteMapper } from "./quote.mapper";
const source = (showLineDiscountColumn: boolean | null) =>
const source = (showLineDiscountColumn: boolean | null, taxLabel?: string | null) =>
({
show_line_discount_column: showLineDiscountColumn,
items: [],
@ -19,6 +19,7 @@ const source = (showLineDiscountColumn: boolean | null) =>
notes: "",
discount: 0,
tax: 21,
tax_label: taxLabel,
dealer_id: "770672b9-5a87-4e8e-a8af-8b44a7432e12",
date_sent: null,
},
@ -37,4 +38,15 @@ describe("QuoteMapper show_line_discount_column", () => {
it("uses true for historical null values", () => {
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";
import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus";
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";
export interface IQuoteMapper
@ -86,6 +86,9 @@ class QuoteMapper
scale: 2,
})
),
taxLabel: this.mapsValue(source, "tax_label", (label) =>
TextValueObject.create(label ?? DEFAULT_QUOTE_TAX_LABEL)
),
/*totalPrice: this.mapsValue(source, "total_price", (total_price) =>
MoneyValue.create({
@ -137,6 +140,7 @@ class QuoteMapper
before_tax_price: source.beforeTaxPrice.convertScale(2).toPrimitive(),
tax: source.tax.convertScale(2).toPrimitive(),
tax_label: source.taxLabel.toPrimitive(),
tax_price: source.taxPrice.convertScale(2).toPrimitive(),
total_price: source.totalPrice.convertScale(2).toPrimitive(),

View File

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

View File

@ -9,6 +9,10 @@ import {
DEFAULT_TAX_LABEL,
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 { config } from "../../config";
import { app } from "../express/app";
@ -115,6 +119,10 @@ try {
{ default_tax_label: DEFAULT_TAX_LABEL },
{ where: { default_tax_label: null } }
);
await Quote_Model.update(
{ tax_label: DEFAULT_QUOTE_TAX_LABEL },
{ where: { tax_label: null } }
);
initStructure(sequelizeConn.connection);
insertUsers();

View File

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

View File

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

View File

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

View File

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

View File

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