- Migration 029: local_partnumber_books, local_partnumber_book_items, vendor_spec TEXT column on local_configurations - Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec, VendorSpecItem with JSON Valuer/Scanner - Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber, SaveBook/Items, ListBooks, CountBookItems) - Service: VendorSpecResolver 3-step resolution (book → manual suggestion → unresolved) + AggregateLOTs with is_primary_pn qty logic - Sync: PullPartnumberBooks append-only pull from qt_partnumber_books - Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler - Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books, /api/sync/partnumber-books, /partnumber-books page - UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste, PN resolution, inline LOT autocomplete, pricing table - Bible: 03-database.md updated, 09-vendor-spec.md added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.7 KiB
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
[
{
"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:
- 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". - Manual suggestion — if
manual_lot_suggestionis non-empty (user typed it before) → pre-fill as grey suggestion,resolution_source = "manual_suggestion". - 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:
- Estimate — existing cart/component configurator (unchanged)
- BOM вендора — paste from Excel, auto-resolution, manual LOT input for unresolved rows,
Пересчитать эстимейтbutton - Ценообразование — 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)
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.