|
|
|
|
@@ -15,7 +15,8 @@ The vendor spec feature allows importing a vendor BOM (Bill of Materials) into a
|
|
|
|
|
| `vendor_spec` JSON | `local_configurations.vendor_spec` (TEXT, JSON-encoded) | 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` 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
|
|
|
|
|
|
|
|
|
|
@@ -160,6 +161,201 @@ Persistence note: the application stores the final user-visible mappings in `lot
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## CFXML Workspace Import Contract
|
|
|
|
|
|
|
|
|
|
QuoteForge may import a vendor configurator workspace in `CFXML` format as an existing project update path.
|
|
|
|
|
This import path must convert one external workspace into one QuoteForge project containing multiple configurations.
|
|
|
|
|
|
|
|
|
|
### Import Unit Boundaries
|
|
|
|
|
|
|
|
|
|
- One `CFXML` workspace file = one QuoteForge project import session.
|
|
|
|
|
- One top-level configuration group inside the workspace = one QuoteForge configuration.
|
|
|
|
|
- Software rows are **not** imported as standalone configurations.
|
|
|
|
|
- All software rows must be attached to the configuration group they belong to.
|
|
|
|
|
|
|
|
|
|
### Configuration Grouping
|
|
|
|
|
|
|
|
|
|
Top-level `ProductLineItem` rows are grouped by:
|
|
|
|
|
|
|
|
|
|
- `ProprietaryGroupIdentifier`
|
|
|
|
|
|
|
|
|
|
This field is the canonical boundary of one imported configuration.
|
|
|
|
|
|
|
|
|
|
Rules:
|
|
|
|
|
|
|
|
|
|
1. Read all top-level `ProductLineItem` rows in document order.
|
|
|
|
|
2. Group them by `ProprietaryGroupIdentifier`.
|
|
|
|
|
3. Preserve document order of groups by the first encountered `ProductLineNumber`.
|
|
|
|
|
4. Import each group as exactly one QuoteForge configuration.
|
|
|
|
|
|
|
|
|
|
`ConfigurationGroupLineNumberReference` is not sufficient for grouping imported configurations because
|
|
|
|
|
multiple independent configuration groups may share the same value in one workspace.
|
|
|
|
|
|
|
|
|
|
### Primary Row Selection (no SKU hardcode)
|
|
|
|
|
|
|
|
|
|
The importer must not hardcode vendor, model, or server SKU values.
|
|
|
|
|
|
|
|
|
|
Within each `ProprietaryGroupIdentifier` group, the importer selects one primary top-level row using
|
|
|
|
|
structural rules only:
|
|
|
|
|
|
|
|
|
|
1. Prefer rows with `ProductTypeCode = Hardware`.
|
|
|
|
|
2. If multiple rows match, prefer the row with the largest number of `ProductSubLineItem` children.
|
|
|
|
|
3. If there is still a tie, prefer the first row by `ProductLineNumber`.
|
|
|
|
|
|
|
|
|
|
The primary row provides configuration-level metadata such as:
|
|
|
|
|
|
|
|
|
|
- configuration name
|
|
|
|
|
- server count
|
|
|
|
|
- server model / description
|
|
|
|
|
- article / support code candidate
|
|
|
|
|
|
|
|
|
|
### Software Inclusion Rule
|
|
|
|
|
|
|
|
|
|
All top-level rows belonging to the same `ProprietaryGroupIdentifier` must be imported into the same
|
|
|
|
|
QuoteForge configuration, including:
|
|
|
|
|
|
|
|
|
|
- `Hardware`
|
|
|
|
|
- `Software`
|
|
|
|
|
- instruction / service rows represented as software-like items
|
|
|
|
|
|
|
|
|
|
Effects:
|
|
|
|
|
|
|
|
|
|
- a workspace never creates a separate configuration made only of software;
|
|
|
|
|
- `software1`, `software2`, license rows, and instruction rows stay inside the related configuration;
|
|
|
|
|
- the user sees one complete configuration instead of fragmented partial imports.
|
|
|
|
|
|
|
|
|
|
### Mapping to QuoteForge Project / Configuration
|
|
|
|
|
|
|
|
|
|
For one imported configuration group:
|
|
|
|
|
|
|
|
|
|
- QuoteForge configuration `name` <- primary row `ProductName`
|
|
|
|
|
- QuoteForge configuration `server_count` <- primary row `Quantity`
|
|
|
|
|
- QuoteForge configuration `server_model` <- primary row `ProductDescription`
|
|
|
|
|
- QuoteForge configuration `article` or `support_code` <- primary row `ProprietaryProductIdentifier`
|
|
|
|
|
- QuoteForge configuration `line` <- stable order by group appearance in the workspace
|
|
|
|
|
|
|
|
|
|
Project-level fields such as QuoteForge `code`, `name`, and `variant` are not reliably defined by `CFXML`
|
|
|
|
|
itself and should come from the existing target project context or explicit user input.
|
|
|
|
|
|
|
|
|
|
### Mapping to `vendor_spec`
|
|
|
|
|
|
|
|
|
|
The importer must build one combined `vendor_spec` array per configuration group.
|
|
|
|
|
|
|
|
|
|
Source rows:
|
|
|
|
|
|
|
|
|
|
- all `ProductSubLineItem` rows from the primary top-level row;
|
|
|
|
|
- all `ProductSubLineItem` rows from every non-primary top-level row in the same group;
|
|
|
|
|
- if a top-level row has no `ProductSubLineItem`, the top-level row itself may be converted into one
|
|
|
|
|
`vendor_spec` row so that software-only content is not lost.
|
|
|
|
|
|
|
|
|
|
Each imported row maps into one `VendorSpecItem`:
|
|
|
|
|
|
|
|
|
|
- `sort_order` <- stable sequence within the group
|
|
|
|
|
- `vendor_partnumber` <- `ProprietaryProductIdentifier`
|
|
|
|
|
- `quantity` <- `Quantity`
|
|
|
|
|
- `description` <- `ProductDescription`
|
|
|
|
|
- `unit_price` <- `UnitListPrice.FinancialAmount.MonetaryAmount` when present
|
|
|
|
|
- `total_price` <- `quantity * unit_price` when unit price is present
|
|
|
|
|
- `lot_mappings` <- resolved immediately from the active partnumber book when a unique match exists
|
|
|
|
|
|
|
|
|
|
The importer stores vendor-native rows in `vendor_spec`, then immediately runs the same logical flow as BOM
|
|
|
|
|
Resolve + Apply:
|
|
|
|
|
|
|
|
|
|
- resolve vendor PN rows through the active partnumber book
|
|
|
|
|
- persist canonical `lot_mappings[]`
|
|
|
|
|
- build normalized configuration `items` from `row.quantity * quantity_per_pn`
|
|
|
|
|
- fill `items.unit_price` from the latest local `estimate` pricelist
|
|
|
|
|
- recalculate configuration `total_price`
|
|
|
|
|
|
|
|
|
|
### Import Pipeline
|
|
|
|
|
|
|
|
|
|
Recommended parser pipeline:
|
|
|
|
|
|
|
|
|
|
1. Parse XML into top-level `ProductLineItem` rows.
|
|
|
|
|
2. Group rows by `ProprietaryGroupIdentifier`.
|
|
|
|
|
3. Select one primary row per group using structural rules.
|
|
|
|
|
4. Build one QuoteForge configuration DTO per group.
|
|
|
|
|
5. Merge all hardware/software rows of the group into one `vendor_spec`.
|
|
|
|
|
6. Resolve imported PN rows into canonical `lot_mappings[]` using the active partnumber book.
|
|
|
|
|
7. Build configuration `items` from resolved `lot_mappings[]`.
|
|
|
|
|
8. Price those `items` from the latest local `estimate` pricelist.
|
|
|
|
|
9. Save or update the QuoteForge configuration inside the existing project.
|
|
|
|
|
|
|
|
|
|
### Recommended Internal DTO
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type ImportedProject struct {
|
|
|
|
|
SourceFormat string
|
|
|
|
|
SourceFilePath string
|
|
|
|
|
SourceDocID string
|
|
|
|
|
|
|
|
|
|
Code string
|
|
|
|
|
Name string
|
|
|
|
|
Variant string
|
|
|
|
|
|
|
|
|
|
Configurations []ImportedConfiguration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ImportedConfiguration struct {
|
|
|
|
|
GroupID string
|
|
|
|
|
|
|
|
|
|
Name string
|
|
|
|
|
Line int
|
|
|
|
|
ServerCount int
|
|
|
|
|
|
|
|
|
|
ServerModel string
|
|
|
|
|
Article string
|
|
|
|
|
SupportCode string
|
|
|
|
|
CurrencyCode string
|
|
|
|
|
|
|
|
|
|
TopLevelRows []ImportedTopLevelRow
|
|
|
|
|
VendorSpec []ImportedVendorRow
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ImportedTopLevelRow struct {
|
|
|
|
|
ProductLineNumber string
|
|
|
|
|
ItemNo string
|
|
|
|
|
GroupID string
|
|
|
|
|
|
|
|
|
|
ProductType string
|
|
|
|
|
ProductCode string
|
|
|
|
|
ProductName string
|
|
|
|
|
Description string
|
|
|
|
|
Quantity int
|
|
|
|
|
UnitPrice *float64
|
|
|
|
|
IsPrimary bool
|
|
|
|
|
|
|
|
|
|
SubRows []ImportedVendorRow
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ImportedVendorRow struct {
|
|
|
|
|
SortOrder int
|
|
|
|
|
|
|
|
|
|
SourceLineNumber string
|
|
|
|
|
SourceParentLine string
|
|
|
|
|
SourceProductType string
|
|
|
|
|
|
|
|
|
|
VendorPartnumber string
|
|
|
|
|
Description string
|
|
|
|
|
Quantity int
|
|
|
|
|
UnitPrice *float64
|
|
|
|
|
TotalPrice *float64
|
|
|
|
|
|
|
|
|
|
ProductCharacter string
|
|
|
|
|
ProductCharPath string
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Current Product Assumption
|
|
|
|
|
|
|
|
|
|
For QuoteForge product behavior, the correct user-facing interpretation is:
|
|
|
|
|
|
|
|
|
|
- one external project/workspace contains several configurations;
|
|
|
|
|
- each configuration contains both hardware and software rows that belong to it;
|
|
|
|
|
- the importer must preserve that grouping exactly.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Qty Aggregation Logic
|
|
|
|
|
|
|
|
|
|
After resolution, qty per LOT is computed as:
|
|
|
|
|
@@ -294,6 +490,7 @@ Estimate-only rows are shown as separate rows with:
|
|
|
|
|
| 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 |
|
|
|
|
|
| POST | `/api/projects/:uuid/vendor-import` | Import `CFXML` workspace into an existing project and create grouped configurations |
|
|
|
|
|
| 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 MariaDB |
|
|
|
|
|
@@ -306,15 +503,20 @@ After each `resolveBOM()` call, QuoteForge pushes PN rows to `POST /api/sync/par
|
|
|
|
|
- unresolved BOM rows (`ignored = false`)
|
|
|
|
|
- raw BOM rows explicitly marked as ignored in UI (`ignored = true`) — these rows are **not** saved to `vendor_spec`, but are reported for server-side tracking
|
|
|
|
|
|
|
|
|
|
The handler calls `sync.PushPartnumberSeen()` which upserts into `qt_vendor_partnumber_seen`:
|
|
|
|
|
The handler calls `sync.PushPartnumberSeen()` which inserts into `qt_vendor_partnumber_seen`.
|
|
|
|
|
If a row with the same `partnumber` already exists, QuoteForge must leave it untouched:
|
|
|
|
|
|
|
|
|
|
- do not update `last_seen_at`
|
|
|
|
|
- do not update `is_ignored`
|
|
|
|
|
- do not update `description`
|
|
|
|
|
|
|
|
|
|
Canonical insert behavior:
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
INSERT INTO qt_vendor_partnumber_seen (source_type, vendor, partnumber, description, is_ignored, last_seen_at)
|
|
|
|
|
VALUES ('manual', '', ?, ?, ?, NOW())
|
|
|
|
|
ON DUPLICATE KEY UPDATE
|
|
|
|
|
last_seen_at = VALUES(last_seen_at),
|
|
|
|
|
is_ignored = VALUES(is_ignored),
|
|
|
|
|
description = COALESCE(NULLIF(VALUES(description), ''), description)
|
|
|
|
|
partnumber = partnumber
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Uniqueness key: `partnumber` only (after PriceForge migration 025). PriceForge uses this table for populating the partnumber book.
|
|
|
|
|
|