@zauru-sdk/common 6.4.1 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/common.d.ts +10 -0
- package/dist/esm/common.js +25 -10
- package/dist/esm/index.js +1 -0
- package/dist/esm/rejectionPercentageAdjustment.js +130 -0
- package/dist/index.d.ts +1 -0
- package/dist/rejectionPercentageAdjustment.d.ts +11 -0
- package/package.json +3 -3
- package/src/common.ts +44 -12
- package/src/index.ts +1 -0
- package/src/rejectionPercentageAdjustment.ts +211 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [6.6.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.5.0...@zauru-sdk/common@6.6.0) (2026-09-14)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* apply rejection percentage rules inside calculatePurchaseOrderFinancials ([e1852ba](https://github.com/intuitiva/zauru-typescript-sdk/commit/e1852ba86e8263df8a365adda884e6b88802a001))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# [6.5.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.4.1...@zauru-sdk/common@6.5.0) (2026-09-14)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* apply rejection percentage rules when saving PO discounts ([2b44998](https://github.com/intuitiva/zauru-typescript-sdk/commit/2b44998241e3b25d2a0d98a1c7f83a21b21cc265))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [6.4.1](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.4.0...@zauru-sdk/common@6.4.1) (2026-09-14)
|
|
7
29
|
|
|
8
30
|
**Note:** Version bump only for package @zauru-sdk/common
|
package/dist/common.d.ts
CHANGED
|
@@ -12,10 +12,20 @@ export declare const getRejectionPercentage: (source: string | {
|
|
|
12
12
|
memo?: string | object;
|
|
13
13
|
} | JsonMemoType | undefined) => number;
|
|
14
14
|
export declare const setRejectionPercentage: (memo: string | object | undefined, percentage: number) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Rejection % of origin stored in the memo, ignoring the adjustment rules
|
|
17
|
+
* already applied on top of it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const resolveRejectionPercentageBase: (memo?: string | object) => number;
|
|
15
20
|
/**
|
|
16
21
|
* Header money for a purchase order: subtotal = qty × unit_cost,
|
|
17
22
|
* discount = subtotal × rejectionPercentage / 100.
|
|
18
23
|
* Does not change unit cost. purchase_orders.discount is the monetary column.
|
|
24
|
+
*
|
|
25
|
+
* If `rules` is given, the rejection % is resolved back to the origin stored in
|
|
26
|
+
* the memo and the rules are stacked on top of it before calculating the money.
|
|
27
|
+
* `rejectionPercentage` in the result is the % that was actually charged and
|
|
28
|
+
* `rejectionCalculations` is the trace to store in the memo.
|
|
19
29
|
*/
|
|
20
30
|
export declare const calculatePurchaseOrderFinancials: (input: CalculatePurchaseOrderFinancialsInput) => CalculatePurchaseOrderFinancialsResult;
|
|
21
31
|
/**
|
package/dist/esm/common.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.parsedObject = exports.sortByProperty = exports.labFormPatter = exports.labServicePattern = exports.getRandomNum = exports.CURRENCY_PREFIX = exports.truncateDecimals = exports.ZAURU_REGEX = exports.priceToText = exports.arrayToObject = exports.isNumeric = exports.toFixedIfNeeded = exports.formatTimeToTimePicker = exports.formatDateToDatePicker = exports.getFormattedDate = exports.parsedBaculoFormValue = exports.getPayeeInfoOptions = exports.getPayeeInfoIdOptions = exports.getPayeeFormated = exports.getDateAfterDays = exports.getTimePickerCurrentTime = exports.obtenerFechaActualConZonaHoraria = exports.getDatePickerCurrentDate = exports.stringDateToParsedUTCDate = exports.localDateToUSDate = exports.getStringFullDate = exports.getTodayMinutesDifference = exports.getTodayDaysDifference = exports.getStringDate = exports.getZauruDateByText = exports.getNewDateByFormat = exports.getFechaJuliana = exports.getBasketsSchema = exports.calculatePurchaseOrderFinancials = exports.setRejectionPercentage = exports.getRejectionPercentage = exports.mergeJsonMemo = exports.stringifyJsonMemo = exports.parseJsonMemo = exports.DESTINOS_MUESTRA_OPTIONS = void 0;
|
|
6
|
+
exports.parsedObject = exports.sortByProperty = exports.labFormPatter = exports.labServicePattern = exports.getRandomNum = exports.CURRENCY_PREFIX = exports.truncateDecimals = exports.ZAURU_REGEX = exports.priceToText = exports.arrayToObject = exports.isNumeric = exports.toFixedIfNeeded = exports.formatTimeToTimePicker = exports.formatDateToDatePicker = exports.getFormattedDate = exports.parsedBaculoFormValue = exports.getPayeeInfoOptions = exports.getPayeeInfoIdOptions = exports.getPayeeFormated = exports.getDateAfterDays = exports.getTimePickerCurrentTime = exports.obtenerFechaActualConZonaHoraria = exports.getDatePickerCurrentDate = exports.stringDateToParsedUTCDate = exports.localDateToUSDate = exports.getStringFullDate = exports.getTodayMinutesDifference = exports.getTodayDaysDifference = exports.getStringDate = exports.getZauruDateByText = exports.getNewDateByFormat = exports.getFechaJuliana = exports.getBasketsSchema = exports.calculatePurchaseOrderFinancials = exports.resolveRejectionPercentageBase = exports.setRejectionPercentage = exports.getRejectionPercentage = exports.mergeJsonMemo = exports.stringifyJsonMemo = exports.parseJsonMemo = exports.DESTINOS_MUESTRA_OPTIONS = void 0;
|
|
7
7
|
exports.generateClientUUID = generateClientUUID;
|
|
8
8
|
exports.extractValueBetweenTags = extractValueBetweenTags;
|
|
9
9
|
exports.isJsonArray = isJsonArray;
|
|
@@ -24,6 +24,7 @@ exports.parseCode = parseCode;
|
|
|
24
24
|
const moment_1 = __importDefault(require("moment"));
|
|
25
25
|
require("moment-timezone");
|
|
26
26
|
const types_1 = require("@zauru-sdk/types");
|
|
27
|
+
const rejectionPercentageAdjustment_js_1 = require("./rejectionPercentageAdjustment.js");
|
|
27
28
|
exports.DESTINOS_MUESTRA_OPTIONS = [
|
|
28
29
|
{ label: "Microbiología", value: "microbiologa" },
|
|
29
30
|
{ label: "Residuos de plaguicidas", value: "residuos_de_plaguicidas" },
|
|
@@ -80,6 +81,12 @@ const getRejectionPercentage = (source) => {
|
|
|
80
81
|
exports.getRejectionPercentage = getRejectionPercentage;
|
|
81
82
|
const setRejectionPercentage = (memo, percentage) => (0, exports.mergeJsonMemo)(memo, { rejectionPercentage: percentage });
|
|
82
83
|
exports.setRejectionPercentage = setRejectionPercentage;
|
|
84
|
+
/**
|
|
85
|
+
* Rejection % of origin stored in the memo, ignoring the adjustment rules
|
|
86
|
+
* already applied on top of it.
|
|
87
|
+
*/
|
|
88
|
+
const resolveRejectionPercentageBase = (memo) => (0, rejectionPercentageAdjustment_js_1.resolveRejectionPercentageOrigin)((0, exports.getRejectionPercentage)(memo), (0, exports.parseJsonMemo)(memo).rejectionCalculations);
|
|
89
|
+
exports.resolveRejectionPercentageBase = resolveRejectionPercentageBase;
|
|
83
90
|
const roundMoney = (value) => Math.round((value + Number.EPSILON) * 100) / 100;
|
|
84
91
|
const lineQuantity = (detail) => {
|
|
85
92
|
const delivered = toFiniteNumber(detail.delivered_quantity) ??
|
|
@@ -95,6 +102,11 @@ const lineQuantity = (detail) => {
|
|
|
95
102
|
* Header money for a purchase order: subtotal = qty × unit_cost,
|
|
96
103
|
* discount = subtotal × rejectionPercentage / 100.
|
|
97
104
|
* Does not change unit cost. purchase_orders.discount is the monetary column.
|
|
105
|
+
*
|
|
106
|
+
* If `rules` is given, the rejection % is resolved back to the origin stored in
|
|
107
|
+
* the memo and the rules are stacked on top of it before calculating the money.
|
|
108
|
+
* `rejectionPercentage` in the result is the % that was actually charged and
|
|
109
|
+
* `rejectionCalculations` is the trace to store in the memo.
|
|
98
110
|
*/
|
|
99
111
|
const calculatePurchaseOrderFinancials = (input) => {
|
|
100
112
|
const details = input.details ?? [];
|
|
@@ -102,16 +114,19 @@ const calculatePurchaseOrderFinancials = (input) => {
|
|
|
102
114
|
const unitCost = toFiniteNumber(detail.unit_cost) ?? 0;
|
|
103
115
|
return sum + lineQuantity(detail) * unitCost;
|
|
104
116
|
}, 0));
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
117
|
+
const safeSubtotal = Number.isFinite(subtotal) ? subtotal : 0;
|
|
118
|
+
const passedPercentage = toFiniteNumber(input.rejectionPercentage) ?? 0;
|
|
119
|
+
const rejectionCalculations = input.rules
|
|
120
|
+
? (0, rejectionPercentageAdjustment_js_1.applyRejectionPercentageAdjustmentRules)((0, rejectionPercentageAdjustment_js_1.resolveRejectionPercentageOrigin)(passedPercentage, (0, exports.parseJsonMemo)(input.memo).rejectionCalculations), input.rules, input.ctx ?? { itemIds: [] })
|
|
121
|
+
: undefined;
|
|
122
|
+
const rejectionPercentage = rejectionCalculations?.finalPercentage ?? passedPercentage;
|
|
112
123
|
return {
|
|
113
|
-
subtotal,
|
|
114
|
-
discount:
|
|
124
|
+
subtotal: safeSubtotal,
|
|
125
|
+
discount: rejectionPercentage > 0 && safeSubtotal > 0
|
|
126
|
+
? roundMoney(safeSubtotal * (rejectionPercentage / 100))
|
|
127
|
+
: 0,
|
|
128
|
+
rejectionPercentage,
|
|
129
|
+
rejectionCalculations,
|
|
115
130
|
};
|
|
116
131
|
};
|
|
117
132
|
exports.calculatePurchaseOrderFinancials = calculatePurchaseOrderFinancials;
|
package/dist/esm/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./common.js"), exports);
|
|
18
18
|
__exportStar(require("./gtZones.js"), exports);
|
|
19
|
+
__exportStar(require("./rejectionPercentageAdjustment.js"), exports);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyRejectionPercentageAdjustmentRules = exports.resolveRejectionPercentageOrigin = exports.formatRejectionPercentageAdjustmentDescription = exports.rejectionPercentageAdjustmentRuleMatches = exports.filterActiveRejectionPercentageAdjustmentRules = exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = void 0;
|
|
4
|
+
exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = "rejection_percentage_adjustment_rules_web_app_table_id";
|
|
5
|
+
const defaultFilters = () => ({
|
|
6
|
+
itemMode: "all",
|
|
7
|
+
itemIds: [],
|
|
8
|
+
tipoMode: "all",
|
|
9
|
+
tipos: [],
|
|
10
|
+
programaMode: "all",
|
|
11
|
+
providerCategoryIds: [],
|
|
12
|
+
});
|
|
13
|
+
const normalizeComparableValue = (value) => String(value ?? "").replace(/\s+/g, " ").trim();
|
|
14
|
+
const matchesFilter = (mode, selected, value) => {
|
|
15
|
+
const normalizedMode = mode ?? "all";
|
|
16
|
+
const values = selected ?? [];
|
|
17
|
+
if (normalizedMode === "all") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (value === undefined || value === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const asString = normalizeComparableValue(value);
|
|
24
|
+
if (!asString) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const included = values.some((item) => normalizeComparableValue(item) === asString);
|
|
28
|
+
if (normalizedMode === "include") {
|
|
29
|
+
return values.length > 0 && included;
|
|
30
|
+
}
|
|
31
|
+
return !included;
|
|
32
|
+
};
|
|
33
|
+
const filterActiveRejectionPercentageAdjustmentRules = (rules) => (rules ?? [])
|
|
34
|
+
.filter((rule) => !rule.data?.fechaEliminacion && rule.data?.activa !== false)
|
|
35
|
+
.sort((a, b) => (a.data?.prioridad ?? 0) - (b.data?.prioridad ?? 0) || a.id - b.id);
|
|
36
|
+
exports.filterActiveRejectionPercentageAdjustmentRules = filterActiveRejectionPercentageAdjustmentRules;
|
|
37
|
+
const rejectionPercentageAdjustmentRuleMatches = (rule, ctx) => {
|
|
38
|
+
const filtros = rule.filtros ?? defaultFilters();
|
|
39
|
+
const itemIds = ctx.itemIds ?? [];
|
|
40
|
+
const tipoAndPrograma = matchesFilter(filtros.tipoMode, filtros.tipos, ctx.tipo) &&
|
|
41
|
+
matchesFilter(filtros.programaMode, filtros.providerCategoryIds, ctx.providerCategoryId);
|
|
42
|
+
if (!tipoAndPrograma) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (filtros.itemMode === "all") {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (itemIds.length === 0) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return itemIds.some((itemId) => matchesFilter(filtros.itemMode, filtros.itemIds, itemId));
|
|
52
|
+
};
|
|
53
|
+
exports.rejectionPercentageAdjustmentRuleMatches = rejectionPercentageAdjustmentRuleMatches;
|
|
54
|
+
const roundPercentage = (value, digits = 4) => {
|
|
55
|
+
const factor = 10 ** digits;
|
|
56
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
57
|
+
};
|
|
58
|
+
const clampPercentage = (value) => Math.min(100, Math.max(0, value));
|
|
59
|
+
const formatPercentage = (value) => Number.isInteger(value) ? String(value) : String(roundPercentage(value));
|
|
60
|
+
const formatFinalPercentage = (value) => roundPercentage(value, 2).toFixed(2);
|
|
61
|
+
const formatRuleEffect = (step) => {
|
|
62
|
+
const sign = step.operation === "subtract" ? "-" : "+";
|
|
63
|
+
return `${sign} ${formatPercentage(step.value)}% (${step.ruleName})`;
|
|
64
|
+
};
|
|
65
|
+
const formatRejectionPercentageAdjustmentDescription = (result, baseLabel = "origen") => {
|
|
66
|
+
const base = `${formatPercentage(result.basePercentage)} (${baseLabel})`;
|
|
67
|
+
if (result.steps.length === 0) {
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
return `${base} ${result.steps.map(formatRuleEffect).join(" ")} = ${formatFinalPercentage(result.finalPercentage)}`;
|
|
71
|
+
};
|
|
72
|
+
exports.formatRejectionPercentageAdjustmentDescription = formatRejectionPercentageAdjustmentDescription;
|
|
73
|
+
/**
|
|
74
|
+
* Callers may pass the origin % or the previously stored final %. With the
|
|
75
|
+
* previous calculations we go back to the origin so the rules never stack twice.
|
|
76
|
+
*/
|
|
77
|
+
const resolveRejectionPercentageOrigin = (passedPercentage, previousCalculations) => {
|
|
78
|
+
const passed = clampPercentage(Number.isFinite(Number(passedPercentage)) ? Number(passedPercentage) : 0);
|
|
79
|
+
const calc = previousCalculations;
|
|
80
|
+
if (!calc || !Number.isFinite(Number(calc.finalPercentage))) {
|
|
81
|
+
return passed;
|
|
82
|
+
}
|
|
83
|
+
const previousFinal = roundPercentage(Number(calc.finalPercentage), 2);
|
|
84
|
+
const previousBase = Number(calc.basePercentage);
|
|
85
|
+
const safePreviousBase = Number.isFinite(previousBase)
|
|
86
|
+
? clampPercentage(previousBase)
|
|
87
|
+
: passed;
|
|
88
|
+
if (roundPercentage(passed, 2) === previousFinal) {
|
|
89
|
+
return safePreviousBase;
|
|
90
|
+
}
|
|
91
|
+
return clampPercentage(safePreviousBase + (passed - previousFinal));
|
|
92
|
+
};
|
|
93
|
+
exports.resolveRejectionPercentageOrigin = resolveRejectionPercentageOrigin;
|
|
94
|
+
const applyRejectionPercentageAdjustmentRules = (basePercentage, rules, ctx) => {
|
|
95
|
+
const start = Number(basePercentage);
|
|
96
|
+
const safeBase = Number.isFinite(start) ? clampPercentage(start) : 0;
|
|
97
|
+
let runningTotal = safeBase;
|
|
98
|
+
const steps = [];
|
|
99
|
+
const normalized = rules
|
|
100
|
+
.map((rule) => ("data" in rule ? rule.data : rule))
|
|
101
|
+
.filter((rule) => Boolean(rule))
|
|
102
|
+
.filter((rule) => !rule.fechaEliminacion && rule.activa !== false)
|
|
103
|
+
.sort((a, b) => (a.prioridad ?? 0) - (b.prioridad ?? 0));
|
|
104
|
+
for (const rule of normalized) {
|
|
105
|
+
if (!(0, exports.rejectionPercentageAdjustmentRuleMatches)(rule, ctx)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const value = Number(rule.valor) || 0;
|
|
109
|
+
const previous = runningTotal;
|
|
110
|
+
runningTotal =
|
|
111
|
+
rule.operacion === "subtract" ? previous - value : previous + value;
|
|
112
|
+
runningTotal = clampPercentage(roundPercentage(runningTotal));
|
|
113
|
+
steps.push({
|
|
114
|
+
ruleName: rule.nombre,
|
|
115
|
+
operation: rule.operacion,
|
|
116
|
+
value,
|
|
117
|
+
appliedAmount: roundPercentage(runningTotal - previous),
|
|
118
|
+
runningTotal,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const result = {
|
|
122
|
+
basePercentage: roundPercentage(safeBase),
|
|
123
|
+
finalPercentage: roundPercentage(runningTotal, 2),
|
|
124
|
+
description: "",
|
|
125
|
+
steps,
|
|
126
|
+
};
|
|
127
|
+
result.description = (0, exports.formatRejectionPercentageAdjustmentDescription)(result);
|
|
128
|
+
return result;
|
|
129
|
+
};
|
|
130
|
+
exports.applyRejectionPercentageAdjustmentRules = applyRejectionPercentageAdjustmentRules;
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RejectionCalculationMemo, RejectionPercentageAdjustmentContext, RejectionPercentageAdjustmentResult, RejectionPercentageAdjustmentRule, WebAppRowGraphQL } from "@zauru-sdk/types";
|
|
2
|
+
export declare const REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = "rejection_percentage_adjustment_rules_web_app_table_id";
|
|
3
|
+
export declare const filterActiveRejectionPercentageAdjustmentRules: (rules?: WebAppRowGraphQL<RejectionPercentageAdjustmentRule>[]) => WebAppRowGraphQL<RejectionPercentageAdjustmentRule>[];
|
|
4
|
+
export declare const rejectionPercentageAdjustmentRuleMatches: (rule: RejectionPercentageAdjustmentRule, ctx: RejectionPercentageAdjustmentContext) => boolean;
|
|
5
|
+
export declare const formatRejectionPercentageAdjustmentDescription: (result: Pick<RejectionPercentageAdjustmentResult, "basePercentage" | "finalPercentage" | "steps">, baseLabel?: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Callers may pass the origin % or the previously stored final %. With the
|
|
8
|
+
* previous calculations we go back to the origin so the rules never stack twice.
|
|
9
|
+
*/
|
|
10
|
+
export declare const resolveRejectionPercentageOrigin: (passedPercentage: number, previousCalculations?: RejectionCalculationMemo | null) => number;
|
|
11
|
+
export declare const applyRejectionPercentageAdjustmentRules: (basePercentage: number, rules: Array<RejectionPercentageAdjustmentRule | WebAppRowGraphQL<RejectionPercentageAdjustmentRule>>, ctx: RejectionPercentageAdjustmentContext) => RejectionPercentageAdjustmentResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/common",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.0",
|
|
4
4
|
"description": "Funciones de uso común en el desarrollo de webapps en Zauru",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@remix-run/node": "^2.8.1",
|
|
24
|
-
"@zauru-sdk/types": "^6.
|
|
24
|
+
"@zauru-sdk/types": "^6.16.0",
|
|
25
25
|
"moment": "^2.30.1",
|
|
26
26
|
"moment-timezone": "^0.5.45"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "9e840d01a540f2ab49233dda2cb2b0c916ffba0b"
|
|
29
29
|
}
|
package/src/common.ts
CHANGED
|
@@ -12,6 +12,10 @@ import type {
|
|
|
12
12
|
PurchaseOrderFinancialDetail,
|
|
13
13
|
} from "@zauru-sdk/types";
|
|
14
14
|
import { MONTHS } from "@zauru-sdk/types";
|
|
15
|
+
import {
|
|
16
|
+
applyRejectionPercentageAdjustmentRules,
|
|
17
|
+
resolveRejectionPercentageOrigin,
|
|
18
|
+
} from "./rejectionPercentageAdjustment.js";
|
|
15
19
|
|
|
16
20
|
export const DESTINOS_MUESTRA_OPTIONS: SelectFieldOption[] = [
|
|
17
21
|
{ label: "Microbiología", value: "microbiologa" },
|
|
@@ -85,6 +89,18 @@ export const setRejectionPercentage = (
|
|
|
85
89
|
percentage: number,
|
|
86
90
|
): string => mergeJsonMemo(memo, { rejectionPercentage: percentage });
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Rejection % of origin stored in the memo, ignoring the adjustment rules
|
|
94
|
+
* already applied on top of it.
|
|
95
|
+
*/
|
|
96
|
+
export const resolveRejectionPercentageBase = (
|
|
97
|
+
memo?: string | object,
|
|
98
|
+
): number =>
|
|
99
|
+
resolveRejectionPercentageOrigin(
|
|
100
|
+
getRejectionPercentage(memo),
|
|
101
|
+
parseJsonMemo(memo).rejectionCalculations,
|
|
102
|
+
);
|
|
103
|
+
|
|
88
104
|
const roundMoney = (value: number): number =>
|
|
89
105
|
Math.round((value + Number.EPSILON) * 100) / 100;
|
|
90
106
|
|
|
@@ -106,6 +122,11 @@ const lineQuantity = (detail: PurchaseOrderFinancialDetail): number => {
|
|
|
106
122
|
* Header money for a purchase order: subtotal = qty × unit_cost,
|
|
107
123
|
* discount = subtotal × rejectionPercentage / 100.
|
|
108
124
|
* Does not change unit cost. purchase_orders.discount is the monetary column.
|
|
125
|
+
*
|
|
126
|
+
* If `rules` is given, the rejection % is resolved back to the origin stored in
|
|
127
|
+
* the memo and the rules are stacked on top of it before calculating the money.
|
|
128
|
+
* `rejectionPercentage` in the result is the % that was actually charged and
|
|
129
|
+
* `rejectionCalculations` is the trace to store in the memo.
|
|
109
130
|
*/
|
|
110
131
|
export const calculatePurchaseOrderFinancials = (
|
|
111
132
|
input: CalculatePurchaseOrderFinancialsInput,
|
|
@@ -117,20 +138,31 @@ export const calculatePurchaseOrderFinancials = (
|
|
|
117
138
|
return sum + lineQuantity(detail) * unitCost;
|
|
118
139
|
}, 0),
|
|
119
140
|
);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
const safeSubtotal = Number.isFinite(subtotal) ? subtotal : 0;
|
|
142
|
+
|
|
143
|
+
const passedPercentage = toFiniteNumber(input.rejectionPercentage) ?? 0;
|
|
144
|
+
const rejectionCalculations = input.rules
|
|
145
|
+
? applyRejectionPercentageAdjustmentRules(
|
|
146
|
+
resolveRejectionPercentageOrigin(
|
|
147
|
+
passedPercentage,
|
|
148
|
+
parseJsonMemo(input.memo).rejectionCalculations,
|
|
149
|
+
),
|
|
150
|
+
input.rules,
|
|
151
|
+
input.ctx ?? { itemIds: [] },
|
|
152
|
+
)
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
155
|
+
const rejectionPercentage =
|
|
156
|
+
rejectionCalculations?.finalPercentage ?? passedPercentage;
|
|
130
157
|
|
|
131
158
|
return {
|
|
132
|
-
subtotal,
|
|
133
|
-
discount:
|
|
159
|
+
subtotal: safeSubtotal,
|
|
160
|
+
discount:
|
|
161
|
+
rejectionPercentage > 0 && safeSubtotal > 0
|
|
162
|
+
? roundMoney(safeSubtotal * (rejectionPercentage / 100))
|
|
163
|
+
: 0,
|
|
164
|
+
rejectionPercentage,
|
|
165
|
+
rejectionCalculations,
|
|
134
166
|
};
|
|
135
167
|
};
|
|
136
168
|
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PriceAdjustmentFilterMode,
|
|
3
|
+
PriceAdjustmentFilters,
|
|
4
|
+
RejectionCalculationMemo,
|
|
5
|
+
RejectionPercentageAdjustmentContext,
|
|
6
|
+
RejectionPercentageAdjustmentResult,
|
|
7
|
+
RejectionPercentageAdjustmentRule,
|
|
8
|
+
RejectionPercentageAdjustmentStep,
|
|
9
|
+
WebAppRowGraphQL,
|
|
10
|
+
} from "@zauru-sdk/types";
|
|
11
|
+
|
|
12
|
+
export const REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR =
|
|
13
|
+
"rejection_percentage_adjustment_rules_web_app_table_id";
|
|
14
|
+
|
|
15
|
+
const defaultFilters = (): PriceAdjustmentFilters => ({
|
|
16
|
+
itemMode: "all",
|
|
17
|
+
itemIds: [],
|
|
18
|
+
tipoMode: "all",
|
|
19
|
+
tipos: [],
|
|
20
|
+
programaMode: "all",
|
|
21
|
+
providerCategoryIds: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const normalizeComparableValue = (
|
|
25
|
+
value: string | number | undefined | null,
|
|
26
|
+
): string => String(value ?? "").replace(/\s+/g, " ").trim();
|
|
27
|
+
|
|
28
|
+
const matchesFilter = (
|
|
29
|
+
mode: PriceAdjustmentFilterMode | undefined,
|
|
30
|
+
selected: Array<string | number> | undefined,
|
|
31
|
+
value: string | number | undefined,
|
|
32
|
+
): boolean => {
|
|
33
|
+
const normalizedMode = mode ?? "all";
|
|
34
|
+
const values = selected ?? [];
|
|
35
|
+
|
|
36
|
+
if (normalizedMode === "all") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value === undefined || value === null) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const asString = normalizeComparableValue(value);
|
|
45
|
+
if (!asString) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const included = values.some(
|
|
50
|
+
(item) => normalizeComparableValue(item) === asString,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (normalizedMode === "include") {
|
|
54
|
+
return values.length > 0 && included;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return !included;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const filterActiveRejectionPercentageAdjustmentRules = (
|
|
61
|
+
rules?: WebAppRowGraphQL<RejectionPercentageAdjustmentRule>[],
|
|
62
|
+
) =>
|
|
63
|
+
(rules ?? [])
|
|
64
|
+
.filter(
|
|
65
|
+
(rule) => !rule.data?.fechaEliminacion && rule.data?.activa !== false,
|
|
66
|
+
)
|
|
67
|
+
.sort(
|
|
68
|
+
(a, b) =>
|
|
69
|
+
(a.data?.prioridad ?? 0) - (b.data?.prioridad ?? 0) || a.id - b.id,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const rejectionPercentageAdjustmentRuleMatches = (
|
|
73
|
+
rule: RejectionPercentageAdjustmentRule,
|
|
74
|
+
ctx: RejectionPercentageAdjustmentContext,
|
|
75
|
+
): boolean => {
|
|
76
|
+
const filtros = rule.filtros ?? defaultFilters();
|
|
77
|
+
const itemIds = ctx.itemIds ?? [];
|
|
78
|
+
|
|
79
|
+
const tipoAndPrograma =
|
|
80
|
+
matchesFilter(filtros.tipoMode, filtros.tipos, ctx.tipo) &&
|
|
81
|
+
matchesFilter(
|
|
82
|
+
filtros.programaMode,
|
|
83
|
+
filtros.providerCategoryIds,
|
|
84
|
+
ctx.providerCategoryId,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!tipoAndPrograma) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (filtros.itemMode === "all") {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (itemIds.length === 0) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return itemIds.some((itemId) =>
|
|
100
|
+
matchesFilter(filtros.itemMode, filtros.itemIds, itemId),
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const roundPercentage = (value: number, digits = 4) => {
|
|
105
|
+
const factor = 10 ** digits;
|
|
106
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const clampPercentage = (value: number) => Math.min(100, Math.max(0, value));
|
|
110
|
+
|
|
111
|
+
const formatPercentage = (value: number) =>
|
|
112
|
+
Number.isInteger(value) ? String(value) : String(roundPercentage(value));
|
|
113
|
+
|
|
114
|
+
const formatFinalPercentage = (value: number) =>
|
|
115
|
+
roundPercentage(value, 2).toFixed(2);
|
|
116
|
+
|
|
117
|
+
const formatRuleEffect = (step: RejectionPercentageAdjustmentStep) => {
|
|
118
|
+
const sign = step.operation === "subtract" ? "-" : "+";
|
|
119
|
+
return `${sign} ${formatPercentage(step.value)}% (${step.ruleName})`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const formatRejectionPercentageAdjustmentDescription = (
|
|
123
|
+
result: Pick<
|
|
124
|
+
RejectionPercentageAdjustmentResult,
|
|
125
|
+
"basePercentage" | "finalPercentage" | "steps"
|
|
126
|
+
>,
|
|
127
|
+
baseLabel = "origen",
|
|
128
|
+
): string => {
|
|
129
|
+
const base = `${formatPercentage(result.basePercentage)} (${baseLabel})`;
|
|
130
|
+
if (result.steps.length === 0) {
|
|
131
|
+
return base;
|
|
132
|
+
}
|
|
133
|
+
return `${base} ${result.steps.map(formatRuleEffect).join(" ")} = ${formatFinalPercentage(result.finalPercentage)}`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Callers may pass the origin % or the previously stored final %. With the
|
|
138
|
+
* previous calculations we go back to the origin so the rules never stack twice.
|
|
139
|
+
*/
|
|
140
|
+
export const resolveRejectionPercentageOrigin = (
|
|
141
|
+
passedPercentage: number,
|
|
142
|
+
previousCalculations?: RejectionCalculationMemo | null,
|
|
143
|
+
): number => {
|
|
144
|
+
const passed = clampPercentage(
|
|
145
|
+
Number.isFinite(Number(passedPercentage)) ? Number(passedPercentage) : 0,
|
|
146
|
+
);
|
|
147
|
+
const calc = previousCalculations;
|
|
148
|
+
if (!calc || !Number.isFinite(Number(calc.finalPercentage))) {
|
|
149
|
+
return passed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const previousFinal = roundPercentage(Number(calc.finalPercentage), 2);
|
|
153
|
+
const previousBase = Number(calc.basePercentage);
|
|
154
|
+
const safePreviousBase = Number.isFinite(previousBase)
|
|
155
|
+
? clampPercentage(previousBase)
|
|
156
|
+
: passed;
|
|
157
|
+
|
|
158
|
+
if (roundPercentage(passed, 2) === previousFinal) {
|
|
159
|
+
return safePreviousBase;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return clampPercentage(safePreviousBase + (passed - previousFinal));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const applyRejectionPercentageAdjustmentRules = (
|
|
166
|
+
basePercentage: number,
|
|
167
|
+
rules: Array<
|
|
168
|
+
| RejectionPercentageAdjustmentRule
|
|
169
|
+
| WebAppRowGraphQL<RejectionPercentageAdjustmentRule>
|
|
170
|
+
>,
|
|
171
|
+
ctx: RejectionPercentageAdjustmentContext,
|
|
172
|
+
): RejectionPercentageAdjustmentResult => {
|
|
173
|
+
const start = Number(basePercentage);
|
|
174
|
+
const safeBase = Number.isFinite(start) ? clampPercentage(start) : 0;
|
|
175
|
+
let runningTotal = safeBase;
|
|
176
|
+
const steps: RejectionPercentageAdjustmentStep[] = [];
|
|
177
|
+
|
|
178
|
+
const normalized = rules
|
|
179
|
+
.map((rule) => ("data" in rule ? rule.data : rule))
|
|
180
|
+
.filter((rule): rule is RejectionPercentageAdjustmentRule => Boolean(rule))
|
|
181
|
+
.filter((rule) => !rule.fechaEliminacion && rule.activa !== false)
|
|
182
|
+
.sort((a, b) => (a.prioridad ?? 0) - (b.prioridad ?? 0));
|
|
183
|
+
|
|
184
|
+
for (const rule of normalized) {
|
|
185
|
+
if (!rejectionPercentageAdjustmentRuleMatches(rule, ctx)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const value = Number(rule.valor) || 0;
|
|
190
|
+
const previous = runningTotal;
|
|
191
|
+
runningTotal =
|
|
192
|
+
rule.operacion === "subtract" ? previous - value : previous + value;
|
|
193
|
+
runningTotal = clampPercentage(roundPercentage(runningTotal));
|
|
194
|
+
steps.push({
|
|
195
|
+
ruleName: rule.nombre,
|
|
196
|
+
operation: rule.operacion,
|
|
197
|
+
value,
|
|
198
|
+
appliedAmount: roundPercentage(runningTotal - previous),
|
|
199
|
+
runningTotal,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result: RejectionPercentageAdjustmentResult = {
|
|
204
|
+
basePercentage: roundPercentage(safeBase),
|
|
205
|
+
finalPercentage: roundPercentage(runningTotal, 2),
|
|
206
|
+
description: "",
|
|
207
|
+
steps,
|
|
208
|
+
};
|
|
209
|
+
result.description = formatRejectionPercentageAdjustmentDescription(result);
|
|
210
|
+
return result;
|
|
211
|
+
};
|