feat: Nx BOM import — формат <qty>x <description>
Добавлен новый вариант импорта спеки: quantity-first формат, где каждая строка начинается с `<qty>x <description>` (например, «2x Intel Xeon 8570»). Порядок детекции: Inspur → Nx → Text BOM. Заголовок «, в составе:» работает так же, как в Text BOM — последний токен перед запятой становится server_model. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,8 @@ func (s *LocalConfigurationService) ImportVendorWorkspaceToProject(projectUUID s
|
||||
workspace, err = parseQuoteForgeCSV(data, filepath.Base(sourceFileName))
|
||||
case IsInspurBOM(data):
|
||||
workspace, err = parseInspurBOM(data, filepath.Base(sourceFileName))
|
||||
case IsNxBOM(data):
|
||||
workspace, err = parseNxBOM(data, filepath.Base(sourceFileName))
|
||||
case IsTextBOM(data):
|
||||
workspace, err = parseTextBOM(data, filepath.Base(sourceFileName))
|
||||
default:
|
||||
@@ -683,6 +685,93 @@ func parseInspurBOM(data []byte, sourceFileName string) (*importedWorkspace, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
// nxBOMItemLine matches a quantity-first BOM line: "<qty>x <description>"
|
||||
// where the quantity prefix is digits followed immediately by "x" (case-insensitive).
|
||||
// Parentheses, commas, and hyphens inside the description are preserved.
|
||||
var nxBOMItemLine = regexp.MustCompile(`(?i)^(\d+)[xX]\s+(.+\S)\s*$`)
|
||||
|
||||
// IsNxBOM reports whether data looks like a quantity-first "Nx" BOM where each
|
||||
// item line begins with "<qty>x <description>" (e.g. "2x Intel Xeon 8570 ...").
|
||||
func IsNxBOM(data []byte) bool {
|
||||
for _, raw := range strings.Split(string(data), "\n") {
|
||||
if nxBOMItemLine.MatchString(strings.TrimSpace(raw)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseNxBOM parses a quantity-first "Nx" BOM into a single configuration.
|
||||
// An optional header line ending with ", в составе:" supplies server_model and name.
|
||||
// Each "<qty>x <description>" line becomes one vendor spec row; 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.
|
||||
func parseNxBOM(data []byte, sourceFileName string) (*importedWorkspace, error) {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
rows := make([]localdb.VendorSpecItem, 0, len(lines))
|
||||
sortOrder := 10
|
||||
serverModel := ""
|
||||
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if m := textBOMHeaderLine.FindStringSubmatch(line); m != nil {
|
||||
if fields := strings.Fields(m[1]); len(fields) > 0 {
|
||||
serverModel = fields[len(fields)-1]
|
||||
}
|
||||
continue
|
||||
}
|
||||
m := nxBOMItemLine.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
qty, err := strconv.Atoi(m[1])
|
||||
if err != nil || qty <= 0 {
|
||||
continue
|
||||
}
|
||||
description := strings.TrimSpace(m[2])
|
||||
if description == "" {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, localdb.VendorSpecItem{
|
||||
SortOrder: sortOrder,
|
||||
VendorPartnumber: description,
|
||||
Quantity: qty,
|
||||
Description: description,
|
||||
})
|
||||
sortOrder += 10
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return nil, fmt.Errorf("Nx BOM has no importable rows")
|
||||
}
|
||||
|
||||
name := serverModel
|
||||
if name == "" {
|
||||
name = strings.TrimSuffix(filepath.Base(sourceFileName), filepath.Ext(sourceFileName))
|
||||
}
|
||||
if name == "" {
|
||||
name = "Nx BOM Import"
|
||||
}
|
||||
|
||||
return &importedWorkspace{
|
||||
SourceFormat: "Nx",
|
||||
SourceFileName: sourceFileName,
|
||||
Configurations: []importedConfiguration{
|
||||
{
|
||||
GroupID: "nx-0",
|
||||
Name: name,
|
||||
Line: 10,
|
||||
ServerCount: 1,
|
||||
ServerModel: serverModel,
|
||||
Rows: rows,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// textBOMItemLine matches a human-readable BOM line of the form
|
||||
// "<description> - <quantity> шт." where the separator may be a hyphen,
|
||||
// en-dash or em-dash and the quantity may have an optional space before "шт".
|
||||
@@ -709,6 +798,8 @@ func ParsePastedBOMText(text string) ([]localdb.VendorSpecItem, string) {
|
||||
switch {
|
||||
case IsInspurBOM(data):
|
||||
ws, err = parseInspurBOM(data, "")
|
||||
case IsNxBOM(data):
|
||||
ws, err = parseNxBOM(data, "")
|
||||
case IsTextBOM(data):
|
||||
ws, err = parseTextBOM(data, "")
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user