diff --git a/nss-hex-nut-din934-grade8-a2.csv b/nss-hex-nut-din934-grade8-a2.csv new file mode 100644 index 0000000..2305464 --- /dev/null +++ b/nss-hex-nut-din934-grade8-a2.csv @@ -0,0 +1,123 @@ +SKU,PosDescription,Description,Material,Grade,Diameter,Thread,Finish,Pack,UnitPrice +AMFNUT93448300,M48 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,48 mm,MF 3.00,Plain,Single Piece,R 59.66 +AMFNUT93442300,M42 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,42 mm,MF 3.00,Plain,Single Piece,R 41.06 +AMFNUT93439300,M39 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,39 mm,MF 3.00,Plain,Single Piece,R 36.24 +AMFNUT93436300,M36 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,36 mm,MF 3.00,Plain,Single Piece,R 33.14 +AMFNUT93430200,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,MF 2.00,Plain,Single Piece,R 18.05 +AMFNUT93424150,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,MF 1.50,Plain,Single Piece,R 5.85 +AMFNUT93422150,M22 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,22 mm,MF 1.50,Plain,Single Piece,R 5.20 +AMFNUT93420150,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,MF 1.50,Plain,Single Piece,R 2.66 +AMFNUT93418150,M18 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,18 mm,MF 1.50,Plain,Single Piece,R 3.13 +AMFNUT93416150,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,MF 1.50,Plain,Single Piece,R 1.39 +AMFNUT93414150,M14 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,14 mm,MF 1.50,Plain,Single Piece,R 1.17 +AMFNUT93412150,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,MF 1.50,Plain,Single Piece,R 0.65 +AMFNUT93412125,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,MF 1.25,Plain,Single Piece,R 0.74 +AMFNUT93410125,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,MF 1.25,Plain,Single Piece,R 0.43 +AMFNUT93410100,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,MF 1.00,Plain,Single Piece,R 0.49 +AMFNUT93408100,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,MF 1.00,Plain,,R 0.36 +AMCNUT93472,M72 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,72 mm,Standard,Plain,Single Piece,R 180.70 +AMCNUT93464,M64 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,64 mm,Standard,Plain,Single Piece,R 88.32 +AMCNUT93456,M56 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,56 mm,Standard,Plain,Single Piece,R 63.74 +AMCNUT93452,M52 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,52 mm,Standard,Plain,Single Piece,R 53.32 +AMCNUT93448H,M48 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,48 mm,Standard,HDG,Single Piece,R 51.42 +AMCNUT93448,M48 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,48 mm,Standard,Plain,Single Piece,R 43.10 +AMCNUT93445H,M45 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,45 mm,Standard,HDG,Single Piece,R 42.34 +AMCNUT93445,M45 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,45 mm,Standard,Plain,Single Piece,R 35.16 +AMCNUT93442Z,M42 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,Zinc Plated,Single Piece,R 31.76 +AMCNUT93442H,M42 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,HDG,Single Piece,R 35.15 +AMCNUT93442,M42 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,Plain,Single Piece,R 28.96 +AMCNUT93439H,M39 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,39 mm,Standard,HDG,Single Piece,R 26.55 +AMCNUT93439,M39 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,39 mm,Standard,Plain,Single Piece,R 22.23 +AMCNUT93436Z,M36 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,Zinc Plated,Single Piece,R 18.16 +AMCNUT93436H,M36 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,HDG,Single Piece,R 19.99 +AMCNUT93436,M36 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,Plain,Single Piece,R 16.58 +AMCNUT93433Z,M33 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,Zinc Plated,Single Piece,R 15.06 +AMCNUT93433H,M33 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,HDG,Single Piece,R 17.10 +AMCNUT93433,M33 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,Plain,Single Piece,R 12.99 +AMCNUT93430ZBAG,M30 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Zinc Plated,,R 1 +AMCNUT93430Z,M30 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Zinc Plated,Single Piece,R 10.24 +AMCNUT93430HBAG,M30 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,HDG,,R 1 +AMCNUT93430H,M30 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,HDG,Single Piece,R 11.27 +AMCNUT93430BAG,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Plain,,R 1 +AMCNUT93430,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Plain,Single Piece,R 9.35 +AMCNUT93427Z,M27 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,Zinc Plated,Single Piece,R 7.74 +AMCNUT93427H,M27 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,HDG,Single Piece,R 11.91 +AMCNUT93427,M27 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,Plain,Single Piece,R 7.15 +AMCNUT93424ZBAG,M24 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Zinc Plated,,R 1 +AMCNUT93424Z,M24 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Zinc Plated,Single Piece,R 4.26 +AMCNUT93424HBAG,M24 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,HDG,,R 1 +AMCNUT93424H,M24 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,HDG,Single Piece,R 4.83 +AMCNUT93424BAG,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Plain,,R 1 +AMCNUT93424,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Plain,Single Piece,R 4.06 +AMCNUT93422Z,M22 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,Zinc Plated,Single Piece,R 3.56 +AMCNUT93422H,M22 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,HDG,Single Piece,R 5.73 +AMCNUT93422,M22 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,Plain,Single Piece,R 3.35 +AMCNUT93420ZBAG,M20 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Zinc Plated,,R 957.00 +AMCNUT93420Z,M20 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Zinc Plated,Single Piece,R 2.21 +AMCNUT93420HBAG,M20 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,HDG,,R 1 +AMCNUT93420H,M20 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,HDG,Single Piece,R 2.70 +AMCNUT93420BAG,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Plain,,R 888.00 +AMCNUT93420,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Plain,Single Piece,R 2.05 +AMCNUT93418Z,M18 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,Zinc Plated,Single Piece,R 2.10 +AMCNUT93418H,M18 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,HDG,Single Piece,R 3.10 +AMCNUT93418,M18 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,Plain,Single Piece,R 1.89 +AMCNUT93416ZBAG,M16 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Zinc Plated,,R 957.00 +AMCNUT93416Z,M16 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Zinc Plated,Single Piece,R 1.17 +AMCNUT93416HBAG,M16 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,HDG,,R 1 +AMCNUT93416H,M16 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,HDG,Single Piece,R 1.41 +AMCNUT93416BAG,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Plain,,R 875.50 +AMCNUT93416,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Plain,Single Piece,R 1.07 +AMCNUT93414Z,M14 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,Zinc Plated,Single Piece,R 1.01 +AMCNUT93414H,M14 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,HDG,Single Piece,R 1.14 +AMCNUT93414,M14 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,Plain,Single Piece,R 0.90 +AMCNUT93412ZBAG,M12 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Zinc Plated,,R 926.10 +AMCNUT93412Z,M12 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Zinc Plated,Single Piece,R 0.60 +AMCNUT93412HBAG,M12 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,HDG,,R 1 +AMCNUT93412H,M12 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,HDG,Single Piece,R 0.75 +AMCNUT93412BAG,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Plain,,R 875.50 +AMCNUT93412,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Plain,Single Piece,R 0.57 +AMCNUT93410ZBAG,M10 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Zinc Plated,,R 1 +AMCNUT93410Z,M10 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Zinc Plated,Single Piece,R 0.40 +AMCNUT93410HBAG,M10 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,HDG,,R 1 +AMCNUT93410H,M10 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,HDG,Single Piece,R 0.51 +AMCNUT93410BAG,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Plain,,R 875.50 +AMCNUT93410,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Plain,Single Piece,R 0.38 +AMCNUT93408ZBAG,M8 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Zinc Plated,,R 926.10 +AMCNUT93408Z,M8 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Zinc Plated,Single Piece,R 0.18 +AMCNUT93408HBAG,M8 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,HDG,,R 1 +AMCNUT93408H,M8 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,HDG,Single Piece,R 0.23 +AMCNUT93408BAG,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Plain,,R 875.50 +AMCNUT93408,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Plain,Single Piece,R 0.17 +AMCNUT93406ZBAG,M6 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Zinc Plated,,R 926.10 +AMCNUT93406Z,M6 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Zinc Plated,Single Piece,R 0.08 +AMCNUT93406BAG,M6 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Plain,,R 1 +AMCNUT93406,M6 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Plain,Single Piece,R 0.08 +AMCNUT93405Z,M5 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,5 mm,Standard,Zinc Plated,Single Piece,R 0.06 +AMCNUT93405,M5 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,5 mm,Standard,Plain,Single Piece,R 0.06 +AMCNUT93404Z,M4 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,4 mm,Standard,Zinc Plated,Single Piece,R 0.06 +AMCNUT93404,M4 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,4 mm,Standard,Plain,Single Piece,R 0.05 +AMCNUT93403Z,M3 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,3 mm,Standard,Zinc Plated,Single Piece,R 0.03 +AMCNUT93403,M3 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,3 mm,Standard,Plain,,R 0.03 +AMCNUT93402Z,M2 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,2 mm,Standard,Zinc Plated,,R 0.02 +AMCNUT93402,M2 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,2 mm,Standard,Plain,,R 0.01 +SM2NUT93439,M39 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,39 mm,Standard,Plain,Single Piece,R 52.18 +SM2NUT93436,M36 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,36 mm,Standard,Plain,Single Piece,R 39.15 +SM2NUT93433,M33 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,33 mm,Standard,Plain,Single Piece,R 31.40 +SM2NUT93430,M30 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,30 mm,Standard,Plain,Single Piece,R 21.62 +SM2NUT93427,M27 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,27 mm,Standard,Plain,Single Piece,R 15.75 +SM2NUT93424,M24 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,24 mm,Standard,Plain,Single Piece,R 7.40 +SM2NUT93422,M22 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,22 mm,Standard,Plain,Single Piece,R 6.01 +SM2NUT93420,M20 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,20 mm,Standard,Plain,,R 4.60 +SM2NUT93418,M18 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,18 mm,Standard,Plain,,R 3.98 +SM2NUT93416,M16 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,16 mm,Standard,Plain,,R 2.23 +SM2NUT93414,M14 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,14 mm,Standard,Plain,,R 1.80 +SM2NUT93412,M12 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,12 mm,Standard,Plain,,R 1.15 +SM2NUT93410,M10 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,10 mm,Standard,Plain,,R 0.77 +SM2NUT93408,M8 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,8 mm,Standard,Plain,,R 0.39 +SM2NUT93406,M6 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,6 mm,Standard,Plain,,R 0.17 +SM2NUT93405,M5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,5 mm,Standard,Plain,,R 0.11 +SM2NUT93404,M4 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,4 mm,Standard,Plain,,R 0.08 +SM2NUT934035,M3.5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,3.5 mm,Standard,Plain,,R 0.22 +SM2NUT93403,M3 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,3 mm,Standard,Plain,,R 0.06 +SM2NUT934025,M2.5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,2.5 mm,Standard,Plain,,R 0.09 +SM2NUT93402,M2 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,2 mm,Standard,Plain,,R 0.09 \ No newline at end of file diff --git a/scripts/nss-fasteners-pb-import.json b/scripts/nss-fasteners-pb-import.json new file mode 100644 index 0000000..4f92bda --- /dev/null +++ b/scripts/nss-fasteners-pb-import.json @@ -0,0 +1,2586 @@ +{ + "meta": { + "source_csv": "scripts/nss-fasteners-single-pieces.csv", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "extracted_at": "2026-09-16", + "row_count": 99, + "pb_collection": "stock_items", + "notes": "selling_price and markup are unset (null/0). supplier and category are null — look up IDs separately before ingest." + }, + "records": [ + { + "name": "M48 NUT 8 PLN", + "sku": "AMFNUT93448300", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 48 mm | Thread: MF 3.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 59.66, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 59.66, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M42 NUT 8 PLN", + "sku": "AMFNUT93442300", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 42 mm | Thread: MF 3.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 41.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 41.06, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M39 NUT 8 PLN", + "sku": "AMFNUT93439300", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 39 mm | Thread: MF 3.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 36.24, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 36.24, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M36 NUT 8 PLN", + "sku": "AMFNUT93436300", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 36 mm | Thread: MF 3.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 33.14, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 33.14, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M30 NUT 8 PLN", + "sku": "AMFNUT93430200", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 30 mm | Thread: MF 2.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 18.05, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 18.05, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M24 NUT 8 PLN", + "sku": "AMFNUT93424150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 24 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 5.85, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 5.85, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M22 NUT 8 PLN", + "sku": "AMFNUT93422150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 22 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 5.2, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 5.2, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M20 NUT 8 PLN", + "sku": "AMFNUT93420150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 20 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 2.66, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 2.66, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M18 NUT 8 PLN", + "sku": "AMFNUT93418150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 18 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 3.13, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 3.13, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M16 NUT 8 PLN", + "sku": "AMFNUT93416150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 16 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.39, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.39, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M14 NUT 8 PLN", + "sku": "AMFNUT93414150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 14 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.17, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.17, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M12 NUT 8 PLN", + "sku": "AMFNUT93412150", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 12 mm | Thread: MF 1.50 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.65, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.65, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M12 NUT 8 PLN", + "sku": "AMFNUT93412125", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 12 mm | Thread: MF 1.25 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.74, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.74, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M10 NUT 8 PLN", + "sku": "AMFNUT93410125", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 10 mm | Thread: MF 1.25 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.43, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.43, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M10 NUT 8 PLN", + "sku": "AMFNUT93410100", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 10 mm | Thread: MF 1.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.49, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.49, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M8 NUT 8 PLN", + "sku": "AMFNUT93408100", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 8 mm | Thread: MF 1.00 | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.36, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.36, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M72 NUT 8 PLN", + "sku": "AMCNUT93472", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 72 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 180.7, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 180.7, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M64 NUT 8 PLN", + "sku": "AMCNUT93464", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 64 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 88.32, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 88.32, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M56 NUT 8 PLN", + "sku": "AMCNUT93456", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 56 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 63.74, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 63.74, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M52 NUT 8 PLN", + "sku": "AMCNUT93452", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 52 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 53.32, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 53.32, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M48 NUT 8 HDG", + "sku": "AMCNUT93448H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 48 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 51.42, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 51.42, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M48 NUT 8 PLN", + "sku": "AMCNUT93448", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 48 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 43.1, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 43.1, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M45 NUT 8 HDG", + "sku": "AMCNUT93445H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 45 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 42.34, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 42.34, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M45 NUT 8 PLN", + "sku": "AMCNUT93445", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 45 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 35.16, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 35.16, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M42 NUT 8 ZINC PLATED", + "sku": "AMCNUT93442Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 42 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 31.76, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 31.76, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M42 NUT 8 HDG", + "sku": "AMCNUT93442H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 42 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 35.15, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 35.15, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M42 NUT 8 PLN", + "sku": "AMCNUT93442", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 42 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 28.96, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 28.96, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M39 NUT 8 HDG", + "sku": "AMCNUT93439H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 39 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 26.55, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 26.55, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M39 NUT 8 PLN", + "sku": "AMCNUT93439", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 39 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 22.23, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 22.23, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M36 NUT 8 ZINC PLATED", + "sku": "AMCNUT93436Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 36 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 18.16, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 18.16, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M36 NUT 8 HDG", + "sku": "AMCNUT93436H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 36 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 19.99, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 19.99, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M36 NUT 8 PLN", + "sku": "AMCNUT93436", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 36 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 16.58, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 16.58, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M33 NUT 8 ZINC PLATED", + "sku": "AMCNUT93433Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 33 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 15.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 15.06, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M33 NUT 8 HDG", + "sku": "AMCNUT93433H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 33 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 17.1, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 17.1, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M33 NUT 8 PLN", + "sku": "AMCNUT93433", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 33 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 12.99, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 12.99, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M30 NUT 8 ZINC PLATED", + "sku": "AMCNUT93430Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 30 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 10.24, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 10.24, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M30 NUT 8 HDG", + "sku": "AMCNUT93430H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 30 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 11.27, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 11.27, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M30 NUT 8 PLN", + "sku": "AMCNUT93430", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 30 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 9.35, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 9.35, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M27 NUT 8 ZINC PLATED", + "sku": "AMCNUT93427Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 27 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 7.74, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 7.74, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M27 NUT 8 HDG", + "sku": "AMCNUT93427H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 27 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 11.91, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 11.91, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M27 NUT 8 PLN", + "sku": "AMCNUT93427", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 27 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 7.15, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 7.15, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M24 NUT 8 ZINC PLATED", + "sku": "AMCNUT93424Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 24 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 4.26, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 4.26, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M24 NUT 8 HDG", + "sku": "AMCNUT93424H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 24 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 4.83, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 4.83, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M24 NUT 8 PLN", + "sku": "AMCNUT93424", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 24 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 4.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 4.06, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M22 NUT 8 ZINC PLATED", + "sku": "AMCNUT93422Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 22 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 3.56, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 3.56, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M22 NUT 8 HDG", + "sku": "AMCNUT93422H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 22 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 5.73, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 5.73, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M22 NUT 8 PLN", + "sku": "AMCNUT93422", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 22 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 3.35, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 3.35, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M20 NUT 8 ZINC PLATED", + "sku": "AMCNUT93420Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 20 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 2.21, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 2.21, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M20 NUT 8 HDG", + "sku": "AMCNUT93420H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 20 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 2.7, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 2.7, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M20 NUT 8 PLN", + "sku": "AMCNUT93420", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 20 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 2.05, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 2.05, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M18 NUT 8 ZINC PLATED", + "sku": "AMCNUT93418Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 18 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 2.1, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 2.1, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M18 NUT 8 HDG", + "sku": "AMCNUT93418H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 18 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 3.1, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 3.1, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M18 NUT 8 PLN", + "sku": "AMCNUT93418", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 18 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.89, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.89, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M16 NUT 8 ZINC PLATED", + "sku": "AMCNUT93416Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 16 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.17, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.17, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M16 NUT 8 HDG", + "sku": "AMCNUT93416H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 16 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.41, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.41, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M16 NUT 8 PLN", + "sku": "AMCNUT93416", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 16 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.07, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.07, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M14 NUT 8 ZINC PLATED", + "sku": "AMCNUT93414Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 14 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.01, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.01, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M14 NUT 8 HDG", + "sku": "AMCNUT93414H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 14 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.14, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.14, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M14 NUT 8 PLN", + "sku": "AMCNUT93414", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 14 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.9, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.9, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M12 NUT 8 ZINC PLATED", + "sku": "AMCNUT93412Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 12 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.6, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.6, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M12 NUT 8 HDG", + "sku": "AMCNUT93412H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 12 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.75, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.75, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M12 NUT 8 PLN", + "sku": "AMCNUT93412", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 12 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.57, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.57, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M10 NUT 8 ZINC PLATED", + "sku": "AMCNUT93410Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 10 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.4, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.4, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M10 NUT 8 HDG", + "sku": "AMCNUT93410H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 10 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.51, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.51, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M10 NUT 8 PLN", + "sku": "AMCNUT93410", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 10 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.38, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.38, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M8 NUT 8 ZINC PLATED", + "sku": "AMCNUT93408Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 8 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.18, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.18, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M8 NUT 8 HDG", + "sku": "AMCNUT93408H", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 8 mm | Thread: Standard | Finish: HDG | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.23, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.23, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M8 NUT 8 PLN", + "sku": "AMCNUT93408", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 8 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.17, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.17, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M6 NUT 8 ZINC PLATED", + "sku": "AMCNUT93406Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 6 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.08, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.08, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M6 NUT 8 PLN", + "sku": "AMCNUT93406", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 6 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.08, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.08, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M5 NUT 8 ZINC PLATED", + "sku": "AMCNUT93405Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 5 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.06, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M5 NUT 8 PLN", + "sku": "AMCNUT93405", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 5 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.06, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M4 NUT 8 ZINC PLATED", + "sku": "AMCNUT93404Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 4 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.06, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M4 NUT 8 PLN", + "sku": "AMCNUT93404", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 4 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.05, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.05, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M3 NUT 8 ZINC PLATED", + "sku": "AMCNUT93403Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 3 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.03, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.03, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M3 NUT 8 PLN", + "sku": "AMCNUT93403", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 3 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.03, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.03, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M2 NUT 8 ZINC PLATED", + "sku": "AMCNUT93402Z", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 2 mm | Thread: Standard | Finish: Zinc Plated | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.02, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.02, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M2 NUT 8 PLN", + "sku": "AMCNUT93402", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Hi-Tensile | Grade: 8 | Diameter: 2 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.01, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.01, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M39 NUT A2 PLN", + "sku": "SM2NUT93439", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 39 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 52.18, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 52.18, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M36 NUT A2 PLN", + "sku": "SM2NUT93436", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 36 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 39.15, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 39.15, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M33 NUT A2 PLN", + "sku": "SM2NUT93433", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 33 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 31.4, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 31.4, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M30 NUT A2 PLN", + "sku": "SM2NUT93430", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 30 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 21.62, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 21.62, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M27 NUT A2 PLN", + "sku": "SM2NUT93427", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 27 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 15.75, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 15.75, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M24 NUT A2 PLN", + "sku": "SM2NUT93424", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 24 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 7.4, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 7.4, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M22 NUT A2 PLN", + "sku": "SM2NUT93422", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 22 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 6.01, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 6.01, + "unit": "Single Piece", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M20 NUT A2 PLN", + "sku": "SM2NUT93420", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 20 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 4.6, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 4.6, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M18 NUT A2 PLN", + "sku": "SM2NUT93418", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 18 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 3.98, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 3.98, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M16 NUT A2 PLN", + "sku": "SM2NUT93416", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 16 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 2.23, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 2.23, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M14 NUT A2 PLN", + "sku": "SM2NUT93414", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 14 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.8, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.8, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M12 NUT A2 PLN", + "sku": "SM2NUT93412", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 12 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 1.15, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 1.15, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M10 NUT A2 PLN", + "sku": "SM2NUT93410", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 10 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.77, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.77, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M8 NUT A2 PLN", + "sku": "SM2NUT93408", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 8 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.39, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.39, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M6 NUT A2 PLN", + "sku": "SM2NUT93406", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 6 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.17, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.17, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M5 NUT A2 PLN", + "sku": "SM2NUT93405", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 5 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.11, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.11, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M4 NUT A2 PLN", + "sku": "SM2NUT93404", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 4 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.08, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.08, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M3.5 NUT A2 PLN", + "sku": "SM2NUT934035", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 3.5 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.22, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.22, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M3 NUT A2 PLN", + "sku": "SM2NUT93403", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 3 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.06, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.06, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M2.5 NUT A2 PLN", + "sku": "SM2NUT934025", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 2.5 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.09, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.09, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + }, + { + "name": "M2 NUT A2 PLN", + "sku": "SM2NUT93402", + "description": "Hex Nut DIN934", + "location": "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "notes": "Material: Stainless Steel | Grade: A2 | Diameter: 2 mm | Thread: Standard | Finish: Plain | Source: NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)", + "cost_history": [ + { + "price": 0.09, + "currency": "ZAR", + "source": "nssfasteners.co.za", + "extracted_at": "2026-09-16", + "source_url": "https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773", + "qty_basis": 1, + "extraction_method": "browser_automation" + } + ], + "cost_price": 0.09, + "unit": "Each", + "current_qty": 0, + "min_qty": 0, + "target_qty": 0, + "is_active": true, + "supplier": null, + "category": null + } + ] +} \ No newline at end of file diff --git a/scripts/nss-fasteners-pb-transform.ts b/scripts/nss-fasteners-pb-transform.ts new file mode 100644 index 0000000..d213c24 --- /dev/null +++ b/scripts/nss-fasteners-pb-transform.ts @@ -0,0 +1,144 @@ +/** + * Maps scripts/nss-fasteners-single-pieces.csv into PocketBase + * `stock_items` records for review. + * + * Output: scripts/nss-fasteners-pb-import.json + * + * No data is written to PocketBase yet — this is for the user to + * inspect the field mapping before any ingest. + * + * Run: bun scripts/nss-fasteners-pb-transform.ts + * or: npx tsx scripts/nss-fasteners-pb-transform.ts + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const SOURCE_CSV = join(import.meta.dirname, 'nss-fasteners-single-pieces.csv'); +const OUTPUT_JSON = join(import.meta.dirname, 'nss-fasteners-pb-import.json'); + +// === supplied by operator on 2026-09-16 === +const SUPPLIER_NSS_ID = '6xuu7a0pmmubtqd'; +const CATEGORY_FASTENERS_ID = '4emvaanzogj0586'; +const MARKUP_PERCENT = 80; // selling_price = cost_price * (1 + MARKUP_PERCENT / 100) + +const SOURCE_URL = + 'https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773'; +const SOURCE_LABEL = 'NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)'; +const EXTRACTED_AT = '2026-09-16'; // YYYY-MM-DD + +type CsvRow = { + SKU: string; + PosDescription: string; + Description: string; + Material: string; + Grade: string; + Diameter: string; + Thread: string; + Finish: string; + Pack: string; + UnitPrice: string; +}; + +const parsePrice = (s: string): number | null => { + // "R 59.66" -> 59.66 + const m = s.match(/([\d.]+)/); + if (!m) return null; + const n = parseFloat(m[1]); + return Number.isFinite(n) ? n : null; +}; + +const toPbRecord = (r: CsvRow) => { + const costPrice = parsePrice(r.UnitPrice); + const notes = [ + `Material: ${r.Material}`, + `Grade: ${r.Grade}`, + `Diameter: ${r.Diameter}`, + `Thread: ${r.Thread}`, + `Finish: ${r.Finish}`, + `Source: ${SOURCE_LABEL}`, + ] + .filter(Boolean) + .join(' | '); + + const costHistory = [ + { + price: costPrice, + currency: 'ZAR', + source: 'nssfasteners.co.za', + extracted_at: EXTRACTED_AT, + source_url: SOURCE_URL, + qty_basis: 1, + extraction_method: 'browser_automation', + }, + ]; + + return { + // === required === + name: r.PosDescription, + + // === identifying === + sku: r.SKU, + description: r.Description, + + // === sourcing / tracking (user's suggestion) === + location: SOURCE_LABEL, + notes, + cost_history: costHistory, + + // === pricing === + cost_price: costPrice, + // selling_price and markup intentionally left for the user to set + // (depends on customer-facing markup policy, not supplier data). + + // === inventory defaults (no source data) === + unit: r.Pack || 'Each', + current_qty: 0, + min_qty: 0, + target_qty: 0, + is_active: true, + + // === relations intentionally NULL === + // supplier + category are relations that need IDs the user must look up. + // Do not invent them here. + supplier: null, + category: null, + }; +}; + +const csv = readFileSync(SOURCE_CSV, 'utf8').trim(); +const [headerLine, ...dataLines] = csv.split('\n'); +const headers = headerLine.split(',') as (keyof CsvRow)[]; + +const rows: CsvRow[] = dataLines.map(line => { + // naive CSV split - no embedded commas in current dataset, so safe. + const cols = line.split(','); + const obj = {} as CsvRow; + headers.forEach((h, i) => { + (obj as Record)[h] = cols[i] ?? ''; + }); + return obj; +}); + +const records = rows.map(toPbRecord); + +writeFileSync( + OUTPUT_JSON, + JSON.stringify( + { + meta: { + source_csv: 'scripts/nss-fasteners-single-pieces.csv', + source_url: SOURCE_URL, + extracted_at: EXTRACTED_AT, + row_count: records.length, + pb_collection: 'stock_items', + notes: + 'selling_price and markup are unset (null/0). supplier and category are null — look up IDs separately before ingest.', + }, + records, + }, + null, + 2, + ), +); + +console.log(`Wrote ${records.length} records to ${OUTPUT_JSON}`); diff --git a/scripts/nss-fasteners-pilot.csv b/scripts/nss-fasteners-pilot.csv new file mode 100644 index 0000000..1d72086 --- /dev/null +++ b/scripts/nss-fasteners-pilot.csv @@ -0,0 +1,123 @@ +SKU,PosDescription,Description,Material,Grade,Diameter,Thread,Finish,Pack,UnitPrice +AMFNUT93448300,M48 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,48 mm,MF 3.00,Plain,Single Piece,R 59.66 +AMFNUT93442300,M42 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,42 mm,MF 3.00,Plain,Single Piece,R 41.06 +AMFNUT93439300,M39 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,39 mm,MF 3.00,Plain,Single Piece,R 36.24 +AMFNUT93436300,M36 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,36 mm,MF 3.00,Plain,Single Piece,R 33.14 +AMFNUT93430200,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,MF 2.00,Plain,Single Piece,R 18.05 +AMFNUT93424150,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,MF 1.50,Plain,Single Piece,R 5.85 +AMFNUT93422150,M22 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,22 mm,MF 1.50,Plain,Single Piece,R 5.20 +AMFNUT93420150,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,MF 1.50,Plain,Single Piece,R 2.66 +AMFNUT93418150,M18 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,18 mm,MF 1.50,Plain,Single Piece,R 3.13 +AMFNUT93416150,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,MF 1.50,Plain,Single Piece,R 1.39 +AMFNUT93414150,M14 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,14 mm,MF 1.50,Plain,Single Piece,R 1.17 +AMFNUT93412150,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,MF 1.50,Plain,Single Piece,R 0.65 +AMFNUT93412125,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,MF 1.25,Plain,Single Piece,R 0.74 +AMFNUT93410125,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,MF 1.25,Plain,Single Piece,R 0.43 +AMFNUT93410100,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,MF 1.00,Plain,Single Piece,R 0.49 +AMFNUT93408100,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,MF 1.00,Plain,,R 0.36 +AMCNUT93472,M72 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,72 mm,Standard,Plain,Single Piece,R 180.70 +AMCNUT93464,M64 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,64 mm,Standard,Plain,Single Piece,R 88.32 +AMCNUT93456,M56 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,56 mm,Standard,Plain,Single Piece,R 63.74 +AMCNUT93452,M52 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,52 mm,Standard,Plain,Single Piece,R 53.32 +AMCNUT93448H,M48 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,48 mm,Standard,HDG,Single Piece,R 51.42 +AMCNUT93448,M48 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,48 mm,Standard,Plain,Single Piece,R 43.10 +AMCNUT93445H,M45 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,45 mm,Standard,HDG,Single Piece,R 42.34 +AMCNUT93445,M45 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,45 mm,Standard,Plain,Single Piece,R 35.16 +AMCNUT93442Z,M42 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,Zinc Plated,Single Piece,R 31.76 +AMCNUT93442H,M42 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,HDG,Single Piece,R 35.15 +AMCNUT93442,M42 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,Plain,Single Piece,R 28.96 +AMCNUT93439H,M39 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,39 mm,Standard,HDG,Single Piece,R 26.55 +AMCNUT93439,M39 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,39 mm,Standard,Plain,Single Piece,R 22.23 +AMCNUT93436Z,M36 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,Zinc Plated,Single Piece,R 18.16 +AMCNUT93436H,M36 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,HDG,Single Piece,R 19.99 +AMCNUT93436,M36 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,Plain,Single Piece,R 16.58 +AMCNUT93433Z,M33 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,Zinc Plated,Single Piece,R 15.06 +AMCNUT93433H,M33 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,HDG,Single Piece,R 17.10 +AMCNUT93433,M33 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,Plain,Single Piece,R 12.99 +AMCNUT93430ZBAG,M30 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Zinc Plated,,R 1 +AMCNUT93430Z,M30 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Zinc Plated,Single Piece,R 10.24 +AMCNUT93430HBAG,M30 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,HDG,,R 1 +AMCNUT93430H,M30 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,HDG,Single Piece,R 11.27 +AMCNUT93430BAG,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Plain,,R 1 +AMCNUT93430,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Plain,Single Piece,R 9.35 +AMCNUT93427Z,M27 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,Zinc Plated,Single Piece,R 7.74 +AMCNUT93427H,M27 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,HDG,Single Piece,R 11.91 +AMCNUT93427,M27 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,Plain,Single Piece,R 7.15 +AMCNUT93424ZBAG,M24 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Zinc Plated,,R 1 +AMCNUT93424Z,M24 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Zinc Plated,Single Piece,R 4.26 +AMCNUT93424HBAG,M24 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,HDG,,R 1 +AMCNUT93424H,M24 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,HDG,Single Piece,R 4.83 +AMCNUT93424BAG,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Plain,,R 1 +AMCNUT93424,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Plain,Single Piece,R 4.06 +AMCNUT93422Z,M22 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,Zinc Plated,Single Piece,R 3.56 +AMCNUT93422H,M22 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,HDG,Single Piece,R 5.73 +AMCNUT93422,M22 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,Plain,Single Piece,R 3.35 +AMCNUT93420ZBAG,M20 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Zinc Plated,,R 957.00 +AMCNUT93420Z,M20 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Zinc Plated,Single Piece,R 2.21 +AMCNUT93420HBAG,M20 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,HDG,,R 1 +AMCNUT93420H,M20 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,HDG,Single Piece,R 2.70 +AMCNUT93420BAG,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Plain,,R 888.00 +AMCNUT93420,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Plain,Single Piece,R 2.05 +AMCNUT93418Z,M18 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,Zinc Plated,Single Piece,R 2.10 +AMCNUT93418H,M18 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,HDG,Single Piece,R 3.10 +AMCNUT93418,M18 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,Plain,Single Piece,R 1.89 +AMCNUT93416ZBAG,M16 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Zinc Plated,,R 957.00 +AMCNUT93416Z,M16 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Zinc Plated,Single Piece,R 1.17 +AMCNUT93416HBAG,M16 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,HDG,,R 1 +AMCNUT93416H,M16 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,HDG,Single Piece,R 1.41 +AMCNUT93416BAG,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Plain,,R 875.50 +AMCNUT93416,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Plain,Single Piece,R 1.07 +AMCNUT93414Z,M14 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,Zinc Plated,Single Piece,R 1.01 +AMCNUT93414H,M14 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,HDG,Single Piece,R 1.14 +AMCNUT93414,M14 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,Plain,Single Piece,R 0.90 +AMCNUT93412ZBAG,M12 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Zinc Plated,,R 926.10 +AMCNUT93412Z,M12 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Zinc Plated,Single Piece,R 0.60 +AMCNUT93412HBAG,M12 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,HDG,,R 1 +AMCNUT93412H,M12 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,HDG,Single Piece,R 0.75 +AMCNUT93412BAG,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Plain,,R 875.50 +AMCNUT93412,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Plain,Single Piece,R 0.57 +AMCNUT93410ZBAG,M10 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Zinc Plated,,R 1 +AMCNUT93410Z,M10 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Zinc Plated,Single Piece,R 0.40 +AMCNUT93410HBAG,M10 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,HDG,,R 1 +AMCNUT93410H,M10 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,HDG,Single Piece,R 0.51 +AMCNUT93410BAG,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Plain,,R 875.50 +AMCNUT93410,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Plain,Single Piece,R 0.38 +AMCNUT93408ZBAG,M8 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Zinc Plated,,R 926.10 +AMCNUT93408Z,M8 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Zinc Plated,Single Piece,R 0.18 +AMCNUT93408HBAG,M8 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,HDG,,R 1 +AMCNUT93408H,M8 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,HDG,Single Piece,R 0.23 +AMCNUT93408BAG,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Plain,,R 875.50 +AMCNUT93408,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Plain,Single Piece,R 0.17 +AMCNUT93406ZBAG,M6 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Zinc Plated,,R 926.10 +AMCNUT93406Z,M6 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Zinc Plated,Single Piece,R 0.08 +AMCNUT93406BAG,M6 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Plain,,R 1 +AMCNUT93406,M6 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Plain,Single Piece,R 0.08 +AMCNUT93405Z,M5 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,5 mm,Standard,Zinc Plated,Single Piece,R 0.06 +AMCNUT93405,M5 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,5 mm,Standard,Plain,Single Piece,R 0.06 +AMCNUT93404Z,M4 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,4 mm,Standard,Zinc Plated,Single Piece,R 0.06 +AMCNUT93404,M4 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,4 mm,Standard,Plain,Single Piece,R 0.05 +AMCNUT93403Z,M3 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,3 mm,Standard,Zinc Plated,Single Piece,R 0.03 +AMCNUT93403,M3 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,3 mm,Standard,Plain,,R 0.03 +AMCNUT93402Z,M2 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,2 mm,Standard,Zinc Plated,,R 0.02 +AMCNUT93402,M2 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,2 mm,Standard,Plain,,R 0.01 +SM2NUT93439,M39 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,39 mm,Standard,Plain,Single Piece,R 52.18 +SM2NUT93436,M36 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,36 mm,Standard,Plain,Single Piece,R 39.15 +SM2NUT93433,M33 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,33 mm,Standard,Plain,Single Piece,R 31.40 +SM2NUT93430,M30 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,30 mm,Standard,Plain,Single Piece,R 21.62 +SM2NUT93427,M27 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,27 mm,Standard,Plain,Single Piece,R 15.75 +SM2NUT93424,M24 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,24 mm,Standard,Plain,Single Piece,R 7.40 +SM2NUT93422,M22 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,22 mm,Standard,Plain,Single Piece,R 6.01 +SM2NUT93420,M20 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,20 mm,Standard,Plain,,R 4.60 +SM2NUT93418,M18 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,18 mm,Standard,Plain,,R 3.98 +SM2NUT93416,M16 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,16 mm,Standard,Plain,,R 2.23 +SM2NUT93414,M14 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,14 mm,Standard,Plain,,R 1.80 +SM2NUT93412,M12 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,12 mm,Standard,Plain,,R 1.15 +SM2NUT93410,M10 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,10 mm,Standard,Plain,,R 0.77 +SM2NUT93408,M8 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,8 mm,Standard,Plain,,R 0.39 +SM2NUT93406,M6 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,6 mm,Standard,Plain,,R 0.17 +SM2NUT93405,M5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,5 mm,Standard,Plain,,R 0.11 +SM2NUT93404,M4 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,4 mm,Standard,Plain,,R 0.08 +SM2NUT934035,M3.5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,3.5 mm,Standard,Plain,,R 0.22 +SM2NUT93403,M3 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,3 mm,Standard,Plain,,R 0.06 +SM2NUT934025,M2.5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,2.5 mm,Standard,Plain,,R 0.09 +SM2NUT93402,M2 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,2 mm,Standard,Plain,,R 0.09 diff --git a/scripts/nss-fasteners-single-pieces.csv b/scripts/nss-fasteners-single-pieces.csv new file mode 100644 index 0000000..b488ac9 --- /dev/null +++ b/scripts/nss-fasteners-single-pieces.csv @@ -0,0 +1,100 @@ +SKU,PosDescription,Description,Material,Grade,Diameter,Thread,Finish,Pack,UnitPrice +AMFNUT93448300,M48 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,48 mm,MF 3.00,Plain,Single Piece,R 59.66 +AMFNUT93442300,M42 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,42 mm,MF 3.00,Plain,Single Piece,R 41.06 +AMFNUT93439300,M39 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,39 mm,MF 3.00,Plain,Single Piece,R 36.24 +AMFNUT93436300,M36 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,36 mm,MF 3.00,Plain,Single Piece,R 33.14 +AMFNUT93430200,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,MF 2.00,Plain,Single Piece,R 18.05 +AMFNUT93424150,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,MF 1.50,Plain,Single Piece,R 5.85 +AMFNUT93422150,M22 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,22 mm,MF 1.50,Plain,Single Piece,R 5.20 +AMFNUT93420150,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,MF 1.50,Plain,Single Piece,R 2.66 +AMFNUT93418150,M18 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,18 mm,MF 1.50,Plain,Single Piece,R 3.13 +AMFNUT93416150,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,MF 1.50,Plain,Single Piece,R 1.39 +AMFNUT93414150,M14 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,14 mm,MF 1.50,Plain,Single Piece,R 1.17 +AMFNUT93412150,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,MF 1.50,Plain,Single Piece,R 0.65 +AMFNUT93412125,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,MF 1.25,Plain,Single Piece,R 0.74 +AMFNUT93410125,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,MF 1.25,Plain,Single Piece,R 0.43 +AMFNUT93410100,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,MF 1.00,Plain,Single Piece,R 0.49 +AMFNUT93408100,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,MF 1.00,Plain,,R 0.36 +AMCNUT93472,M72 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,72 mm,Standard,Plain,Single Piece,R 180.70 +AMCNUT93464,M64 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,64 mm,Standard,Plain,Single Piece,R 88.32 +AMCNUT93456,M56 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,56 mm,Standard,Plain,Single Piece,R 63.74 +AMCNUT93452,M52 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,52 mm,Standard,Plain,Single Piece,R 53.32 +AMCNUT93448H,M48 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,48 mm,Standard,HDG,Single Piece,R 51.42 +AMCNUT93448,M48 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,48 mm,Standard,Plain,Single Piece,R 43.10 +AMCNUT93445H,M45 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,45 mm,Standard,HDG,Single Piece,R 42.34 +AMCNUT93445,M45 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,45 mm,Standard,Plain,Single Piece,R 35.16 +AMCNUT93442Z,M42 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,Zinc Plated,Single Piece,R 31.76 +AMCNUT93442H,M42 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,HDG,Single Piece,R 35.15 +AMCNUT93442,M42 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,42 mm,Standard,Plain,Single Piece,R 28.96 +AMCNUT93439H,M39 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,39 mm,Standard,HDG,Single Piece,R 26.55 +AMCNUT93439,M39 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,39 mm,Standard,Plain,Single Piece,R 22.23 +AMCNUT93436Z,M36 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,Zinc Plated,Single Piece,R 18.16 +AMCNUT93436H,M36 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,HDG,Single Piece,R 19.99 +AMCNUT93436,M36 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,36 mm,Standard,Plain,Single Piece,R 16.58 +AMCNUT93433Z,M33 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,Zinc Plated,Single Piece,R 15.06 +AMCNUT93433H,M33 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,HDG,Single Piece,R 17.10 +AMCNUT93433,M33 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,33 mm,Standard,Plain,Single Piece,R 12.99 +AMCNUT93430Z,M30 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Zinc Plated,Single Piece,R 10.24 +AMCNUT93430H,M30 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,HDG,Single Piece,R 11.27 +AMCNUT93430,M30 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,30 mm,Standard,Plain,Single Piece,R 9.35 +AMCNUT93427Z,M27 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,Zinc Plated,Single Piece,R 7.74 +AMCNUT93427H,M27 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,HDG,Single Piece,R 11.91 +AMCNUT93427,M27 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,27 mm,Standard,Plain,Single Piece,R 7.15 +AMCNUT93424Z,M24 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Zinc Plated,Single Piece,R 4.26 +AMCNUT93424H,M24 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,HDG,Single Piece,R 4.83 +AMCNUT93424,M24 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,24 mm,Standard,Plain,Single Piece,R 4.06 +AMCNUT93422Z,M22 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,Zinc Plated,Single Piece,R 3.56 +AMCNUT93422H,M22 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,HDG,Single Piece,R 5.73 +AMCNUT93422,M22 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,22 mm,Standard,Plain,Single Piece,R 3.35 +AMCNUT93420Z,M20 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Zinc Plated,Single Piece,R 2.21 +AMCNUT93420H,M20 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,HDG,Single Piece,R 2.70 +AMCNUT93420,M20 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,20 mm,Standard,Plain,Single Piece,R 2.05 +AMCNUT93418Z,M18 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,Zinc Plated,Single Piece,R 2.10 +AMCNUT93418H,M18 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,HDG,Single Piece,R 3.10 +AMCNUT93418,M18 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,18 mm,Standard,Plain,Single Piece,R 1.89 +AMCNUT93416Z,M16 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Zinc Plated,Single Piece,R 1.17 +AMCNUT93416H,M16 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,HDG,Single Piece,R 1.41 +AMCNUT93416,M16 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,16 mm,Standard,Plain,Single Piece,R 1.07 +AMCNUT93414Z,M14 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,Zinc Plated,Single Piece,R 1.01 +AMCNUT93414H,M14 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,HDG,Single Piece,R 1.14 +AMCNUT93414,M14 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,14 mm,Standard,Plain,Single Piece,R 0.90 +AMCNUT93412Z,M12 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Zinc Plated,Single Piece,R 0.60 +AMCNUT93412H,M12 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,HDG,Single Piece,R 0.75 +AMCNUT93412,M12 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,12 mm,Standard,Plain,Single Piece,R 0.57 +AMCNUT93410Z,M10 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Zinc Plated,Single Piece,R 0.40 +AMCNUT93410H,M10 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,HDG,Single Piece,R 0.51 +AMCNUT93410,M10 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,10 mm,Standard,Plain,Single Piece,R 0.38 +AMCNUT93408Z,M8 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Zinc Plated,Single Piece,R 0.18 +AMCNUT93408H,M8 NUT 8 HDG,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,HDG,Single Piece,R 0.23 +AMCNUT93408,M8 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,8 mm,Standard,Plain,Single Piece,R 0.17 +AMCNUT93406Z,M6 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Zinc Plated,Single Piece,R 0.08 +AMCNUT93406,M6 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,6 mm,Standard,Plain,Single Piece,R 0.08 +AMCNUT93405Z,M5 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,5 mm,Standard,Zinc Plated,Single Piece,R 0.06 +AMCNUT93405,M5 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,5 mm,Standard,Plain,Single Piece,R 0.06 +AMCNUT93404Z,M4 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,4 mm,Standard,Zinc Plated,Single Piece,R 0.06 +AMCNUT93404,M4 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,4 mm,Standard,Plain,Single Piece,R 0.05 +AMCNUT93403Z,M3 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,3 mm,Standard,Zinc Plated,Single Piece,R 0.03 +AMCNUT93403,M3 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,3 mm,Standard,Plain,,R 0.03 +AMCNUT93402Z,M2 NUT 8 ZINC PLATED,Hex Nut DIN934,Hi-Tensile,8,2 mm,Standard,Zinc Plated,,R 0.02 +AMCNUT93402,M2 NUT 8 PLN,Hex Nut DIN934,Hi-Tensile,8,2 mm,Standard,Plain,,R 0.01 +SM2NUT93439,M39 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,39 mm,Standard,Plain,Single Piece,R 52.18 +SM2NUT93436,M36 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,36 mm,Standard,Plain,Single Piece,R 39.15 +SM2NUT93433,M33 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,33 mm,Standard,Plain,Single Piece,R 31.40 +SM2NUT93430,M30 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,30 mm,Standard,Plain,Single Piece,R 21.62 +SM2NUT93427,M27 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,27 mm,Standard,Plain,Single Piece,R 15.75 +SM2NUT93424,M24 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,24 mm,Standard,Plain,Single Piece,R 7.40 +SM2NUT93422,M22 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,22 mm,Standard,Plain,Single Piece,R 6.01 +SM2NUT93420,M20 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,20 mm,Standard,Plain,,R 4.60 +SM2NUT93418,M18 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,18 mm,Standard,Plain,,R 3.98 +SM2NUT93416,M16 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,16 mm,Standard,Plain,,R 2.23 +SM2NUT93414,M14 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,14 mm,Standard,Plain,,R 1.80 +SM2NUT93412,M12 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,12 mm,Standard,Plain,,R 1.15 +SM2NUT93410,M10 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,10 mm,Standard,Plain,,R 0.77 +SM2NUT93408,M8 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,8 mm,Standard,Plain,,R 0.39 +SM2NUT93406,M6 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,6 mm,Standard,Plain,,R 0.17 +SM2NUT93405,M5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,5 mm,Standard,Plain,,R 0.11 +SM2NUT93404,M4 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,4 mm,Standard,Plain,,R 0.08 +SM2NUT934035,M3.5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,3.5 mm,Standard,Plain,,R 0.22 +SM2NUT93403,M3 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,3 mm,Standard,Plain,,R 0.06 +SM2NUT934025,M2.5 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,2.5 mm,Standard,Plain,,R 0.09 +SM2NUT93402,M2 NUT A2 PLN,Hex Nut DIN934,Stainless Steel,A2,2 mm,Standard,Plain,,R 0.09 diff --git a/scripts/nss-scrape-bundle.js b/scripts/nss-scrape-bundle.js new file mode 100644 index 0000000..04a2385 --- /dev/null +++ b/scripts/nss-scrape-bundle.js @@ -0,0 +1,131 @@ +// Run in the browser console on nssfasteners.co.za after login. +// Fetches all Grade 8 (101 items) + A2 (21 items) hex nut DIN934 product pages, +// parses the hidden unit prices out of the HTML, builds PosDescription strings, +// and triggers a CSV download. + +(async () => { + const BASE = 'https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html'; + const FILTERS = [ + { grade: '8', gradeId: '771', expectedCount: 101 }, + { grade: 'A2', gradeId: '773', expectedCount: 21 }, + ]; + + // ---- helpers ---- + const fetchHtml = async (url) => { + const res = await fetch(url, { credentials: 'include' }); + if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); + return await res.text(); + }; + + const parsePage = (html) => { + const doc = new DOMParser().parseFromString(html, 'text/html'); + const items = Array.from(doc.querySelectorAll('.product-item')); + return items.map(item => { + const titleEl = item.querySelector('.items-center.justify-center.text-primary'); + const title = titleEl ? titleEl.textContent.trim() : ''; + const sku = title.includes(' - ') ? title.split(' - ').pop().trim() : ''; + const linkEl = item.querySelector('a.product-item-photo, a.product.photo'); + const url = linkEl ? linkEl.getAttribute('href') : ''; + + // pull the description block (Material / Diameter / Thread / Grade / Finish / Pack) + const block = (item.textContent || '').replace(/\s+/g, ' '); + const get = (label) => { + const m = block.match(new RegExp(label + '\\s*:?\\s*([^\\s].*?)(?=\\s+(?:Diameter|Thread|Grade|Finish|Sold|Metric|Hi-Tensile|Stainless|Mild|Brass|$))', 'i')); + return m ? m[1].trim() : ''; + }; + + const material = /Hi-Tensile/i.test(block) ? 'Hi-Tensile' + : /Stainless Steel/i.test(block) ? 'Stainless Steel' + : /Mild Steel/i.test(block) ? 'Mild Steel' + : /Brass/i.test(block) ? 'Brass' + : ''; + const diameter = get('Diameter'); + const thread = get('Thread'); + const grade = get('Grade'); + const finish = get('Finish'); + const pack = /Sold as a Single Piece/i.test(block) ? 'Single Piece' + : /Sold as a Box/i.test(block) ? 'Box' + : /Sold as a Pack/i.test(block) ? 'Pack' + : ''; + + // Unit price is in the HTML but hidden with display:none + const unitPriceEl = item.querySelector('.unit-price'); + const priceMatch = unitPriceEl ? unitPriceEl.textContent.match(/R\s*([\d.]+)/) : null; + const unitPrice = priceMatch ? priceMatch[1] : ''; + + // Description is the parent product type (e.g. "Hex Nut DIN934") + const desc = title.includes(' - ') ? title.split(' - ')[0].trim() : ''; + + // Build PosDescription like: "M39 NUT 8 PLN" + // diameter -> "M{value}", normalised + const sizeMatch = diameter.match(/(\d+(?:\.\d+)?)\s*mm/i); + const size = sizeMatch ? 'M' + sizeMatch[1].replace(/\.0$/, '') : diameter; + const gradeShort = grade === 'Stainless' ? '' : grade; // "8" or "A2" + const posDescription = [size, 'NUT', gradeShort, finish.toUpperCase() === 'PLAIN' ? 'PLN' : finish.toUpperCase()] + .filter(Boolean).join(' '); + + return { + sku, + posDescription, + description: desc, + material, + grade, + diameter, + thread, + finish, + pack, + unitPrice: unitPrice ? 'R ' + unitPrice : '', + url, + }; + }); + }; + + const allRows = []; + for (const f of FILTERS) { + // First page + const firstUrl = `${BASE}?gradematerial=${f.gradeId}`; + const firstHtml = await fetchHtml(firstUrl); + const firstRows = parsePage(firstHtml); + allRows.push(...firstRows); + + // Discover total page count from toolbar + const doc = new DOMParser().parseFromString(firstHtml, 'text/html'); + const m = (doc.querySelector('.toolbar-amount')?.textContent || '').match(/of\s+(\d+)/); + const total = m ? parseInt(m[1], 10) : f.expectedCount; + const pages = Math.ceil(total / 10); + console.log(`Grade ${f.grade}: ${firstRows.length} items on page 1, ${pages} pages total, expected ${total}`); + + // Remaining pages + for (let p = 2; p <= pages; p++) { + const html = await fetchHtml(`${BASE}?gradematerial=${f.gradeId}&p=${p}`); + const rows = parsePage(html); + allRows.push(...rows); + console.log(`Grade ${f.grade}: page ${p}/${pages} -> ${rows.length} items`); + } + } + + console.log(`TOTAL ROWS: ${allRows.length}`); + + // Build CSV + const headers = ['SKU','PosDescription','Description','Material','Grade','Diameter','Thread','Finish','Pack','UnitPrice','SourceURL']; + const esc = (v) => { + const s = (v ?? '').toString().replace(/"/g, '""'); + return /[",\n]/.test(s) ? `"${s}"` : s; + }; + const csv = [headers.join(',')] + .concat(allRows.map(r => headers.map(h => esc(r[h.toLowerCase()])).join(','))) + .join('\n'); + + // Trigger download + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'nss-hex-nut-din934-grade8-a2.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + return { count: allRows.length, sample: allRows.slice(0, 3) }; +})(); diff --git a/scripts/recent-stock-movements.ts b/scripts/recent-stock-movements.ts new file mode 100644 index 0000000..d25aa76 --- /dev/null +++ b/scripts/recent-stock-movements.ts @@ -0,0 +1,273 @@ +/** + * One-off diagnostic: list every `stock_items` row that was created + * or updated since 2026-09-14T22:00:00Z (~36h ago, "yesterday + + * today" Johannesburg time), and highlight the ones still missing + * a `cost_price` or `selling_price`. + * + * Why this exists + * ─────────────── + * The operator noticed "new" items on the POS are showing without + * prices. The recent 80% markup commit (Mon night) patched a lot + * of rows in-place via `updated`, so the "updated in window" set + * conflates two populations: + * (a) genuinely-new items added recently, and + * (b) pre-existing rows whose `selling_price` / `markup` was just + * patched by the markup run. + * This script splits those two apart in the summary block and + * then prints a full table of every in-window row that STILL has + * `cost_price <= 0` OR `selling_price <= 0` — the actual gap the + * POS is exposing. + * + * Read-only — never writes to PB. + * + * Run with: + * npx tsx scripts/recent-stock-movements.ts + * + * Requires .env.local with NEXT_PUBLIC_POCKETBASE_URL, + * PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD. + */ + +import "./_env"; // side-effect import: loads .env.local before anything else + +import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin"; + +// ─── Constants ──────────────────────────────────────────────────────── + +/** + * Lower bound of the "recently moved" window. Hard-coded to the + * Johannesburg "yesterday + today" cutoff the operator asked for + * (2026-09-14 22:00 UTC == 2026-09-15 00:00 SAST). Edit here if + * the operator wants a different window. + */ +const WINDOW_START_ISO = "2026-09-14T22:00:00.000Z"; + +// ─── Types ──────────────────────────────────────────────────────────── + +/** Single `stock_items` row shape — the fields we read for the + * report. `expand.supplier` is the joined supplier name. */ +type StockRow = { + id: string; + sku: string; + name: string; + current_qty: number; + cost_price?: number | null; + selling_price?: number | null; + markup?: number | null; + created: string; + updated: string; + supplier?: string; + expand?: { + supplier?: { id?: string; suppliername?: string }; + }; +}; + +// ─── Formatting ──────────────────────────────────────────────────────── + +/** Format a number as R X.XX, or "—" for missing/non-finite values. */ +function fmtMoney(n: number | null | undefined): string { + if (typeof n !== "number" || !Number.isFinite(n)) return "—"; + return `R ${n.toFixed(2)}`; +} + +/** Format a markup percent. PB stores markup as a plain number + * (e.g. 80 means 80%); render with a trailing "%" when present. */ +function fmtMarkup(n: number | null | undefined): string { + if (typeof n !== "number" || !Number.isFinite(n)) return "—"; + return `${n}%`; +} + +/** Truncate an ISO timestamp to "YYYY-MM-DD HH:MM" in UTC. PB's + * autodate strings are already UTC, so no conversion is needed — + * we just slice the trailing ":00.000Z" off to keep the table + * compact. */ +function fmtDateTime(iso: string): string { + if (!iso) return "—"; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + const pad = (n: number): string => String(n).padStart(2, "0"); + return ( + `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` + + ` ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}` + ); +} + +/** Render the gap table. Numeric columns are right-aligned via the + * `---:` colon convention so the operator can scan decimals. */ +function renderGapTable(rows: StockRow[]): string { + const lines: string[] = []; + lines.push( + "| SKU | Name | Supplier | Cost (R) | Selling (R) | Markup | Qty | Created | Updated |", + ); + lines.push( + "| --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- |", + ); + + for (const r of rows) { + const sku = r.sku || "(no sku)"; + const name = (r.name || "(no name)").slice(0, 60); // keep names from blowing up the table + const supplier = r.expand?.supplier?.suppliername?.trim() || "—"; + const qty = Number.isFinite(r.current_qty) ? r.current_qty : 0; + + lines.push( + `| ${sku} | ${name} | ${supplier} | ${fmtMoney(r.cost_price)} | ${fmtMoney(r.selling_price)} | ${fmtMarkup(r.markup)} | ${qty} | ${fmtDateTime(r.created)} | ${fmtDateTime(r.updated)} |`, + ); + } + + return lines.join("\n"); +} + +// ─── PB plumbing ────────────────────────────────────────────────────── + +/** + * Pull every `stock_items` row whose `created` or `updated` is on + * or after `WINDOW_START_ISO`. Both fields are autodate strings in + * PB and lexicographically compare correctly as ISO-8601 UTC, so + * the `>=` operator works without parsing. + * + * The `~ ` operator is NOT used here — PB's `~` is `contains`, + * not `regex`, and it's irrelevant for an exact-bound comparison. + * + * We do NOT pass a `fields` projection: passing `fields=` together + * with `expand=supplier` causes PB to drop the `expand` payload + * (see the comment in `list-irda-stock.ts`). Pulling the full row + * is fine here — the dataset is bounded by the ~36h window, not + * by the whole catalogue. + * + * Sort: `-updated` so the most-recently-moved rows float to the + * top. The client-side sub-sort for the gap table also uses + * `updated DESC` (and then `created DESC`) so an item patched + * moments ago reads before one patched yesterday morning. + */ +async function listRecentStockItems(windowStartIso: string): Promise { + const pb = await getAdminPocketBase(); + const rows = await pb.collection("stock_items").getFullList({ + filter: `created >= "${windowStartIso}" || updated >= "${windowStartIso}"`, + expand: "supplier", + sort: "-updated", + batch: 500, + }); + return rows; +} + +// ─── Main ───────────────────────────────────────────────────────────── + +async function main(): Promise { + const startTs = Date.now(); + const windowStartIso = WINDOW_START_ISO; + const windowStart = new Date(windowStartIso); + + console.log( + `=== Recently-moved stock items (since ${windowStartIso}) ===\n`, + ); + + const allRows = await listRecentStockItems(windowStartIso); + const totalInWindow = allRows.length; + + // Split the in-window set into "newly created" vs "only patched" + // by comparing `created` against the window start. An item whose + // `created` is BEFORE the window but whose `updated` is IN the + // window was patched (the Monday-night 80% markup run hit a lot + // of these). An item whose `created` is IN the window is + // genuinely new. + const isInWindow = (iso: string): boolean => { + if (!iso) return false; + const d = new Date(iso); + return d.getTime() >= windowStart.getTime(); + }; + + const newlyCreated = allRows.filter((r) => isInWindow(r.created)); + const updatedOnly = allRows.filter( + (r) => !isInWindow(r.created) && isInWindow(r.updated), + ); + + // The gap = cost_price <= 0 OR selling_price <= 0. PB's autodate + // for missing numerics resolves to 0 / null on the typed shape; + // we treat anything `!(x > 0)` as missing, matching the + // convention used in `apply-default-markup.ts` and + // `list-irda-stock.ts`. + const isGapRow = (r: StockRow): boolean => { + const cost = Number(r.cost_price ?? 0); + const sell = Number(r.selling_price ?? 0); + return !(cost > 0) || !(sell > 0); + }; + + const gapRows = allRows + .filter(isGapRow) + // Sort newest-created/updated first within the gap. Compare on + // the max(created, updated) so a row created last week but + // patched yesterday sorts before one created last week and + // never touched again. Falls back to created if both are + // missing. + .toSorted((a, b) => { + const aMax = Math.max( + new Date(a.created).getTime() || 0, + new Date(a.updated).getTime() || 0, + ); + const bMax = Math.max( + new Date(b.created).getTime() || 0, + new Date(b.updated).getTime() || 0, + ); + return bMax - aMax; + }); + + const missingCost = gapRows.filter((r) => !(Number(r.cost_price ?? 0) > 0)); + const missingSelling = gapRows.filter( + (r) => !(Number(r.selling_price ?? 0) > 0), + ); + // Rows that have BOTH prices populated (the "happy path" + // population — what the markup commit was aiming for). + const fullyPriced = allRows.filter( + (r) => + Number(r.cost_price ?? 0) > 0 && Number(r.selling_price ?? 0) > 0, + ); + + // Summary block. The operator specifically asked us to be loud + // about `Items missing cost_price` — if it's zero we say so. + console.log("Summary:"); + console.log(` Total items changed in window: ${totalInWindow}`); + console.log(` Items still missing cost_price: ${missingCost.length}${missingCost.length === 0 ? " ← no missing-cost rows in window; the gap is purely on the SELL side" : ""}`); + console.log(` Items still missing selling_price: ${missingSelling.length}`); + console.log(` Items with both fields populated: ${fullyPriced.length}`); + console.log(` Items created fresh (not just patched): ${newlyCreated.length}`); + console.log(` Items updated only (cost/markup patch): ${updatedOnly.length}`); + + console.log( + `\n=== Gap rows: cost_price <= 0 OR selling_price <= 0 (${gapRows.length} row${gapRows.length === 1 ? "" : "s"}) ===\n`, + ); + if (gapRows.length === 0) { + console.log( + " (none — every in-window row has both cost_price and selling_price populated)", + ); + } else { + console.log(renderGapTable(gapRows)); + } + + // A small pattern-recognition addendum: tally supplier frequency + // and qty=0 rate inside the gap, so the operator can eyeball + // whether the gap is "one supplier forgot to ship pricing" or + // "every supplier has a few stragglers". + if (gapRows.length > 0) { + const supplierCounts = new Map(); + let zeroQty = 0; + for (const r of gapRows) { + const name = r.expand?.supplier?.suppliername?.trim() || "(no supplier)"; + supplierCounts.set(name, (supplierCounts.get(name) ?? 0) + 1); + if (!(Number(r.current_qty) > 0)) zeroQty += 1; + } + const supplierLines = [...supplierCounts.entries()] + .toSorted((a, b) => b[1] - a[1]) + .map(([name, n]) => ` ${n}× ${name}`); + + console.log(`\nGap breakdown by supplier:`); + console.log(supplierLines.join("\n")); + console.log(`\nGap rows with current_qty = 0: ${zeroQty} / ${gapRows.length}`); + } + + const duration = ((Date.now() - startTs) / 1000).toFixed(1); + console.log(`\n duration: ${duration}s`); +} + +main().catch((err) => { + console.error("FATAL:", err); + process.exit(1); +}); diff --git a/src/app/api/v1/stock/items/create/route.ts b/src/app/api/v1/stock/items/create/route.ts index bfa86b8..948c887 100644 --- a/src/app/api/v1/stock/items/create/route.ts +++ b/src/app/api/v1/stock/items/create/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { assertAgentAuth } from "@/lib/server/agent-auth"; import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; +import { applyDefaultMarkupToPatch } from "@/lib/server/stock-default-markup-service"; import type { StockItem } from "@/lib/types"; /** @@ -55,7 +56,18 @@ export async function POST(req: NextRequest) { const hasMarkup = typeof markupRaw === "number" && Number.isFinite(markupRaw); - const item = await pb.collection("stock_items").create({ + // Build the patch in two stages: + // 1. The operator-supplied fields (sku, name, opening qty, + // pricing, etc). + // 2. `applyDefaultMarkupToPatch` runs over the patch — if + // `costPrice` is positive and the operator did not also + // supply `sellingPrice` / `markup`, the helper fills + // those in at 80% (rounded to R cents) and bootstraps + // `cost_history` with a single entry. Idempotent: the + // helper never overwrites an operator-set price. Pass + // `undefined` for the existing row (no row yet — this + // is the create path). + const rawPatch: Record = { sku: (body.sku || "").trim(), name: body.name.trim(), description: body.description || "", @@ -67,21 +79,15 @@ export async function POST(req: NextRequest) { is_active: true, notes: body.notes || "", ...(hasCostPrice - ? { - cost_price: costPriceRaw, - cost_history: [ - { - cost_price: costPriceRaw, - quantity: 0, - purchased_at: new Date().toISOString(), - purchase_id: "", - }, - ], - } + ? { cost_price: costPriceRaw, cost_history: [] } : { cost_history: [] }), - ...(hasSellingPrice ? { selling_price: sellingPriceRaw } : {}), - ...(hasMarkup ? { markup: markupRaw } : {}), - }); + }; + if (hasSellingPrice) rawPatch.selling_price = sellingPriceRaw; + if (hasMarkup) rawPatch.markup = markupRaw; + + const item = await pb.collection("stock_items").create( + applyDefaultMarkupToPatch(rawPatch, undefined) as Partial, + ); // Opening-balance ledger row — only when the operator seeded a // non-zero qty. Reason "adjustment" is the closest fit: it's a diff --git a/src/app/api/v1/stock/items/route.test.ts b/src/app/api/v1/stock/items/route.test.ts index b531568..1b7507c 100644 --- a/src/app/api/v1/stock/items/route.test.ts +++ b/src/app/api/v1/stock/items/route.test.ts @@ -372,6 +372,10 @@ describe("/api/v1/stock/items", () => { purchase_id: "", }); expect(typeof itemCall.cost_history[0].purchased_at).toBe("string"); + // Auto-markup fires (cost > 0, no operator-set selling/markup): + // 12.5 * 1.80 = 22.5. + expect(itemCall.selling_price).toBe(22.5); + expect(itemCall.markup).toBe(80); }); it("writes selling_price and markup through to the stock_items row when supplied", async () => { @@ -405,6 +409,9 @@ describe("/api/v1/stock/items", () => { const itemCall = create.mock.calls[0][0]; expect(itemCall.cost_price).toBeUndefined(); expect(itemCall.cost_history).toEqual([]); + // No cost → no auto-markup. + expect(itemCall.selling_price).toBeUndefined(); + expect(itemCall.markup).toBeUndefined(); }); }); diff --git a/src/lib/server/stock-booking-service.ts b/src/lib/server/stock-booking-service.ts index 743d9c7..4d4756e 100644 --- a/src/lib/server/stock-booking-service.ts +++ b/src/lib/server/stock-booking-service.ts @@ -223,6 +223,9 @@ export async function bookStockFromInvoice( .getOne(stockItemId)) as unknown as { current_qty?: number; cost_history?: unknown; + cost_price?: number; + selling_price?: number; + markup?: number; }; await pb.collection(STOCK_ITEMS).update(stockItemId, { current_qty: Number(stockItem.current_qty ?? 0) + qtyReceived, diff --git a/src/lib/server/stock-cost-service.ts b/src/lib/server/stock-cost-service.ts index 3d61c33..20f07fb 100644 --- a/src/lib/server/stock-cost-service.ts +++ b/src/lib/server/stock-cost-service.ts @@ -1,12 +1,27 @@ import type { StockCostHistoryEntry } from "@/lib/types"; +import { applyDefaultMarkupToPatch } from "./stock-default-markup-service"; /** * Build the stock-item cost patch written when a purchase line is received. * The purchase line remains the immutable source record; this keeps a compact * operational current cost plus an append-only history on the stock item. + * + * Also opportunistically seeds `selling_price` / `markup` at the + * default 80% rate when the row doesn't already have them set — + * delegates the idempotency check to + * `applyDefaultMarkupToPatch` so the operator CLI backfill and + * this write-path stay in lockstep. The `cost_history` patch + * (append + entry) is built here and is committed to the patch + * BEFORE the helper runs, so the helper's "patch already has a + * non-empty history" rule preserves it. */ export function buildStockReceiptCostPatch( - stockItem: { cost_history?: unknown }, + stockItem: { + cost_history?: unknown; + cost_price?: number | null; + selling_price?: number | null; + markup?: number | null; + }, input: { costPrice: number; quantity: number; @@ -14,7 +29,12 @@ export function buildStockReceiptCostPatch( supplierId?: string; purchaseId: string; }, -): { cost_price: number; cost_history: StockCostHistoryEntry[] } { +): { + cost_price: number; + cost_history: StockCostHistoryEntry[]; + selling_price?: number; + markup?: number; +} { const history = Array.isArray(stockItem.cost_history) ? stockItem.cost_history.filter( (entry): entry is StockCostHistoryEntry => @@ -32,8 +52,34 @@ export function buildStockReceiptCostPatch( ...(input.supplierId ? { supplier_id: input.supplierId } : {}), }; - return { + // Base patch — committed by the caller. We run it through the + // helper LAST so the helper can opportunistically add + // `selling_price` / `markup` (without touching our `cost_history` + // append). The helper's "patch owns history" guard means the + // appended entry is never overwritten. + const basePatch: Record = { cost_price: input.costPrice, cost_history: [...history, entry], }; + const finalised = applyDefaultMarkupToPatch( + basePatch, + { + cost_price: stockItem.cost_price, + selling_price: stockItem.selling_price, + markup: stockItem.markup, + cost_history: stockItem.cost_history, + }, + input.receivedAt, + ); + + return { + cost_price: input.costPrice, + cost_history: finalised.cost_history as StockCostHistoryEntry[], + ...(finalised.selling_price !== undefined + ? { selling_price: finalised.selling_price as number } + : {}), + ...(finalised.markup !== undefined + ? { markup: finalised.markup as number } + : {}), + }; } diff --git a/src/lib/server/stock-default-markup-service.test.ts b/src/lib/server/stock-default-markup-service.test.ts new file mode 100644 index 0000000..f9e8adc --- /dev/null +++ b/src/lib/server/stock-default-markup-service.test.ts @@ -0,0 +1,291 @@ +/** + * Tests for the pure default-markup helper. + * + * Mirrors the test style of `scripts/apply-default-markup.test.ts`: + * no PB stub needed (the helper is pure), exported `applyDefaultMarkupToPatch` + * + `round2` are the surface under test. Each test pins one of the + * helper's documented rules — keep them in lockstep with the + * JSDoc bullets in `stock-default-markup-service.ts`. + */ +import { describe, it, expect } from "vitest"; + +import { + applyDefaultMarkupToPatch, + round2, + DEFAULT_MARKUP_PERCENT, + DEFAULT_MARKUP_MULTIPLIER, + DEFAULT_COST_HISTORY_PURCHASE_ID, +} from "./stock-default-markup-service"; + +// A fixed clock so the optional `cost_history` seed entry's +// `purchased_at` is deterministic across runs. +const NOW = "2026-09-16T08:50:00.000Z"; + +describe("round2", () => { + it("rounds 117.39 * 1.80 to two decimals (211.302 → 211.30)", () => { + expect(round2(117.39 * DEFAULT_MARKUP_MULTIPLIER)).toBe(211.30); + }); + + it("rounds 33.33 * 1.80 (59.994 → 59.99)", () => { + expect(round2(33.33 * DEFAULT_MARKUP_MULTIPLIER)).toBe(59.99); + }); + + it("preserves a value that's already at 2dp", () => { + expect(round2(50.55 * DEFAULT_MARKUP_MULTIPLIER)).toBe(90.99); + }); + + it("returns 0 for non-finite input", () => { + expect(round2(Number.NaN)).toBe(0); + expect(round2(Number.POSITIVE_INFINITY)).toBe(0); + }); +}); + +describe("applyDefaultMarkupToPatch", () => { + // 1. Guard: cost_price = 0 in patch → helper does nothing. + it("returns the patch unchanged when cost_price in the patch is 0", () => { + const out = applyDefaultMarkupToPatch({ cost_price: 0 }, undefined, NOW); + expect(out).toEqual({ cost_price: 0 }); + expect(out.selling_price).toBeUndefined(); + expect(out.markup).toBeUndefined(); + }); + + // 2. Happy path: cost > 0, no existing row, no selling/markup in patch. + it("populates selling_price and markup when cost_price is positive and selling/markup are unset", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 100 }, + undefined, + NOW, + ); + expect(out.selling_price).toBe(180); + expect(out.markup).toBe(DEFAULT_MARKUP_PERCENT); + expect(out.cost_price).toBe(100); + }); + + // 3. Idempotency — existing row has selling_price > 0. + it("does NOT overwrite selling_price when the existing row already has a non-zero selling_price", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 100 }, + { cost_price: 100, selling_price: 150, markup: 0 }, + NOW, + ); + expect(out.selling_price).toBeUndefined(); // helper never writes the field + expect(out.markup).toBeUndefined(); // and never the markup either (selling was already set) + }); + + // 4. Idempotency — existing row has markup > 0. + it("does NOT overwrite markup when the existing row already has a non-zero markup", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 100 }, + { cost_price: 100, selling_price: 0, markup: 50 }, + NOW, + ); + expect(out.markup).toBeUndefined(); + expect(out.selling_price).toBeUndefined(); + }); + + // 5. Idempotency — patch already sets selling_price explicitly. + it("does NOT overwrite selling_price when the patch sets it to a non-zero value", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 100, selling_price: 222 }, + undefined, + NOW, + ); + expect(out.selling_price).toBe(222); // patch's value, untouched + expect(out.markup).toBeUndefined(); // operator chose the shelf price directly; markup left for them + }); + + // 5b. Idempotency — patch sets markup explicitly (and selling_price is zero). + it("does NOT add selling_price when the patch sets markup to a non-zero value", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 100, markup: 25 }, + undefined, + NOW, + ); + expect(out.markup).toBe(25); // patch's value, untouched + expect(out.selling_price).toBeUndefined(); + }); + + // 6. cost_history bootstrap — patch sets cost_price > 0, existing row has no history. + it("seeds cost_history with a single entry when cost_price is positive and the existing row has no history", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 18.5 }, + undefined, + NOW, + ); + expect(Array.isArray(out.cost_history)).toBe(true); + expect(out.cost_history).toHaveLength(1); + expect(out.cost_history).toEqual([ + { + cost_price: 18.5, + quantity: 0, + purchased_at: NOW, + purchase_id: DEFAULT_COST_HISTORY_PURCHASE_ID, + }, + ]); + }); + + it("seeds cost_history using the existing row's cost when the patch only adds markup (no patch.cost_price)", () => { + // Edge: the patch is just `{ markup: 50 }` on a row that already + // has cost_price > 0 and a populated history. The history + // preservation rule fires (don't overwrite history). The cost + // we record in the seed would be the existing row's value. + const out = applyDefaultMarkupToPatch( + { markup: 50 }, + { cost_price: 25, selling_price: 0, markup: 0, cost_history: [] }, + NOW, + ); + // Patch set markup explicitly → helper doesn't auto-set markup. + expect(out.markup).toBe(50); + // Patch did NOT set cost_price, so the cost_history bootstrap + // is a no-op (rule: only seed when the patch is the source of + // the cost). + expect(out.cost_history).toBeUndefined(); + }); + + // 7. Existing history preserved. + it("does NOT seed cost_history when the existing row already has history", () => { + const existingHistory = [ + { + cost_price: 17.0, + quantity: 5, + purchased_at: "2026-08-01T08:00:00.000Z", + purchase_id: "PO26080101STK", + }, + ]; + const out = applyDefaultMarkupToPatch( + { cost_price: 18.5 }, + { cost_price: 18.5, selling_price: 0, markup: 0, cost_history: existingHistory }, + NOW, + ); + expect(out.cost_history).toBeUndefined(); // helper didn't touch it + }); + + // 7b. cost_history=null on existing row → treated as no history → seed fires. + it("seeds cost_history when existing.cost_history is null", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 18.5 }, + { cost_price: 18.5, selling_price: 0, markup: 0, cost_history: null }, + NOW, + ); + expect(Array.isArray(out.cost_history)).toBe(true); + expect(out.cost_history).toHaveLength(1); + }); + + // 7c. Patch already has non-empty cost_history → helper doesn't seed. + it("does NOT seed cost_history when the patch already has a non-empty history (patch wins)", () => { + // The booking flow appends to existing history on the patch + // before calling the helper. The helper must not overwrite + // that — the patch's array is what we're committing to. + const appendedEntry = { + cost_price: 19.0, + quantity: 10, + purchased_at: "2026-09-01T08:00:00.000Z", + purchase_id: "PO26090101STK", + }; + const out = applyDefaultMarkupToPatch( + { cost_price: 18.5, cost_history: [appendedEntry] }, + { cost_price: 0, selling_price: 0, markup: 0, cost_history: [] }, + NOW, + ); + expect(out.cost_history).toEqual([appendedEntry]); + }); + + // 8. Rounding: cost_price = 117.39 → selling_price = 211.30. + it("rounds 117.39 → 211.30 (the exact case the operator script pins)", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 117.39 }, + undefined, + NOW, + ); + expect(out.selling_price).toBe(211.30); + expect(out.markup).toBe(DEFAULT_MARKUP_PERCENT); + }); + + // 9. Edge: cost_price = 7.80 → selling_price = 14.04. + it("rounds 7.80 → 14.04", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 7.80 }, + undefined, + NOW, + ); + expect(out.selling_price).toBe(14.04); + expect(out.markup).toBe(DEFAULT_MARKUP_PERCENT); + }); + + // 10. Edge: very small cost like 0.93 → selling_price = 1.67. + it("rounds 0.93 → 1.67 (cents-rounding sanity check)", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 0.93 }, + undefined, + NOW, + ); + expect(out.selling_price).toBe(1.67); + expect(out.markup).toBe(DEFAULT_MARKUP_PERCENT); + }); + + // Bonus: a patch that includes unrelated fields keeps them intact. + it("leaves unrelated fields in the patch untouched", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 50, name: "Widget", sku: "WID-1" }, + undefined, + NOW, + ); + expect(out.name).toBe("Widget"); + expect(out.sku).toBe("WID-1"); + expect(out.selling_price).toBe(90); // 50 * 1.80 + expect(out.markup).toBe(DEFAULT_MARKUP_PERCENT); + }); + + // Bonus: existing cost_history is an empty array → seed fires. + it("seeds cost_history when existing.cost_history is an empty array", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 9.99 }, + { cost_price: 9.99, selling_price: 0, markup: 0, cost_history: [] }, + NOW, + ); + const history = out.cost_history as unknown[]; + expect(Array.isArray(history)).toBe(true); + expect(history).toHaveLength(1); + expect((history[0] as { cost_price: number }).cost_price).toBe(9.99); + }); + + // Bonus: when no `now` is supplied, the helper still writes a + // valid ISO `purchased_at`. We don't pin a specific value + // (it's `Date.now()`-driven), just the shape. + it("generates a fresh ISO timestamp when `now` is omitted", () => { + const out = applyDefaultMarkupToPatch({ cost_price: 10 }, undefined); + const history = out.cost_history as unknown[]; + expect(Array.isArray(history)).toBe(true); + expect(typeof (history[0] as { purchased_at: unknown }).purchased_at).toBe( + "string", + ); + expect( + new Date( + (history[0] as { purchased_at: string }).purchased_at, + ).toString(), + ).not.toBe("Invalid Date"); + }); + + // Bonus: when patch.cost_price is undefined and existing has no + // cost, the helper short-circuits (no auto-markup, no history seed). + it("does nothing when neither the patch nor the existing row have a cost", () => { + const out = applyDefaultMarkupToPatch({}, undefined, NOW); + expect(out).toEqual({}); + }); + + // Bonus: when the operator pre-sets selling_price AND markup on a + // create, the helper still seeds cost_history (the audit log is + // independent from the pricing block). The helper does NOT touch + // selling_price / markup when the operator has set them. + it("preserves operator-set selling_price/markup while still seeding cost_history", () => { + const out = applyDefaultMarkupToPatch( + { cost_price: 100, selling_price: 175, markup: 75 }, + undefined, + NOW, + ); + expect(out.selling_price).toBe(175); + expect(out.markup).toBe(75); + expect(Array.isArray(out.cost_history)).toBe(true); + expect(out.cost_history).toHaveLength(1); + }); +}); diff --git a/src/lib/server/stock-default-markup-service.ts b/src/lib/server/stock-default-markup-service.ts new file mode 100644 index 0000000..8f555cb --- /dev/null +++ b/src/lib/server/stock-default-markup-service.ts @@ -0,0 +1,220 @@ +/** + * Stock default-markup helper — auto-populate `selling_price` and + * `markup` on any `stock_items` write that leaves those fields at + * zero (or absent) while setting a positive `cost_price`. + * + * Why this exists + * ─────────────── + * The catalogue has a long-standing gap: rows that have a known + * `cost_price` but never had `selling_price` / `markup` set. These + * items have a known input cost but no shelf price — the till + * can't ring them up. The companion `scripts/apply-default-markup.ts` + * CLI backfills those rows in bulk; this helper does the same + * thing inline for every service-side write that creates or + * updates a stock_item. + * + * The helper is pure (no PB I/O). Callers merge its output into + * the patch they're about to send to PB; the helper decides + * "should this patch also set selling_price + markup?" and + * (optionally) bootstraps the `cost_history` array when a brand- + * new row is being given a cost for the first time. + * + * Rules (mirrored exactly from `scripts/apply-default-markup.ts` + * — keep the two in lockstep) + * ───── + * 1. Helper fires ONLY when the post-write `cost_price` is > 0. + * Without a cost, there's nothing to base a markup on. + * 2. Helper fires ONLY when the post-write `selling_price` is + * zero or absent (no operator override of the shelf price). + * 3. Helper fires ONLY when the post-write `markup` is zero or + * absent (no operator override of the POS markup field). + * 4. Never overwrite a non-zero `selling_price` or `markup` + * anywhere — neither on the existing row, nor in the patch. + * Idempotency is the whole point. + * 5. Optionally seeds `cost_history` with a single entry when: + * - the proposed patch sets `cost_price` to a positive value, AND + * - the existing row has no `cost_history` at all (null/undefined + * or empty array), AND + * - the patch hasn't already set a non-empty `cost_history` + * (the patch wins — if the caller is appending to an existing + * history, don't overwrite it). + * When the existing row already has history, preserve it (the + * booking flow keeps appending to it). When the patch doesn't + * set `cost_price` but the existing row has one, leave the + * existing history alone. + * 6. The amount we compare for the cost_history bootstrap uses + * the post-write `cost_price` (i.e. the patch's value if + * provided, otherwise the existing row's value). + * + * Defaults — keep these aligned with the operator script + * ────────────────────────────────────────────────────── + * `DEFAULT_MARKUP_PERCENT = 80` and `DEFAULT_MARKUP_MULTIPLIER + * = 1.80`. Tweak only this one constant if the rate changes; + * re-run the operator script with `--commit` to backfill any + * existing rows that pre-date the change. + */ +import type { StockCostHistoryEntry } from "@/lib/types"; + +export const DEFAULT_MARKUP_PERCENT = 80; +export const DEFAULT_MARKUP_MULTIPLIER = 1.80; + +/** + * Matches the seed shape used by `stock-quick-draft-service.ts` and + * `/api/v1/stock/items/create` — `purchase_id` is the empty string + * for "we knew the price when we typed it, no PO row to point at + * yet"; the accountant's booking flow appends a second entry with a + * real `purchase_id` when the supplier invoice is booked. + */ +export const DEFAULT_COST_HISTORY_PURCHASE_ID = ""; + +/** + * The patch shape every caller hands us. Loose so callers can pass + * an existing `Record` (the same shape `pb.update` + * and `pb.create` accept) without us forcing them to cast. + */ +export type StockPricingPatch = Record; + +/** + * The minimal existing-row shape the helper needs. Every field is + * optional because (a) on a create there's no existing row at all + * (pass `undefined`), and (b) PB's typed SDK may surface unset + * numerics as `null` / `undefined`. The helper normalises those + * to zero internally. + */ +export type ExistingStockPricingRow = { + cost_price?: number | null | undefined; + selling_price?: number | null | undefined; + markup?: number | null | undefined; + cost_history?: unknown; +}; + +/** Round to 2 decimal places (R cents). Uses the rounding-to-even + * behaviour of `Math.round` after a scale shift — sufficient for + * currency display; PB stores the number as a plain float + * underneath. Mirrors `apply-default-markup.ts`. */ +export function round2(n: number): number { + if (!Number.isFinite(n)) return 0; + return Math.round(n * 100) / 100; +} + +/** + * Decide whether the proposed write should auto-populate + * `selling_price` and `markup`, and (optionally) seed + * `cost_history` on a brand-new row that has never had a cost + * history. + * + * Pure: no `Date.now()` at module level. The `now` argument lets + * tests pin the `purchased_at` timestamp; production callers pass + * `undefined` to get the current wall-clock. + * + * The function MUTATES the input patch (returning the same + * reference for chaining convenience) — callers that want a + * fresh object should pass a clone. We mutate (instead of + * returning a new object) so the existing test pattern of + * "inspect the captured patch" at the PB boundary keeps working + * with one fewer copy step. + * + * @param patch The patch the caller is about to send to PB. + * May already set `selling_price` / `markup` / + * `cost_history` / `cost_price` — those values + * are honoured. The function ONLY writes the + * pricing fields when they're zero or absent. + * @param existing The row being updated, or `undefined` on + * create. Used for the idempotency check + * (don't clobber an operator-set price) and + * for the cost_history bootstrap (only seed + * when the row has never had a history). + * @param now ISO timestamp for the optional `cost_history` + * seed entry's `purchased_at`. Defaults to + * `new Date().toISOString()`. + */ +export function applyDefaultMarkupToPatch( + patch: StockPricingPatch, + existing?: ExistingStockPricingRow, + now?: string, +): StockPricingPatch { + // 1. Resolve the post-write cost_price. The patch wins if it + // sets the field; otherwise we use the existing row's + // value. This is the "what cost are we committing to on + // this write?" — the helper fires only when that's > 0. + const patchedCostRaw = patch.cost_price; + const existingCost = existing ? Number(existing.cost_price ?? 0) : 0; + const postWriteCost = + patchedCostRaw === undefined ? existingCost : Number(patchedCostRaw ?? 0); + + // Guard 1: no cost → no markup. Nothing to base a price on. + if (!(postWriteCost > 0)) { + return patch; + } + + // 2. Resolve the post-write selling_price. Patch wins; + // otherwise the existing row's value (or 0). + const patchedSellingRaw = patch.selling_price; + const existingSelling = existing ? Number(existing.selling_price ?? 0) : 0; + const postWriteSelling = + patchedSellingRaw === undefined + ? existingSelling + : Number(patchedSellingRaw ?? 0); + + // Resolve the post-write markup. Same pattern. + const patchedMarkupRaw = patch.markup; + const existingMarkup = existing ? Number(existing.markup ?? 0) : 0; + const postWriteMarkup = + patchedMarkupRaw === undefined + ? existingMarkup + : Number(patchedMarkupRaw ?? 0); + + // Guard 2 + 3: operator override on the shelf price OR the + // POS markup field short-circuits the helper. We never + // overwrite either — they're both authoritative the moment + // they go non-zero. + const patchSetsSellingExplicitly = + patchedSellingRaw !== undefined && postWriteSelling > 0; + const patchSetsMarkupExplicitly = + patchedMarkupRaw !== undefined && postWriteMarkup > 0; + const existingSellingAlreadySet = postWriteSelling > 0; + const existingMarkupAlreadySet = postWriteMarkup > 0; + + const shouldAutoMarkup = + !patchSetsSellingExplicitly && + !patchSetsMarkupExplicitly && + !existingSellingAlreadySet && + !existingMarkupAlreadySet; + + if (shouldAutoMarkup) { + patch.selling_price = round2(postWriteCost * DEFAULT_MARKUP_MULTIPLIER); + patch.markup = DEFAULT_MARKUP_PERCENT; + } + + // 3. Optionally seed cost_history. Only when: + // - the PATCH is setting cost_price to a positive value + // (not when we're just inheriting an existing cost), AND + // - the existing row has no cost_history at all, AND + // - the patch itself hasn't already set a non-empty + // `cost_history` array (the patch wins — if the caller is + // appending to an existing history, don't overwrite it). + // The seed entry uses the post-write cost (so the patch's + // value wins on a create, the existing row's wins on an + // update). `purchase_id` is the empty string for the seed — + // the booking flow will append a real `purchase_id` later. + const existingHistory = existing?.cost_history; + const hasExistingHistory = + Array.isArray(existingHistory) && existingHistory.length > 0; + const patchIsSettingCost = + patchedCostRaw !== undefined && postWriteCost > 0; + const patchOwnsHistory = + Array.isArray(patch.cost_history) && + (patch.cost_history as unknown[]).length > 0; + + if (patchIsSettingCost && !hasExistingHistory && !patchOwnsHistory) { + const seedEntry: StockCostHistoryEntry = { + cost_price: postWriteCost, + quantity: 0, + purchased_at: now ?? new Date().toISOString(), + purchase_id: DEFAULT_COST_HISTORY_PURCHASE_ID, + }; + patch.cost_history = [seedEntry]; + } + + return patch; +} diff --git a/src/lib/server/stock-items-service.test.ts b/src/lib/server/stock-items-service.test.ts index eae4250..7c0f521 100644 --- a/src/lib/server/stock-items-service.test.ts +++ b/src/lib/server/stock-items-service.test.ts @@ -3,11 +3,13 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; // --- PocketBase mock --------------------------------------------------------- // // Hoisted mock so the service module can call `pb.collection(...)`. -// We seed two in-memory maps: `stock_items` rows and a small `suppliers` -// lookup so the `expand: "supplier"` call returns a populated +// We seed two in-memory maps: `stock_items` rows, `stock_ledger` +// rows (for the bulk-import flow), and a small `suppliers` lookup +// so the `expand: "supplier"` call returns a populated // `expand.supplier.suppliername` for the service's JS-side sort. const items = new Map>(); +const ledger = new Map>(); const suppliers = new Map>(); const getFullList = vi.fn(async (opts: { filter?: string; sort?: string; expand?: string } = {}) => { @@ -63,10 +65,32 @@ const update = vi.fn(async (id: string, patch: Record) => { return { ...row }; }); +let createSeq = 0; + +const create = vi.fn(async (data: Record) => { + createSeq++; + const id = `mock-${createSeq}`; + const row = { + id, + created: new Date().toISOString(), + updated: new Date().toISOString(), + ...data, + }; + // The bulk-import flow writes to `stock_ledger` for qty + // adjustments and cost moves. Other collections are + // out-of-scope for the existing tests; we track the create + // call so callers can inspect what was written. + if (data.stock_item !== undefined) { + ledger.set(id, row); + } + return row; +}); + const pb = { collection: (id: string) => { - if (id !== "stock_items") throw new Error(`unexpected collection: ${id}`); - return { getFullList, getOne, update }; + if (id === "stock_items") return { getFullList, getOne, update, create }; + if (id === "stock_ledger") return { create }; + throw new Error(`unexpected collection: ${id}`); }, }; @@ -77,6 +101,9 @@ vi.mock("@/lib/server/pocketbase-admin", () => ({ import { listStockItemsForStocktake, updateStockItemWithPb, + buildBulkImportPlan, + executeBulkImportPlan, + type ParsedImportRow, } from "./stock-items-service"; import type { StockItem } from "@/lib/types"; @@ -90,10 +117,13 @@ function seedItem(id: string, fields: Record) { beforeEach(() => { items.clear(); + ledger.clear(); suppliers.clear(); getFullList.mockClear(); getOne.mockClear(); update.mockClear(); + create.mockClear(); + createSeq = 0; }); // --- Tests ------------------------------------------------------------------- @@ -365,3 +395,242 @@ describe("updateStockItemWithPb", () => { expect(update).not.toHaveBeenCalled(); }); }); + +// --- executeBulkImportPlan (auto-markup wiring) ----------------------------- + +describe("executeBulkImportPlan (auto-markup wiring)", () => { + const ACTOR = { id: "user-1", name: "David Els" }; + const existingById = new Map(); + const existingBySku = new Map(); + + function refreshLookupMaps() { + existingById.clear(); + existingBySku.clear(); + for (const row of items.values()) { + if (!row.id || !row.sku) continue; + existingById.set(String(row.id), row as unknown as StockItem); + existingBySku.set(String(row.sku), row as unknown as StockItem); + } + } + + /** Build a single-row create plan via the planner, for the auto-markup wiring tests. */ + function planForRow(row: ParsedImportRow) { + refreshLookupMaps(); + return buildBulkImportPlan({ + rows: [row], + existingById, + existingBySku, + supplierLookup: new Map(), + }); + } + + it("auto-populates selling_price + markup on create when cost_price > 0 and selling/markup are blank", async () => { + const row: ParsedImportRow = { + __rowNumber: 2, + id: "", + sku: "NEW-001", + name: "Widget A", + cost_price: "50", + }; + const plan = planForRow(row); + expect(plan.plans).toHaveLength(1); + const first = plan.plans[0]; + expect(first?.kind).toBe("create"); + if (first?.kind !== "create") return; + + const result = await executeBulkImportPlan({ + plan, + actor: ACTOR, + pb: pb as never, + }); + expect(result.ok).toBe(true); + expect(result.created).toHaveLength(1); + + // Inspect the captured create call to PB: it must include + // selling_price = round(50 * 1.80, 2) = 90 and markup = 80. + const createCall = create.mock.calls.find( + (c) => (c[0] as { sku?: string }).sku === "NEW-001", + ); + expect(createCall).toBeDefined(); + const payload = createCall?.[0] as Record; + expect(payload.cost_price).toBe(50); + expect(payload.selling_price).toBe(90); + expect(payload.markup).toBe(80); + // cost_history should also be bootstrapped with a single entry. + expect(Array.isArray(payload.cost_history)).toBe(true); + expect((payload.cost_history as unknown[])).toHaveLength(1); + }); + + it("does NOT auto-set selling_price on create when the spreadsheet already supplies it", async () => { + const row: ParsedImportRow = { + __rowNumber: 2, + id: "", + sku: "NEW-002", + name: "Widget B", + cost_price: "100", + selling_price: "175", + }; + const plan = planForRow(row); + const result = await executeBulkImportPlan({ + plan, + actor: ACTOR, + pb: pb as never, + }); + expect(result.ok).toBe(true); + + const createCall = create.mock.calls.find( + (c) => (c[0] as { sku?: string }).sku === "NEW-002", + ); + const payload = createCall?.[0] as Record; + // Operator-supplied selling_price wins. + expect(payload.selling_price).toBe(175); + // markup was not set by the operator in the spreadsheet → + // planner's default (0) flows through; helper doesn't add + // anything because selling_price was explicit. + expect(payload.markup).toBe(0); + }); + + it("auto-populates selling_price + markup on update when the existing row has none", async () => { + seedItem("si-1", { + sku: "OLD-001", + name: "Existing Widget", + cost_price: 0, + selling_price: 0, + markup: 0, + cost_history: [], + }); + // The operator's spreadsheet changes cost_price to 50 (and + // leaves selling/markup blank — operator-style "I'll set the + // cost, you figure out the shelf price"). + const row: ParsedImportRow = { + __rowNumber: 2, + id: "si-1", + sku: "OLD-001", + name: "Existing Widget", + cost_price: "50", + }; + const plan = planForRow(row); + expect(plan.plans).toHaveLength(1); + expect(plan.plans[0]?.kind).toBe("update"); + + const result = await executeBulkImportPlan({ + plan, + actor: ACTOR, + pb: pb as never, + }); + expect(result.ok).toBe(true); + expect(result.updated).toHaveLength(1); + + // The captured update must include the auto-markup fields + // alongside the operator-supplied cost_price. + expect(update).toHaveBeenCalledTimes(1); + const updatePayload = update.mock.calls[0][1] as Record; + expect(updatePayload.cost_price).toBe(50); + expect(updatePayload.selling_price).toBe(90); // 50 * 1.80 + expect(updatePayload.markup).toBe(80); + }); + + it("does NOT auto-set selling_price on update when the existing row already has it", async () => { + seedItem("si-2", { + sku: "OLD-002", + name: "Existing Widget (priced)", + cost_price: 50, + selling_price: 99.99, + markup: 99.98, // legacy operator-set markup + cost_history: [ + { + cost_price: 50, + quantity: 5, + purchased_at: "2026-08-01T08:00:00.000Z", + purchase_id: "PO26080101STK", + }, + ], + }); + // Operator changes cost_price from 50 → 60 (cost-move + // scenario). Selling_price and markup are intentionally left + // blank in the spreadsheet — but the existing row has them, + // so the helper MUST NOT touch them. + const row: ParsedImportRow = { + __rowNumber: 2, + id: "si-2", + sku: "OLD-002", + name: "Existing Widget (priced)", + cost_price: "60", + }; + const plan = planForRow(row); + + const result = await executeBulkImportPlan({ + plan, + actor: ACTOR, + pb: pb as never, + }); + expect(result.ok).toBe(true); + + const updatePayload = update.mock.calls[0][1] as Record; + expect(updatePayload.cost_price).toBe(60); + // selling_price / markup NOT in the patch (helper didn't touch + // them because the existing row had non-zero values for both). + expect(updatePayload.selling_price).toBeUndefined(); + expect(updatePayload.markup).toBeUndefined(); + }); + + it("does NOT auto-set markup when the patch already sets it to a non-zero value", async () => { + seedItem("si-3", { + sku: "OLD-003", + name: "Existing Widget (markup-only)", + cost_price: 50, + selling_price: 0, + markup: 0, + cost_history: [], + }); + // Operator sets markup to 30 but leaves selling_price blank. + // The patch's markup is explicit and non-zero → helper + // short-circuits (per the rule "never overwrite a non-zero + // markup"). + const row: ParsedImportRow = { + __rowNumber: 2, + id: "si-3", + sku: "OLD-003", + name: "Existing Widget (markup-only)", + markup: "30", + }; + const plan = planForRow(row); + + const result = await executeBulkImportPlan({ + plan, + actor: ACTOR, + pb: pb as never, + }); + expect(result.ok).toBe(true); + + const updatePayload = update.mock.calls[0][1] as Record; + expect(updatePayload.markup).toBe(30); // patch's value, untouched + // No selling_price — operator chose markup only; helper + // didn't add it because markup was explicit. + expect(updatePayload.selling_price).toBeUndefined(); + }); + + it("rounds selling_price to 2dp (117.39 → 211.30)", async () => { + const row: ParsedImportRow = { + __rowNumber: 2, + id: "", + sku: "NEW-RND", + name: "Widget Rounding", + cost_price: "117.39", + }; + const plan = planForRow(row); + const result = await executeBulkImportPlan({ + plan, + actor: ACTOR, + pb: pb as never, + }); + expect(result.ok).toBe(true); + + const createCall = create.mock.calls.find( + (c) => (c[0] as { sku?: string }).sku === "NEW-RND", + ); + const payload = createCall?.[0] as Record; + expect(payload.selling_price).toBe(211.30); + expect(payload.markup).toBe(80); + }); +}); diff --git a/src/lib/server/stock-items-service.ts b/src/lib/server/stock-items-service.ts index 85816df..45ea09a 100644 --- a/src/lib/server/stock-items-service.ts +++ b/src/lib/server/stock-items-service.ts @@ -1,4 +1,5 @@ import type { StockItem, Supplier } from "@/lib/types"; +import { applyDefaultMarkupToPatch } from "./stock-default-markup-service"; import * as XLSX from "xlsx"; /** @@ -937,10 +938,23 @@ export async function executeBulkImportPlan(args: { if (supplierLookup && typeof createFields.supplier === "string" && createFields.supplier) { // Already an id (the planner resolves). No-op. } - const createdRow = (await pb.collection("stock_items").create({ - ...createFields, - cost_history: [], - })) as { id: string; sku?: string; name?: string }; + // Route the create through `applyDefaultMarkupToPatch` + // so a positive cost_price auto-populates selling_price + + // markup at 80% (idempotent — never overwrites an + // operator-set value) and bootstraps `cost_history` when + // the row has none. The planner has already collapsed + // any selling_price / markup fields the operator typed + // in the spreadsheet — those win over the helper. + const createPatch = applyDefaultMarkupToPatch( + { + ...createFields, + cost_history: [], + }, + undefined, + ); + const createdRow = (await pb.collection("stock_items").create( + createPatch, + )) as { id: string; sku?: string; name?: string }; created.push({ rowNumber: p.rowNumber, stockItemId: createdRow.id, @@ -950,6 +964,41 @@ export async function executeBulkImportPlan(args: { continue; } if (p.kind === "update") { + // 0. Load the existing row so the helper can honour + // idempotency (don't auto-set selling_price / markup + // if the row already has them, and don't seed + // cost_history if the row already has history). + // Best-effort: if the row was deleted between plan + // build and write, we fall through with no existing + // data and the helper treats the patch as a fresh row. + let existingItem: { + cost_price?: number; + selling_price?: number; + markup?: number; + cost_history?: unknown; + } | undefined; + try { + const fetched = (await pb + .collection("stock_items") + .getOne(p.stockItemId)) as Record; + existingItem = { + cost_price: + typeof fetched.cost_price === "number" + ? (fetched.cost_price as number) + : undefined, + selling_price: + typeof fetched.selling_price === "number" + ? (fetched.selling_price as number) + : undefined, + markup: + typeof fetched.markup === "number" + ? (fetched.markup as number) + : undefined, + cost_history: fetched.cost_history, + }; + } catch { + existingItem = undefined; + } // 1. If qty changed, write a stock_ledger row first. let ledgerId: string | undefined; if (p.qtyDelta !== 0) { @@ -985,7 +1034,7 @@ export async function executeBulkImportPlan(args: { // supplier *name* (for human-readable diffs), but PB // stores the relation *id*. Resolve the name → id // here. Any other field is patched as-is. - const patch: Record = {}; + const rawPatch: Record = {}; for (const change of p.changes) { if (change.field === "supplier") { if (typeof change.after === "string" && change.after) { @@ -995,14 +1044,27 @@ export async function executeBulkImportPlan(args: { `supplier "${change.after}" not found — add it in WFDASH first or fix the spelling`, ); } - patch.supplier = id; + rawPatch.supplier = id; } else { - patch.supplier = ""; + rawPatch.supplier = ""; } } else { - patch[change.field] = change.after; + rawPatch[change.field] = change.after; } } + // Route through `applyDefaultMarkupToPatch` so that an + // operator who fills in a `cost_price` but leaves + // `selling_price` / `markup` blank in the spreadsheet + // still gets the auto-markup applied at write time. The + // helper sees the existing row (read above) so it can + // honour idempotency — never overwrites an operator-set + // price or an existing non-empty `markup`. + const patch = applyDefaultMarkupToPatch(rawPatch, { + cost_price: existingItem?.cost_price, + selling_price: existingItem?.selling_price, + markup: existingItem?.markup, + cost_history: existingItem?.cost_history, + }); await pb.collection("stock_items").update(p.stockItemId, patch); updated.push({ rowNumber: p.rowNumber, diff --git a/src/lib/server/stock-quick-draft-service.test.ts b/src/lib/server/stock-quick-draft-service.test.ts index 86be6f1..d6b737d 100644 --- a/src/lib/server/stock-quick-draft-service.test.ts +++ b/src/lib/server/stock-quick-draft-service.test.ts @@ -567,6 +567,10 @@ describe("addQuickDraftLine", () => { purchase_id: "", }); expect(typeof history[0].purchased_at).toBe("string"); + // Auto-markup fires: cost > 0, no operator-set selling/markup. + // 7.25 * 1.80 = 13.05. + expect(items[0].selling_price).toBe(13.05); + expect(items[0].markup).toBe(80); }); it("does not seed cost_price when newStockItem.costPrice is omitted (legacy behaviour)", async () => { @@ -589,6 +593,9 @@ describe("addQuickDraftLine", () => { // empty cost_history. Existing callers see no change. expect(items[0].cost_price).toBeUndefined(); expect(items[0].cost_history).toEqual([]); + // No cost → no auto-markup either. + expect(items[0].selling_price).toBeUndefined(); + expect(items[0].markup).toBeUndefined(); }); it("backfills cost_price + appends cost_history when the existing stock_item has cost_price = 0", async () => { @@ -622,6 +629,9 @@ describe("addQuickDraftLine", () => { quantity: 0, purchase_id: "", }); + // Auto-markup fires on the backfill: 18.5 * 1.80 = 33.30. + expect(updated.selling_price).toBe(33.3); + expect(updated.markup).toBe(80); }); it("does not clobber an existing non-zero cost_price, but still appends to cost_history", async () => { @@ -663,6 +673,10 @@ describe("addQuickDraftLine", () => { quantity: 0, purchase_id: "", }); + // The row had cost_price=22 but selling_price=0 and markup=0, + // so the helper fills them in at 80% — 22 * 1.80 = 39.60. + expect(updated.selling_price).toBe(39.6); + expect(updated.markup).toBe(80); }); it("rejects non-positive quantities", async () => { diff --git a/src/lib/server/stock-quick-draft-service.ts b/src/lib/server/stock-quick-draft-service.ts index 626d467..8146d20 100644 --- a/src/lib/server/stock-quick-draft-service.ts +++ b/src/lib/server/stock-quick-draft-service.ts @@ -8,6 +8,7 @@ */ import type PocketBase from "pocketbase"; import type { StockCostHistoryEntry } from "@/lib/types"; +import { applyDefaultMarkupToPatch } from "./stock-default-markup-service"; import { generateStockPurchaseNumber } from "./stock-purchases-service"; const STOCK_ITEMS = "stock_items"; @@ -435,27 +436,16 @@ export async function addQuickDraftLine( const inlineCost = input.newStockItem.costPrice; const hasInlineCost = typeof inlineCost === "number" && Number.isFinite(inlineCost) && inlineCost >= 0; - // Bootstrap a one-entry cost_history when the buyer supplied - // an inline cost on a brand-new item. Matches - // `StockCostHistoryEntry` shape (see `lib/types.ts`): - // `cost_price`, `quantity` (0 — the buyer hasn't booked the - // invoice yet), `purchased_at` (now, ISO), `purchase_id` ("" — - // this is a manual seed, not tied to a stock_purchase row). - // The accountant's `bookStockFromInvoice` flow appends a - // second entry with a real purchase_id when the supplier - // invoice is booked — so the inline seed is just the "we - // knew the price when we typed it" snapshot. - const seedHistory: StockCostHistoryEntry[] = hasInlineCost - ? [ - { - cost_price: inlineCost as number, - quantity: 0, - purchased_at: new Date().toISOString(), - purchase_id: "", - }, - ] - : []; - const created = await pb.collection(STOCK_ITEMS).create({ + // Build the operator-supplied portion of the patch, then run + // `applyDefaultMarkupToPatch` over it. When `inlineCost` is + // positive AND the operator didn't separately override the + // shelf price or markup, the helper auto-populates + // `selling_price = round(cost * 1.80, 2)` and `markup = 80`. + // Same idempotency rules as the operator CLI backfill: + // never overwrites an operator-set price. Pass `undefined` + // for the existing row (no row yet — this is the create + // path). + const inlinePatch: Record = { name: input.newStockItem.name, sku: input.newStockItem.sku, description: input.newStockItem.description ?? "", @@ -463,10 +453,12 @@ export async function addQuickDraftLine( current_qty: 0, is_active: true, supplier: input.supplierId, - ...(hasInlineCost - ? { cost_price: inlineCost, cost_history: seedHistory } - : { cost_history: [] }), - }); + cost_history: [], + }; + if (hasInlineCost) inlinePatch.cost_price = inlineCost; + const created = await pb.collection(STOCK_ITEMS).create( + applyDefaultMarkupToPatch(inlinePatch, undefined), + ); stockItemId = String(created.id); inlineCreated = true; } catch (err) { @@ -521,9 +513,17 @@ export async function addQuickDraftLine( // - On the inline-create path the seed step above already // handled `cost_price` + `cost_history` (or intentionally // skipped both). Skip the backfill so we don't double-write. + // - The patch is then run through `applyDefaultMarkupToPatch` + // with the existing row's selling_price / markup / cost. + // When the row has a positive cost AND no operator-set + // selling/markup, the helper fills them in at 80%. The + // helper does NOT touch `cost_history` when the patch has + // already appended an entry — the appended entry wins. if (!inlineCreated) { try { const existingCostPrice = Number(stockItem.cost_price ?? 0); + const existingSellingPrice = Number(stockItem.selling_price ?? 0); + const existingMarkup = Number(stockItem.markup ?? 0); const existingHistory = Array.isArray(stockItem.cost_history) ? (stockItem.cost_history as unknown as StockCostHistoryEntry[]) : []; @@ -535,16 +535,19 @@ export async function addQuickDraftLine( purchased_at: new Date().toISOString(), purchase_id: "", }; - const patch: { - cost_price?: number; - cost_history: StockCostHistoryEntry[]; - } = { + const basePatch: Record = { cost_history: [...existingHistory, newEntry], }; if (shouldBackfill) { - patch.cost_price = input.costPrice; + basePatch.cost_price = input.costPrice; } - await pb.collection(STOCK_ITEMS).update(finalStockItemId, patch); + const finalPatch = applyDefaultMarkupToPatch(basePatch, { + cost_price: existingCostPrice, + selling_price: existingSellingPrice, + markup: existingMarkup, + cost_history: stockItem.cost_history, + }); + await pb.collection(STOCK_ITEMS).update(finalStockItemId, finalPatch); } catch { // Cost-patch failures must NOT block the line append. The line // is the buyer's primary intent; the cost backfill is a