@metron-labs/quote-react 0.1.60 → 0.1.62
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/components/estimators/ArchitraveEstimator.js +2 -1
- package/dist/components/estimators/ConcreteAggregateEstimator.js +12 -74
- package/dist/components/estimators/DeckingEstimator.js +2 -1
- package/dist/components/estimators/FencingEstimator.js +10 -4
- package/dist/components/estimators/PaintEstimator.d.ts +1 -1
- package/dist/components/estimators/PaintEstimator.js +7 -2
- package/dist/embed/quote.js +13 -13
- package/dist/lib/estimators/fixedProducts.d.ts +5 -0
- package/dist/lib/estimators/fixedProducts.js +13 -2
- package/dist/util/pricing.js +46 -0
- package/package.json +1 -1
|
@@ -3,5 +3,10 @@ import type { FixedBasketLine } from "../../components/estimators/useFixedBasket
|
|
|
3
3
|
import type { EstimatorRequirement } from "./types";
|
|
4
4
|
/** The catalogue search for this line's fixed product, or null when the active client has none. */
|
|
5
5
|
export declare function fixedProductSearch(tool: EstimatorToolId, requirement: EstimatorRequirement): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a calculator should offer an option to the active client. A client with fixed products
|
|
8
|
+
* for that calculator is only offered what it stocks; any other client sees every option.
|
|
9
|
+
*/
|
|
10
|
+
export declare function offersOption(tool: EstimatorToolId, key: string): boolean;
|
|
6
11
|
/** Basket lines when every line has a fixed product, so the calculator can skip the matching step. */
|
|
7
12
|
export declare function fixedBasketLines(tool: EstimatorToolId, requirements: EstimatorRequirement[]): FixedBasketLine[] | null;
|
|
@@ -12,11 +12,14 @@ const FIXED_PRODUCTS = {
|
|
|
12
12
|
"NZ4Y-ZMSQ": {
|
|
13
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
|
-
// profiles only in 94mm and 144mm, so the nearer usable width is taken. FORT has no ovolo
|
|
15
|
+
// profiles only in 94mm and 144mm, so the nearer usable width is taken. FORT has no ovolo, so
|
|
16
|
+
// the architrave calculator doesn't offer it (see offersOption).
|
|
16
17
|
skirting: { "skirting-chamfered": "1525", "skirting-ogee": "135 primed mdf ogee", "skirting-pencil-round": "892", "skirting-torus": "1941" },
|
|
17
|
-
architrave: { "architrave-ogee": "134 primed mdf ogee", "architrave-
|
|
18
|
+
architrave: { "architrave-ogee": "134 primed mdf ogee", "architrave-pencil-round": "1896", "architrave-square-edge": "118 primed mdf square", "architrave-torus": "1531" },
|
|
18
19
|
// Board keys come from the board type; see fixedProductSearch.
|
|
19
20
|
plasterboard: { standard: "1647", moisture: "11 moisture plasterboard", fire: "14 fire plasterboard", sound: "1769", screws: "131160", tape: "1766", adhesive: "108439" },
|
|
21
|
+
paint: { "paint-emulsion": "157217" },
|
|
22
|
+
"concrete-aggregate": { cement: "1762 cement", "sharp-sand": "563", gravel: "571", mot: "569" },
|
|
20
23
|
insulation: { "insulation-floor": "152187", "insulation-ground": "442", "insulation-wall": "147", "insulation-roof": "442", "insulation-loft": "152180" },
|
|
21
24
|
timber: { studs: "1213", joists: "208", battens: "1729" },
|
|
22
25
|
fencing: {
|
|
@@ -38,6 +41,14 @@ export function fixedProductSearch(tool, requirement) {
|
|
|
38
41
|
const key = tool === "plasterboard" && requirement.key === "board" ? requirement.label.split(" ")[0].toLowerCase() : requirement.key;
|
|
39
42
|
return table[key] ?? null;
|
|
40
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether a calculator should offer an option to the active client. A client with fixed products
|
|
46
|
+
* for that calculator is only offered what it stocks; any other client sees every option.
|
|
47
|
+
*/
|
|
48
|
+
export function offersOption(tool, key) {
|
|
49
|
+
const table = FIXED_PRODUCTS[getQuoteConfig().client ?? ""]?.[tool];
|
|
50
|
+
return !table || key in table;
|
|
51
|
+
}
|
|
41
52
|
/** Basket lines when every line has a fixed product, so the calculator can skip the matching step. */
|
|
42
53
|
export function fixedBasketLines(tool, requirements) {
|
|
43
54
|
const lines = requirements.map((requirement) => {
|
package/dist/util/pricing.js
CHANGED
|
@@ -102,6 +102,24 @@ const packItemsForRequestedUnit = (item, requestedUnit) => {
|
|
|
102
102
|
}
|
|
103
103
|
return null;
|
|
104
104
|
};
|
|
105
|
+
// A tin, bag or tub priced per item usually names its size, e.g. "Vinyl Matt Emulsion - 5L" or
|
|
106
|
+
// "Sharp Sand Bulk Bag (800kg)"; a request in litres or kilograms is turned into whole packs from
|
|
107
|
+
// that. Only used when the catalogue gives no volume or weight itself.
|
|
108
|
+
const packSizeFromDescription = (item, measure) => {
|
|
109
|
+
const pattern = measure === "volume" ? /(\d+(?:[.,]\d+)?)\s*(ml|l|ltr|ltrs|litres?|liters?)\b/i : /(\d+(?:[.,]\d+)?)\s*(kg|kgs|tonnes?)\b/i;
|
|
110
|
+
const match = item.description.match(pattern);
|
|
111
|
+
if (!match)
|
|
112
|
+
return null;
|
|
113
|
+
const amount = Number.parseFloat(match[1].replace(",", "."));
|
|
114
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
115
|
+
return null;
|
|
116
|
+
const unit = match[2].toLowerCase();
|
|
117
|
+
if (measure === "volume")
|
|
118
|
+
return { ml: unit === "ml" ? amount : amount * 1000 };
|
|
119
|
+
return { g: unit.startsWith("tonne") ? amount * 1000000 : amount * 1000 };
|
|
120
|
+
};
|
|
121
|
+
const catalogueVolumeTypes = ["volume_from_description", "same_unit_volume"];
|
|
122
|
+
const catalogueWeightTypes = ["weight_from_description", "mass_from_pack", "same_unit_mass"];
|
|
105
123
|
// Product exports often omit structured coverage but include it verbatim, e.g. "13.89m2/Roll".
|
|
106
124
|
// This is intentionally narrow: it must name the same sell unit as the catalogue row.
|
|
107
125
|
const coverageFromDescription = (item, requestedUnit, sellUnit) => {
|
|
@@ -142,6 +160,34 @@ export function getPricingBreakdown(item, options) {
|
|
|
142
160
|
});
|
|
143
161
|
if (!Number.isFinite(requestedAmount) || requestedAmount <= 0)
|
|
144
162
|
return review("Quantity needs confirmation.");
|
|
163
|
+
const requestVolumeMl = volumeInMl(requestedAmount, requestedUnit);
|
|
164
|
+
const requestWeightG = weightInG(requestedAmount, requestedUnit);
|
|
165
|
+
const packFromDescription = sameUnit(requestedUnit, sellUnit)
|
|
166
|
+
? null
|
|
167
|
+
: requestVolumeMl != null && !catalogueVolumeTypes.includes(item.conversionType ?? "")
|
|
168
|
+
? packSizeFromDescription(item, "volume")
|
|
169
|
+
: requestWeightG != null && !catalogueWeightTypes.includes(item.conversionType ?? "")
|
|
170
|
+
? packSizeFromDescription(item, "weight")
|
|
171
|
+
: null;
|
|
172
|
+
const requested = packFromDescription?.ml != null ? requestVolumeMl : requestWeightG;
|
|
173
|
+
const packAmount = packFromDescription?.ml ?? packFromDescription?.g ?? null;
|
|
174
|
+
if (requested != null && packAmount != null) {
|
|
175
|
+
const priceUnit = canonicalUnit(item.uoi) || sellUnit;
|
|
176
|
+
const packLabel = packFromDescription?.ml != null ? `${packAmount / 1000} l` : `${packAmount / 1000} kg`;
|
|
177
|
+
const required = hasOverride ? overriddenSellUnits : Math.ceil((requested * (1 + wasteFactor)) / packAmount - 1e-9);
|
|
178
|
+
return {
|
|
179
|
+
requestedAmount,
|
|
180
|
+
requestedUnit,
|
|
181
|
+
salesUnitSize: 1,
|
|
182
|
+
sellUnit: priceUnit,
|
|
183
|
+
calculatedSellUnits: required,
|
|
184
|
+
requiredSellUnits: required,
|
|
185
|
+
unitPrice: item.price,
|
|
186
|
+
lineTotal: item.price != null ? item.price * required : null,
|
|
187
|
+
calculation: `${requestedAmount} ${requestedUnit}${wasteFactor > 0 ? ` x ${1 + wasteFactor}` : ""} / ${packLabel} per ${priceUnit} (from product description) = ${required} ${priceUnit}`,
|
|
188
|
+
costingMode: "derived",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
145
191
|
// "sales_unit" means one listed-price unit, whatever it holds: 1 sales_unit of a box of
|
|
146
192
|
// 1,000 screws priced "per Each" is one box at the listed price. The estimators ask for it
|
|
147
193
|
// when they have already counted packs (see estimateRequestUnit).
|