# 09 - Vendor BOM ## Storage contract Vendor BOM is stored in `local_configurations.vendor_spec` and synced with `qt_configurations.vendor_spec`. Each row uses this canonical shape: ```json { "sort_order": 10, "vendor_partnumber": "ABC-123", "quantity": 2, "description": "row description", "unit_price": 4500.0, "total_price": 9000.0, "lot_mappings": [ { "lot_name": "LOT_A", "quantity_per_pn": 1 } ] } ``` Rules: - `lot_mappings[]` is the only persisted PN -> LOT mapping contract; - QuoteForge does not use legacy BOM tables; - apply flow rebuilds cart rows from `lot_mappings[]`. ## Partnumber books Partnumber books are pull-only snapshots from PriceForge. Local tables: - `local_partnumber_books` - `local_partnumber_book_items` Server tables: - `qt_partnumber_books` - `qt_partnumber_book_items` Resolution flow: 1. load the active local book; 2. find `vendor_partnumber`; 3. copy `lots_json` into `lot_mappings[]`; 4. keep unresolved rows editable in the UI. ## CFXML import `POST /api/projects/:uuid/vendor-import` imports one vendor workspace into an existing project. Rules: - accepted file field is `file`; - maximum file size is `1 GiB`; - one `ProprietaryGroupIdentifier` becomes one QuoteForge configuration; - software rows stay inside their hardware group and never become standalone configurations; - primary group row is selected structurally, without vendor-specific SKU hardcoding; - imported configuration order follows workspace order. Imported configuration fields: - `name` from primary row `ProductName` - `server_count` from primary row `Quantity` - `server_model` from primary row `ProductDescription` - `article` or `support_code` from `ProprietaryProductIdentifier` Imported BOM rows become `vendor_spec` rows and are resolved through the active local partnumber book when possible. ## Inspur BOM import The same endpoint `POST /api/projects/:uuid/vendor-import` also accepts Inspur text BOM exports. Format: one component per line, `*`. A leading `|` character is optional and stripped. Trailing whitespace around `*` is normalised. Example: ``` |CPU_AMD_9535-EPYC2.4_64C_256M_300W*1 |PowerSupply_1300W_Titanium_220VACor240VDC_GaN*2 ``` Rules: - the entire file becomes a single configuration (`server_count = 1`); - configuration `name` is derived from the uploaded filename (without extension); - lines that do not contain `*` are skipped; - no price data is present in the format; `unit_price` and `total_price` are left nil. ## Text BOM import The same endpoint `POST /api/projects/:uuid/vendor-import` also accepts a human-readable Russian text BOM. Format: an optional header line ending with `, в составе:` followed by one component per line as ` - шт.`. The separator may be a hyphen, en-dash, or em-dash; the space before `шт` is optional; a trailing `.` after `шт` is optional. Quantities are anchored to the end of the line, so hyphens, commas, and digits inside the description (e.g. `8-GPU-2304GB`, `RAID0,1,10`) are preserved. Example: ``` Вычислительный GPU сервер G5500V7, в составе: GPU-NVIDIA HGX B300 8-GPU-2304GB HBM3E - 1 шт. CPU Intel 6760P Xeon 2.2GHz 64C 320M 330W - 2 шт. Mem 128G DDR5-6400MHz ECC-RDIMM - 16 шт. NVIDIA twin port transceiver, 800Gbps, OSFP - 8шт. ``` Rules: - the entire file becomes a single configuration (`server_count = 1`); - the header (any line ending with `, в составе:`) supplies `server_model` and `name`; the model is the last whitespace-separated token before the comma (so both `Сервер X3` and `Вычислительный GPU сервер X3` resolve to `X3`); - without a header, `name` falls back to the uploaded filename (without extension) and `server_model` is empty; - each line is trimmed, so leading/trailing whitespace never enters `vendor_partnumber`; - the format carries no partnumbers — each line's description is stored as both `vendor_partnumber` and `description`, so rows resolve through the active partnumber book when matched and otherwise stay unresolved and editable in the UI; - lines that do not match ` - шт.` are skipped; - no price data is present in the format; `unit_price` and `total_price` are left nil. ## Pasted BOM text parsing `POST /api/vendor-spec/parse-text` is a stateless endpoint that parses pasted single-column text BOM (Inspur and Russian text BOM) into rows. Request body: `{"text": "..."}`. Response: `{"rows": [{vendor_partnumber, quantity, description}], "format": "Inspur"|"Text"|""}`. This shares the exact detectors and parsers used by the file-import path (`ParsePastedBOMText` → `IsInspurBOM`/`parseInspurBOM`, `IsTextBOM`/`parseTextBOM`), so paste and upload behave identically — there is no second parser in the frontend. The configurator's BOM paste box calls this endpoint; an empty `rows` result (or any payload containing tabs, i.e. a real spreadsheet table) falls back to the manual column-mapping grid.