@metron-labs/quote-react 0.1.58 → 0.1.60
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/dist/app/stages/ScheduleResultsStage.js +2 -11
- package/dist/components/estimators/BrickBlockEstimator.js +19 -5
- package/dist/components/estimators/DeckingEstimator.js +1 -1
- package/dist/components/estimators/EstimatorResults.js +2 -2
- package/dist/components/estimators/FencingEstimator.js +3 -2
- package/dist/components/estimators/InsulationEstimator.js +11 -12
- package/dist/embed/quote.js +11 -11
- package/dist/lib/estimators/fixedProducts.js +4 -4
- package/dist/lib/estimators/requestUnit.d.ts +12 -0
- package/dist/lib/estimators/requestUnit.js +28 -0
- package/dist/lib/estimators/resolveProducts.d.ts +1 -7
- package/dist/lib/estimators/resolveProducts.js +2 -14
- package/dist/util/pricing.js +26 -3
- package/package.json +1 -1
|
@@ -10,23 +10,23 @@ import { getQuoteConfig } from "../config";
|
|
|
10
10
|
const FIXED_PRODUCTS = {
|
|
11
11
|
// FORT
|
|
12
12
|
"NZ4Y-ZMSQ": {
|
|
13
|
-
"brick-block": { "brick-common": "105499", "brick-engineering-red": "1654", "brick-engineering-blue": "1723", block: "501", mortar: "
|
|
13
|
+
"brick-block": { "brick-common": "105499", "brick-engineering-red": "1654", "brick-engineering-blue": "1723", block: "501", "mortar-cement": "1762 cement", "mortar-sand": "564 building sand" },
|
|
14
14
|
// Primed MDF, 4.4m lengths. Ogee and torus come in 119mm skirting / 69mm architrave; the other
|
|
15
15
|
// profiles only in 94mm and 144mm, so the nearer usable width is taken. FORT has no ovolo.
|
|
16
16
|
skirting: { "skirting-chamfered": "1525", "skirting-ogee": "135 primed mdf ogee", "skirting-pencil-round": "892", "skirting-torus": "1941" },
|
|
17
17
|
architrave: { "architrave-ogee": "134 primed mdf ogee", "architrave-ovolo": "1531", "architrave-pencil-round": "1896", "architrave-square-edge": "118 primed mdf square", "architrave-torus": "1531" },
|
|
18
18
|
// Board keys come from the board type; see fixedProductSearch.
|
|
19
19
|
plasterboard: { standard: "1647", moisture: "11 moisture plasterboard", fire: "14 fire plasterboard", sound: "1769", screws: "131160", tape: "1766", adhesive: "108439" },
|
|
20
|
-
insulation: { "insulation-floor": "152187", "insulation-wall": "147", "insulation-roof": "442", "insulation-loft": "152180" },
|
|
20
|
+
insulation: { "insulation-floor": "152187", "insulation-ground": "442", "insulation-wall": "147", "insulation-roof": "442", "insulation-loft": "152180" },
|
|
21
21
|
timber: { studs: "1213", joists: "208", battens: "1729" },
|
|
22
22
|
fencing: {
|
|
23
23
|
"panels-boundary-0.9": "66 lap fence panel", "panels-boundary-1.2": "1450", "panels-boundary-1.5": "1449", "panels-boundary-1.8": "1448",
|
|
24
24
|
"panels-skyline-1.8": "1475",
|
|
25
25
|
"concrete-intermediate-8": "40197", "concrete-end-8": "40198",
|
|
26
|
-
"timber-post-6": "141411", "timber-post-8": "1709", postcrete: "12142", gate: "1984", "gravel-boards": "40200",
|
|
26
|
+
"timber-post-6": "141411", "timber-post-8": "1709", postcrete: "12142", "gate-boundary": "1984", "gate-skyline": "40002", "gravel-boards": "40200",
|
|
27
27
|
},
|
|
28
28
|
decking: { "boards-summit": "24322", joists: "208", screws: "131079" },
|
|
29
|
-
paving: { "surface-council": "
|
|
29
|
+
paving: { "surface-council": "1669", "surface-raj": "12363", "surface-brindle": "304", "surface-charcoal": "296", "surface-pea": "431", "sub-base": "569", bedding: "563" },
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
/** The catalogue search for this line's fixed product, or null when the active client has none. */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MatchCandidate } from "../../util/types";
|
|
2
|
+
/**
|
|
3
|
+
* The unit to ask the basket for, given the product chosen for an estimator line. Only for
|
|
4
|
+
* estimator lines: a customer's "3 rolls" matched to a 2-roll pack is a different question.
|
|
5
|
+
* - A bag/box/roll/pack count is already a count of the product's packs - the estimator did that
|
|
6
|
+
* sum - so it is asked as listed-price units ("sales_unit") where the catalogue supports them,
|
|
7
|
+
* otherwise in the product's own unit when that product is sold one item at a time.
|
|
8
|
+
* - An "each" count of whole items (a gate, a panel) is asked as listed-price units only when one
|
|
9
|
+
* listed-price unit is exactly one item, so 600 bricks priced per 1,000 stays 600 each.
|
|
10
|
+
* 3 bags must never become 3 tonnes or 3 x 1000.
|
|
11
|
+
*/
|
|
12
|
+
export declare function estimateRequestUnit(unit: string, candidate: MatchCandidate | null | undefined): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Kept free of runtime imports so plain Node can load it (Boq's quickOrder.check.ts does).
|
|
2
|
+
const PACK_UNITS = new Set(["bag", "box", "roll", "pack"]);
|
|
3
|
+
const SINGLE_ITEM_UNITS = new Set(["each", "bag", "box", "roll", "pack", "tub"]);
|
|
4
|
+
/**
|
|
5
|
+
* The unit to ask the basket for, given the product chosen for an estimator line. Only for
|
|
6
|
+
* estimator lines: a customer's "3 rolls" matched to a 2-roll pack is a different question.
|
|
7
|
+
* - A bag/box/roll/pack count is already a count of the product's packs - the estimator did that
|
|
8
|
+
* sum - so it is asked as listed-price units ("sales_unit") where the catalogue supports them,
|
|
9
|
+
* otherwise in the product's own unit when that product is sold one item at a time.
|
|
10
|
+
* - An "each" count of whole items (a gate, a panel) is asked as listed-price units only when one
|
|
11
|
+
* listed-price unit is exactly one item, so 600 bricks priced per 1,000 stays 600 each.
|
|
12
|
+
* 3 bags must never become 3 tonnes or 3 x 1000.
|
|
13
|
+
*/
|
|
14
|
+
export function estimateRequestUnit(unit, candidate) {
|
|
15
|
+
const supported = (candidate?.conversion?.request_uoms_supported ?? "").split(",").map((entry) => entry.trim().toLowerCase());
|
|
16
|
+
const salesUnits = supported.includes("sales_unit");
|
|
17
|
+
if (unit === "each") {
|
|
18
|
+
const oneItemPerPrice = (candidate?.uoi_count ?? 1) === 1 && (candidate?.conversion?.sales_unit_size ?? 1) === 1;
|
|
19
|
+
return salesUnits && oneItemPerPrice ? "sales_unit" : unit;
|
|
20
|
+
}
|
|
21
|
+
if (!PACK_UNITS.has(unit))
|
|
22
|
+
return unit;
|
|
23
|
+
if (salesUnits)
|
|
24
|
+
return "sales_unit";
|
|
25
|
+
const uoi = candidate?.uoi;
|
|
26
|
+
const singleItem = uoi != null && SINGLE_ITEM_UNITS.has(uoi.toLowerCase()) && (candidate?.uoi_count ?? 1) === 1;
|
|
27
|
+
return singleItem ? uoi : unit;
|
|
28
|
+
}
|
|
@@ -11,13 +11,7 @@ export interface CatalogueStatus {
|
|
|
11
11
|
}
|
|
12
12
|
/** With an account code, prices come back net of that trade account's discounts. */
|
|
13
13
|
export declare function searchCatalogue(query: string, category?: string, limit?: number, account?: string): Promise<MatchCandidate[]>;
|
|
14
|
-
|
|
15
|
-
* The unit to ask the basket for. A bag/box/roll/pack count is already a count of the product's
|
|
16
|
-
* sell units - the estimator did that sum - so it is asked in the product's own unit. A catalogue
|
|
17
|
-
* that sells the same bag "each" would otherwise refuse the line and leave it unpriced. Only a
|
|
18
|
-
* product sold one item at a time qualifies: 3 bags must never become 3 tonnes or 3 x 1000.
|
|
19
|
-
*/
|
|
20
|
-
export declare function estimateRequestUnit(unit: string, candidate: MatchCandidate | null | undefined): string;
|
|
14
|
+
export { estimateRequestUnit } from "./requestUnit";
|
|
21
15
|
/**
|
|
22
16
|
* Looks up every requirement in one go - the one place both the customer calculators and
|
|
23
17
|
* Metron's internal tool turn calculator lines into products. With the calculator's `tool`, a
|
|
@@ -44,19 +44,7 @@ export async function searchCatalogue(query, category, limit = 12, account) {
|
|
|
44
44
|
cache.set(cacheKey, candidates);
|
|
45
45
|
return candidates;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
const SINGLE_ITEM_UNITS = new Set(["each", "bag", "box", "roll", "pack", "tub"]);
|
|
49
|
-
/**
|
|
50
|
-
* The unit to ask the basket for. A bag/box/roll/pack count is already a count of the product's
|
|
51
|
-
* sell units - the estimator did that sum - so it is asked in the product's own unit. A catalogue
|
|
52
|
-
* that sells the same bag "each" would otherwise refuse the line and leave it unpriced. Only a
|
|
53
|
-
* product sold one item at a time qualifies: 3 bags must never become 3 tonnes or 3 x 1000.
|
|
54
|
-
*/
|
|
55
|
-
export function estimateRequestUnit(unit, candidate) {
|
|
56
|
-
const uoi = candidate?.uoi;
|
|
57
|
-
const singleItem = uoi != null && SINGLE_ITEM_UNITS.has(uoi.toLowerCase()) && (candidate?.uoi_count ?? 1) === 1;
|
|
58
|
-
return PACK_UNITS.has(unit) && singleItem ? uoi : unit;
|
|
59
|
-
}
|
|
47
|
+
export { estimateRequestUnit } from "./requestUnit";
|
|
60
48
|
/**
|
|
61
49
|
* Looks up every requirement in one go - the one place both the customer calculators and
|
|
62
50
|
* Metron's internal tool turn calculator lines into products. With the calculator's `tool`, a
|
|
@@ -76,7 +64,7 @@ export async function resolveRequirements(requirements, account, tool) {
|
|
|
76
64
|
searchCatalogue(requirement.query, requirement.category, undefined, account),
|
|
77
65
|
]);
|
|
78
66
|
const candidates = fixed ? [fixed, ...found.filter((item) => item.product_id !== fixed.product_id)] : found;
|
|
79
|
-
return { requirement
|
|
67
|
+
return { requirement, candidates };
|
|
80
68
|
}
|
|
81
69
|
catch {
|
|
82
70
|
return { requirement, candidates: [] };
|
package/dist/util/pricing.js
CHANGED
|
@@ -18,6 +18,8 @@ const unitAliases = {
|
|
|
18
18
|
units: "each",
|
|
19
19
|
pc: "each",
|
|
20
20
|
pcs: "each",
|
|
21
|
+
piece: "each",
|
|
22
|
+
pieces: "each",
|
|
21
23
|
board: "sheet",
|
|
22
24
|
bags: "bag",
|
|
23
25
|
length: "length",
|
|
@@ -140,6 +142,25 @@ export function getPricingBreakdown(item, options) {
|
|
|
140
142
|
});
|
|
141
143
|
if (!Number.isFinite(requestedAmount) || requestedAmount <= 0)
|
|
142
144
|
return review("Quantity needs confirmation.");
|
|
145
|
+
// "sales_unit" means one listed-price unit, whatever it holds: 1 sales_unit of a box of
|
|
146
|
+
// 1,000 screws priced "per Each" is one box at the listed price. The estimators ask for it
|
|
147
|
+
// when they have already counted packs (see estimateRequestUnit).
|
|
148
|
+
if (requestedUnit === "sales_unit") {
|
|
149
|
+
const priceUnit = canonicalUnit(item.uoi) || sellUnit;
|
|
150
|
+
const required = hasOverride ? overriddenSellUnits : roundQuantity(requestedAmount * (1 + wasteFactor), item.roundingRule ?? "round_up_each");
|
|
151
|
+
return {
|
|
152
|
+
requestedAmount,
|
|
153
|
+
requestedUnit: priceUnit,
|
|
154
|
+
salesUnitSize: 1,
|
|
155
|
+
sellUnit: priceUnit,
|
|
156
|
+
calculatedSellUnits: required,
|
|
157
|
+
requiredSellUnits: required,
|
|
158
|
+
unitPrice: item.price,
|
|
159
|
+
lineTotal: item.price != null ? item.price * required : null,
|
|
160
|
+
calculation: `${requestedAmount} ${priceUnit}${wasteFactor > 0 ? ` x ${1 + wasteFactor}` : ""} = ${required} ${priceUnit}`,
|
|
161
|
+
costingMode: "direct",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
143
164
|
if (!supportsRequestedUnit(item, requestedUnit)) {
|
|
144
165
|
return review("Requested unit is not supported by this catalogue conversion.");
|
|
145
166
|
}
|
|
@@ -250,13 +271,15 @@ export function getPricingBreakdown(item, options) {
|
|
|
250
271
|
calculation = `${requestedAmount} ${requestedUnit} x ${wasteMultiplier} / ${item.coverageQty} ${item.coverageUom} per ${sellUnit}`;
|
|
251
272
|
}
|
|
252
273
|
}
|
|
253
|
-
else if (item.conversionType === "weight_from_description") {
|
|
274
|
+
else if (item.conversionType === "weight_from_description" || item.conversionType === "mass_from_pack") {
|
|
275
|
+
// mass_from_pack (newer catalogue rows) states the unit as "kg per price unit"; the weight unit is its first word.
|
|
276
|
+
const coverageUnit = canonicalUnit((item.coverageUom ?? "").split(" ")[0]);
|
|
254
277
|
const requestedWeightG = weightInG(requestedAmount, requestedUnit);
|
|
255
|
-
const coverageWeightG = weightInG(item.coverageQty ?? 0,
|
|
278
|
+
const coverageWeightG = weightInG(item.coverageQty ?? 0, coverageUnit);
|
|
256
279
|
if (requestedWeightG != null && coverageWeightG != null && coverageWeightG > 0) {
|
|
257
280
|
rawSellUnits = (requestedWeightG * wasteMultiplier) / coverageWeightG;
|
|
258
281
|
costingMode = "derived";
|
|
259
|
-
calculation = `${requestedAmount} ${requestedUnit} x ${wasteMultiplier} / ${item.coverageQty} ${
|
|
282
|
+
calculation = `${requestedAmount} ${requestedUnit} x ${wasteMultiplier} / ${item.coverageQty} ${coverageUnit} per ${sellUnit}`;
|
|
260
283
|
}
|
|
261
284
|
}
|
|
262
285
|
else if (item.conversionType === "same_unit_volume") {
|