# 09 — Vendor Spec (BOM Import) ## Overview The vendor spec feature allows importing a vendor BOM (Bill of Materials) into a configuration. It maps vendor part numbers (PN) to internal LOT names using an active partnumber book (snapshot pulled from PriceForge), then aggregates quantities to populate the Estimate (cart). ## Architecture ### Storage | Data | Storage | Sync direction | |------|---------|---------------| | `vendor_spec` JSON | `local_configurations.vendor_spec` (TEXT/JSON) | Two-way via `pending_changes` | | Partnumber book snapshots | `local_partnumber_books` + `local_partnumber_book_items` | Pull-only from PriceForge | `vendor_spec` is a JSON array of `VendorSpecItem` objects stored inside the configuration row. It syncs to MariaDB `qt_configurations.vendor_spec` via the existing pending_changes mechanism. ### `vendor_spec` JSON Schema ```json [ { "sort_order": 10, "vendor_partnumber": "ABC-123", "quantity": 2, "description": "...", "unit_price": 4500.00, "total_price": 9000.00, "resolved_lot_name": "LOT_A", "resolution_source": "book", "manual_lot_suggestion": null } ] ``` `resolution_source` values: `"book"` | `"manual_suggestion"` | `"unresolved"` `manual_lot_suggestion` stores the user's inline LOT input — scoped to this configuration only, never written to the global book. --- ## Resolution Algorithm (3-step) For each `vendor_partnumber` in the BOM: 1. **Active book lookup** — query `local_partnumber_book_items WHERE book_id = activeBook.id AND partnumber = ?`. If found → `resolved_lot_name = match.lot_name`, `resolution_source = "book"`. 2. **Manual suggestion** — if `manual_lot_suggestion` is non-empty (user typed it before) → pre-fill as grey suggestion, `resolution_source = "manual_suggestion"`. 3. **Unresolved** — red background, inline autocomplete input shown to user. --- ## Qty Aggregation Logic After resolution, qty per LOT is computed as: ``` qty(lot) = SUM(quantity of rows where is_primary_pn=1 AND resolved_lot=lot) if at least one primary PN for this lot was found in BOM = 1 if only non-primary PNs for this lot were found ``` Examples (book mapping: LOT_A → x1[primary], x2, x3): - BOM: x2×1, x3×2 → 1×LOT_A (no primary PN found) - BOM: x1×2, x2×1 → 2×LOT_A (primary qty=2) - BOM: x1×1, x2×3 → 1×LOT_A (primary qty=1) --- ## UI: Three Top-Level Tabs The configurator (`/configurator`) has three tabs: 1. **Estimate** — existing cart/component configurator (unchanged) 2. **BOM вендора** — paste from Excel, auto-resolution, manual LOT input for unresolved rows, `Пересчитать эстимейт` button 3. **Ценообразование** — pricing summary table: vendor price | Estimate | Warehouse | Competitors; custom price input with `= сумма цен вендора` shortcut BOM is shared between tabs 2 and 3. The `Пересчитать эстимейт` action shows a confirmation dialog before overwriting the cart. ### Excel Paste Detection - 2 columns → `[PN, qty]` - 3+ columns → first col=PN, first numeric col=qty, subsequent numeric cols=prices - If row 1 has non-numeric qty → treat as header and skip --- ## API Endpoints | Method | URL | Description | |--------|-----|-------------| | GET | `/api/configs/:uuid/vendor-spec` | Fetch stored BOM | | PUT | `/api/configs/:uuid/vendor-spec` | Replace BOM (full update) | | POST | `/api/configs/:uuid/vendor-spec/resolve` | Resolve PNs → LOTs (no cart mutation) | | POST | `/api/configs/:uuid/vendor-spec/apply` | Apply resolved LOTs to cart items | | GET | `/api/partnumber-books` | List local book snapshots | | GET | `/api/partnumber-books/:id` | Items for a book by server ID | | POST | `/api/sync/partnumber-books` | Pull book snapshots from server | --- ## MariaDB Schema (managed by PriceForge) ```sql CREATE TABLE qt_partnumber_books ( id INT AUTO_INCREMENT PRIMARY KEY, version VARCHAR(50) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, is_active TINYINT(1) NOT NULL DEFAULT 1 ); CREATE TABLE qt_partnumber_book_items ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, partnumber VARCHAR(255) NOT NULL, lot_name VARCHAR(255) NOT NULL, is_primary_pn TINYINT(1) NOT NULL DEFAULT 0, description VARCHAR(10000) NULL, INDEX idx_book_pn (book_id, partnumber), FOREIGN KEY (book_id) REFERENCES qt_partnumber_books(id) ); ALTER TABLE qt_configurations ADD COLUMN vendor_spec JSON NULL; ``` QuoteForge only reads (`SELECT`) from `qt_partnumber_books` and `qt_partnumber_book_items`. Writes are managed exclusively by PriceForge.