Колонка «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>
188 lines
8.0 KiB
Markdown
188 lines
8.0 KiB
Markdown
# 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[]`.
|
|
|
|
## 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 code` column type in addition to `P/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 separate
|
|
`pn_code` field**: 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 the `PN code` column 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_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, `<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 `name` is derived from the uploaded filename (without extension);
|
|
- lines that do not contain `*<digits>` 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
|
|
`<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 `, в составе:`) 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 `<description> - <quantity> шт.` are skipped;
|
|
- no price data is present in the format; `unit_price` and `total_price` are 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 `, в составе:`) supplies `server_model` and `name`; the model is the
|
|
last whitespace-separated token before the comma;
|
|
- without a header, `name` falls back to the uploaded filename (without extension) and `server_model` is empty;
|
|
- 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 `<qty>x <description>` are skipped;
|
|
- no price data is present in the format; `unit_price` and `total_price` are 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.
|