diff --git a/client/src/app/settings/edit.tsx b/client/src/app/settings/edit.tsx index 8acd209..164520b 100644 --- a/client/src/app/settings/edit.tsx +++ b/client/src/app/settings/edit.tsx @@ -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 = () => {
+ + + + + + + + + + + ( + + )} + /> + + + + + diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 25babd6..b9ba7c9 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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": "", diff --git a/client/src/locales/es.json b/client/src/locales/es.json index d14ab18..a5fef53 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -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": "", diff --git a/server/src/contexts/profile/application/UpdateProfile.useCase.ts b/server/src/contexts/profile/application/UpdateProfile.useCase.ts index f3961a0..bc31985 100644 --- a/server/src/contexts/profile/application/UpdateProfile.useCase.ts +++ b/server/src/contexts/profile/application/UpdateProfile.useCase.ts @@ -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) { diff --git a/server/src/contexts/profile/domain/entities/Profile.ts b/server/src/contexts/profile/domain/entities/Profile.ts index b6f8197..30c10d5 100644 --- a/server/src/contexts/profile/domain/entities/Profile.ts +++ b/server/src/contexts/profile/domain/entities/Profile.ts @@ -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 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; } diff --git a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts index b533a68..4785575 100644 --- a/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts +++ b/server/src/contexts/profile/infrastructure/express/controllers/getProfile/presenter/GetProfile.presenter.ts @@ -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(), }, diff --git a/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts index de747da..f1dc577 100644 --- a/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts +++ b/server/src/contexts/profile/infrastructure/express/controllers/updateProfile/presenter/UpdateUser.presenter.ts @@ -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, }; }, }; diff --git a/server/src/contexts/profile/infrastructure/mappers/profile.mapper.test.ts b/server/src/contexts/profile/infrastructure/mappers/profile.mapper.test.ts new file mode 100644 index 0000000..ff5e293 --- /dev/null +++ b/server/src/contexts/profile/infrastructure/mappers/profile.mapper.test.ts @@ -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); + }); +}); diff --git a/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts b/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts index e0e456c..8cdd2fe 100644 --- a/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts +++ b/server/src/contexts/profile/infrastructure/mappers/profile.mapper.ts @@ -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(), }; } diff --git a/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts b/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts index 3a0a578..d5365ac 100644 --- a/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts +++ b/server/src/contexts/sales/application/Quote/CreateQuote.useCase.ts @@ -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, diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.test.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.test.ts index cf944de..6fb4bf5 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.test.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.test.ts @@ -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"); }); }); diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts index 04ad929..cc1d683 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.ts @@ -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(), diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote/template.hbs b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote/template.hbs index 0374d69..84e5c68 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote/template.hbs +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/templates/quote/template.hbs @@ -119,6 +119,13 @@ {{subtotal_price}} + {{#if show_line_discount_column}} + + Descuentos + + {{line_discount_price}} + + {{/if}} {{#if has_general_discount}} % Descuento diff --git a/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts b/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts index 8a79460..875ea2c 100644 --- a/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts +++ b/server/src/contexts/sales/infrastructure/mappers/dealer.mapper.ts @@ -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(), }; diff --git a/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts b/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts index 0475d44..ea05583 100644 --- a/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts +++ b/server/src/contexts/sales/infrastructure/sequelize/dealer.model.ts @@ -53,6 +53,7 @@ export class Dealer_Model extends Model< declare default_notes: CreationOptional; declare default_legal_terms: CreationOptional; declare default_quote_validity: CreationOptional; + declare default_show_line_discount_column: CreationOptional; declare default_tax: CreationOptional; declare status: CreationOptional; declare lang_code: CreationOptional; @@ -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(), diff --git a/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts b/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts index 1593100..e16ab07 100644 --- a/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts +++ b/shared/lib/contexts/profile/application/dto/GetProfile.dto/IGetProfile_Response.dto.ts @@ -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; diff --git a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts index 3d3fc0a..9252f5f 100644 --- a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts +++ b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Request.dto.ts @@ -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); diff --git a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts index b568c88..6b81418 100644 --- a/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts +++ b/shared/lib/contexts/profile/application/dto/UpdateProfile.dto/IUpdateProfile_Response.dto.ts @@ -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; }