Колонка «PN code» в таблице импорта BOM: HPE-спецификации хранят партномер в двух столбцах, канонический вид — P52534-B21#B19. Склейка попадает в vendor_partnumber и сравнивается с книгой партномеров как есть; при загрузке разбирается обратно на две колонки. Схему БД менять не пришлось. Список допустимых LOT собирается из world ∪ estimate (componentUniverse), а не только из estimate. Прежняя область видимости молча теряла lot_mappings: резолвер сопоставлял PN по книге, которая про прайслисты не знает, а фронт отбрасывал LOT, которого нет в estimate — при этом в корзину он всё равно попадал. World-only позиции отдают price_quality 0. Кнопка «Пересопоставить» — перерешать BOM по актуальной книге, так как при открытии конфигурации сопоставления остаются замороженными. Итоги на вкладке «Ценообразование»: звёздочки убраны (переносились на новую строку), все три суммы красные, при наведении — попап с долей цен из прайслиста WORLD. Колонка «PN вендора» больше не переносится. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.0 KiB
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:
{
"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[].
Split partnumbers (HPE option codes)
HPE specs carry the partnumber in two columns: base PN plus an option code
(P52534-B21 + B19). The canonical partnumber is the concatenation
P52534-B21#B19, and it is what gets compared against the partnumber book.
Rules:
- the import grid has a
PN codecolumn type in addition toP/N; it is optional and at most one column may carry it; - an empty code cell keeps the bare PN — no trailing
#; - the composed value is stored in
vendor_partnumber. There is no separatepn_codefield: no DDL and no new JSON key, and the stored string is exactly the string the resolver looks up; - the column layout of the import grid is not persisted. On load the grid is
re-derived from
vendor_spec, and thePN codecolumn reappears only when at least one stored partnumber contains#, split on the first#.
Re-resolving an existing BOM
Opening a configuration does not re-resolve its BOM: loadVendorSpec renders the
lot_mappings[] frozen at save time, so a configuration keeps the mapping it was
saved with even after the partnumber book changes.
Book entries added later are picked up only through the Пересопоставить button,
which calls the resolve endpoint for every row. Book matches win over the stored
mapping (resolver step 1 beats step 2); rows the book does not know keep theirs.
Partnumber books
Partnumber books are pull-only snapshots from PriceForge.
Local tables:
local_partnumber_bookslocal_partnumber_book_items
Server tables:
qt_partnumber_booksqt_partnumber_book_items
Resolution flow:
- load the active local book;
- find
vendor_partnumber; - copy
lots_jsonintolot_mappings[]; - 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
ProprietaryGroupIdentifierbecomes 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:
namefrom primary rowProductNameserver_countfrom primary rowQuantityserver_modelfrom primary rowProductDescriptionarticleorsupport_codefromProprietaryProductIdentifier
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, <partnumber>*<quantity>. 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
nameis derived from the uploaded filename (without extension); - lines that do not contain
*<digits>are skipped; - no price data is present in the format;
unit_priceandtotal_priceare 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
<description> - <quantity> шт.. 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
, в составе:) suppliesserver_modelandname; the model is the last whitespace-separated token before the comma (so bothСервер X3andВычислительный GPU сервер X3resolve toX3); - without a header,
namefalls back to the uploaded filename (without extension) andserver_modelis 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_partnumberanddescription, so rows resolve through the active partnumber book when matched and otherwise stay unresolved and editable in the UI; - lines that do not match
<description> - <quantity> шт.are skipped; - no price data is present in the format;
unit_priceandtotal_priceare left nil.
Nx BOM import (quantity-first)
The same endpoint POST /api/projects/:uuid/vendor-import also accepts a quantity-first BOM
where each item line begins with <qty>x <description>.
Format: an optional header line ending with , в составе: followed by one component per line as
<qty>x <description>. The x separator is case-insensitive; parentheses, commas, and hyphens
inside the description are preserved as-is.
Example:
Сервер G893-SD1-AAX3, в составе:
1x 8U 2CPU 8GPU Server System (32x DDR5 DIMM Slots,8x 2.5" Hot-Swap Drive Bays, 4+4 3000W, 2x 10Gb/s RJ45, 2x IPMI RJ45)
2x Intel Xeon 8570 (56 cores, 2.1GHz, 300MB, 350W)
32x 64GB DDR5 ECC RDIMM
1x GPU Nvidia HGX H200 141GB 8GPU
3x 1.92TB NVMe PCIe SFF RI
5x 7.68TB NVMe PCIe SFF RI
8x 1-port 400G NDR OSFP CX7
2x 2-port 100GbE QSFP56 CX6
1x 2-port 10GbE RJ45
Rules:
- the entire file becomes a single configuration (
server_count = 1); - the header (any line ending with
, в составе:) suppliesserver_modelandname; the model is the last whitespace-separated token before the comma; - without a header,
namefalls back to the uploaded filename (without extension) andserver_modelis empty; - the format carries no partnumbers — each line's description is stored as both
vendor_partnumberanddescription, so rows resolve through the active partnumber book when matched and otherwise stay unresolved and editable in the UI; - lines that do not match
<qty>x <description>are skipped; - no price data is present in the format;
unit_priceandtotal_priceare left nil; - detection runs before Text BOM in the format switch (Inspur → Nx → Text).
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, IsNxBOM/parseNxBOM, 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.