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;
}