Añadir opción para mostrar la columna de descuento por defecto en las cotizaciones

This commit is contained in:
David Arranz 2026-10-03 15:14:49 +02:00
parent c46d2b0df7
commit 6f77eaad97
18 changed files with 121 additions and 3 deletions

View File

@ -13,12 +13,13 @@ import {
Form,
Input,
Label,
Switch,
} from "@/ui";
import { t } from "i18next";
import { AlertCircleIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { Trans } from "react-i18next";
import { useUnsavedChangesNotifier } from "@/lib/hooks";
@ -43,6 +44,7 @@ export const SettingsEditor = () => {
default_notes: "",
default_legal_terms: "",
default_quote_validity: "",
default_show_line_discount_column: true,
default_tax: {
amount: undefined,
scale: 2,
@ -251,6 +253,30 @@ export const SettingsEditor = () => {
</Card>
</div>
<div className={cn("grid gap-6", activeSection === "quotes" ? "visible" : "hidden")}>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey='settings.form_fields.default_show_line_discount_column.label' />
</CardTitle>
<CardDescription>
<Trans i18nKey='settings.form_fields.default_show_line_discount_column.desc' />
</CardDescription>
</CardHeader>
<CardContent>
<Controller
control={form.control}
name='default_show_line_discount_column'
render={({ field }) => (
<Switch checked={field.value ?? true} onCheckedChange={field.onChange} />
)}
/>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>
<Trans i18nKey='common.save' />
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>

View File

@ -448,6 +448,10 @@
"placeholder": "",
"desc": "Default tax rate for your quotes"
},
"default_show_line_discount_column": {
"label": "Show discount column by default",
"desc": "New quotes will initially show the discount column in their lines. You can change this option later for each quote."
},
"default_legal_terms": {
"label": "Legal terms",
"placeholder": "",

View File

@ -456,6 +456,10 @@
"placeholder": "",
"desc": "Porcentaje de IVA por defecto en las cotizaciones"
},
"default_show_line_discount_column": {
"label": "Mostrar columna descuento por defecto",
"desc": "Los nuevos presupuestos mostrarán inicialmente la columna de descuento en las líneas. Podrás cambiar esta opción posteriormente en cada presupuesto."
},
"default_legal_terms": {
"label": "Cláusulas legales",
"placeholder": "",

View File

@ -58,6 +58,8 @@ export class UpdateProfileUseCase
profile.defaultLegalTerms = TextValueObject.create(profileDTO.default_legal_terms).object;
profile.defaultNotes = TextValueObject.create(profileDTO.default_notes).object;
profile.defaultQuoteValidity = TextValueObject.create(profileDTO.default_quote_validity).object;
profile.defaultShowLineDiscountColumn =
profileDTO.default_show_line_discount_column ?? profile.defaultShowLineDiscountColumn;
const taxOrError = Percentage.create(profileDTO.default_tax);
if (taxOrError.isFailure) {

View File

@ -24,6 +24,7 @@ export interface IProfileProps {
defaultLegalTerms: Note;
defaultQuoteValidity: Note;
defaultTax: Percentage;
defaultShowLineDiscountColumn: boolean;
logo: ProfileLogotype;
}
@ -42,6 +43,7 @@ export interface IProfile {
defaultQuoteValidity: Note;
defaultTax: Percentage;
defaultShowLineDiscountColumn: boolean;
logo: ProfileLogotype;
}
@ -116,6 +118,14 @@ export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
this.props.defaultTax = newDefaultTax;
}
get defaultShowLineDiscountColumn(): boolean {
return this.props.defaultShowLineDiscountColumn;
}
set defaultShowLineDiscountColumn(value: boolean) {
this.props.defaultShowLineDiscountColumn = value;
}
get logo(): ProfileLogotype {
return this.props.logo;
}

View File

@ -36,6 +36,7 @@ export const GetProfilePresenter: IGetProfilePresenter = {
default_legal_terms: profile.defaultLegalTerms.toString(),
default_quote_validity: profile.defaultQuoteValidity.toString(),
default_tax: profile.defaultTax.convertScale(2).toObject(),
default_show_line_discount_column: profile.defaultShowLineDiscountColumn,
logo: await profile.logo.toBase64(),
},

View File

@ -16,6 +16,7 @@ export const UpdateProfilePresenter: IUpdateProfilePresenter = {
default_legal_terms: profile.defaultLegalTerms.toString(),
default_quote_validity: profile.defaultQuoteValidity.toString(),
default_tax: profile.defaultTax.convertScale(2).toObject(),
default_show_line_discount_column: profile.defaultShowLineDiscountColumn,
};
},
};

View File

@ -0,0 +1,36 @@
import { IProfileContext } from "../Profile.context";
import { createProfileMapper } from "./profile.mapper";
const source = (defaultShowLineDiscountColumn: boolean | null) =>
({
default_show_line_discount_column: defaultShowLineDiscountColumn,
dataValues: {
id: "770672b9-5a87-4e8e-a8af-8b44a7432e12",
name: "Dealer",
status: "actived",
lang_code: "es",
currency_code: "EUR",
logo: "",
contact_information: "",
default_payment_method: "",
default_notes: "",
default_legal_terms: "",
default_quote_validity: "",
default_tax: 2100,
},
}) as any;
describe("ProfileMapper default_show_line_discount_column", () => {
const mapper = createProfileMapper({} as IProfileContext);
it("maps false in both persistence directions", () => {
const profile = mapper.mapToDomain(source(false));
expect(profile.defaultShowLineDiscountColumn).toBe(false);
expect(mapper.mapToPersistence(profile).default_show_line_discount_column).toBe(false);
});
it("uses true for historical null values", () => {
expect(mapper.mapToDomain(source(null)).defaultShowLineDiscountColumn).toBe(true);
});
});

View File

@ -72,6 +72,7 @@ class ProfileMapper
defaultLegalTerms,
defaultQuoteValidity,
defaultTax,
defaultShowLineDiscountColumn: source.default_show_line_discount_column ?? true,
logo,
};
@ -96,6 +97,7 @@ class ProfileMapper
default_legal_terms: source.defaultLegalTerms.toPrimitive(),
default_quote_validity: source.defaultQuoteValidity.toPrimitive(),
default_tax: source.defaultTax.convertScale(2).toPrimitive(),
default_show_line_discount_column: source.defaultShowLineDiscountColumn,
logo: source.logo.toPrimitive(),
};
}

View File

@ -283,7 +283,10 @@ export class CreateQuoteUseCase
paymentMethod: paymentOrError.object,
notes: notesOrError.object,
validity: validityOrError.object,
showLineDiscountColumn: quoteDTO.show_line_discount_column ?? true,
showLineDiscountColumn:
quoteDTO.show_line_discount_column ??
(this._dealer.additionalInfo.get("default_show_line_discount_column")?.toString() !==
"false"),
discount: discountOrError.object,
tax: taxOrError.object,

View File

@ -33,6 +33,7 @@ describe("ReportQuotePresenter", () => {
const html = await ReportQuotePresenter.toHTML(mapper.mapToDomain(source(true, 10)), context);
expect(html).toContain("Dto (%)");
expect(html).toContain("Descuentos");
expect(html).toContain("% Descuento");
});
@ -40,6 +41,7 @@ describe("ReportQuotePresenter", () => {
const html = await ReportQuotePresenter.toHTML(mapper.mapToDomain(source(false, 0)), context);
expect(html).not.toContain("Dto (%)");
expect(html).not.toContain("Descuentos");
expect(html).not.toContain("% Descuento");
});
});

View File

@ -1,4 +1,4 @@
import { ICollection } from "@shared/contexts";
import { ICollection, MoneyValue } from "@shared/contexts";
import { Quote, QuoteItem } from "../../../../../../domain";
import { ISalesContext } from "../../../../../Sales.context";
@ -79,6 +79,13 @@ const map = async (quote: Quote, context: ISalesContext) => {
notes: quote.notes.toString(),
show_line_discount_column: quote.showLineDiscountColumn,
has_general_discount: quote.discount.toNumber() > 0,
line_discount_price: quote.items
.toArray()
.reduce(
(total, item) => total.add(item.subtotalPrice.subtract(item.totalPrice)),
MoneyValue.create({ amount: 0, scale: 2, currencyCode: quote.currency.code }).object
)
.toFormat(),
subtotal_price: quote.subtotalPrice.toFormat(),

View File

@ -119,6 +119,13 @@
<td class="px-4 py-2"></td>
<td class="px-4 py-2 text-right border">{{subtotal_price}}</td>
</tr>
{{#if show_line_discount_column}}
<tr class="border-b">
<td class="px-4 py-2">Descuentos</td>
<td class="px-4 py-2"></td>
<td class="px-4 py-2 text-right border">{{line_discount_price}}</td>
</tr>
{{/if}}
{{#if has_general_discount}}
<tr class="border-b">
<td class="px-4 py-2">% Descuento</td>

View File

@ -33,6 +33,7 @@ class DealerMapper
["default_notes", source.default_notes],
["default_legal_terms", source.default_legal_terms],
["default_quote_validity", source.default_quote_validity],
["default_show_line_discount_column", source.default_show_line_discount_column ?? true],
["default_tax", source.default_tax],
]);
@ -72,6 +73,8 @@ class DealerMapper
default_notes: source.additionalInfo.get("default_notes")?.toString() ?? "",
default_legal_terms: source.additionalInfo.get("default_legal_terms")?.toString() ?? "",
default_quote_validity: source.additionalInfo.get("default_quote_validity")?.toString() ?? "",
default_show_line_discount_column:
source.additionalInfo.get("default_show_line_discount_column")?.toString() !== "false",
default_tax: Number(source.additionalInfo.get("default_tax") ?? 0),
logo: source.logo.toPrimitive(),
};

View File

@ -53,6 +53,7 @@ export class Dealer_Model extends Model<
declare default_notes: CreationOptional<string>;
declare default_legal_terms: CreationOptional<string>;
declare default_quote_validity: CreationOptional<string>;
declare default_show_line_discount_column: CreationOptional<boolean>;
declare default_tax: CreationOptional<number | null>;
declare status: CreationOptional<string>;
declare lang_code: CreationOptional<string>;
@ -86,6 +87,11 @@ export default (sequelize: Sequelize) => {
default_notes: DataTypes.TEXT,
default_legal_terms: DataTypes.TEXT,
default_quote_validity: DataTypes.TEXT,
default_show_line_discount_column: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
default_tax: {
type: new DataTypes.SMALLINT(),

View File

@ -15,6 +15,7 @@ export interface IGetProfileResponse_DTO {
default_legal_terms: string;
default_quote_validity: string;
default_tax: IPercentage_DTO;
default_show_line_discount_column: boolean;
status: string;
lang_code: string;
currency_code: string;

View File

@ -8,6 +8,7 @@ export interface IUpdateProfile_Request_DTO {
default_legal_terms: string;
default_quote_validity: string;
default_tax: IPercentage_DTO;
default_show_line_discount_column?: boolean;
logo: string;
}
@ -22,6 +23,7 @@ export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_R
amount: Joi.number().allow(null),
scale: Joi.number(),
}).optional(),
default_show_line_discount_column: Joi.boolean(),
logo: Joi.string().optional().allow(null).allow("").default(""),
}).unknown(true);

View File

@ -8,4 +8,5 @@ export interface IUpdateProfileResponse_DTO {
default_legal_terms: string;
default_quote_validity: string;
default_tax: IPercentage_DTO;
default_show_line_discount_column: boolean;
}