Files
QuoteForge/bible/09-vendor-spec.md
Michael Chus 5e56f386cc feat: implement vendor spec BOM import and PN→LOT resolution (Phase 1)
- 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>
2026-02-21 10:22:22 +03:00

4.7 KiB
Raw Blame History

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:

  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)

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.