feat: поддержка импорта BOM Inspur в формате PN*qty
Добавлен парсер для текстового формата Inspur (опциональный '|' в начале строки, разделитель '*' перед количеством). На BOM-вкладке вставка такого текста автоматически определяется и разбивается на колонки P/N + Qty без ручного выбора типов. На бэкенде тот же формат поддерживается через POST /api/projects/:uuid/vendor-import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -124,7 +124,15 @@ func (s *LocalConfigurationService) ImportVendorWorkspaceToProject(projectUUID s
|
||||
return nil, ErrProjectNotFound
|
||||
}
|
||||
|
||||
workspace, err := parseCFXMLWorkspace(data, filepath.Base(sourceFileName))
|
||||
var workspace *importedWorkspace
|
||||
switch {
|
||||
case IsCFXMLWorkspace(data):
|
||||
workspace, err = parseCFXMLWorkspace(data, filepath.Base(sourceFileName))
|
||||
case IsInspurBOM(data):
|
||||
workspace, err = parseInspurBOM(data, filepath.Base(sourceFileName))
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported vendor export format")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -558,3 +566,90 @@ func normalizeTopLevelQuantity(raw string, serverCount int) int {
|
||||
func IsCFXMLWorkspace(data []byte) bool {
|
||||
return bytes.Contains(data, []byte("<CFXML>")) || bytes.Contains(data, []byte("<CFXML "))
|
||||
}
|
||||
|
||||
func IsInspurBOM(data []byte) bool {
|
||||
for _, line := range bytes.Split(data, []byte("\n")) {
|
||||
trimmed := bytes.TrimSpace(line)
|
||||
if len(trimmed) == 0 {
|
||||
continue
|
||||
}
|
||||
idx := bytes.LastIndexByte(trimmed, '*')
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
suffix := bytes.TrimSpace(trimmed[idx+1:])
|
||||
if len(suffix) > 0 && allDigits(suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func allDigits(b []byte) bool {
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, c := range b {
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseInspurBOM(data []byte, sourceFileName string) (*importedWorkspace, error) {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
rows := make([]localdb.VendorSpecItem, 0, len(lines))
|
||||
sortOrder := 10
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimPrefix(line, "|")
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
pn := line
|
||||
qty := 1
|
||||
if idx := strings.LastIndex(line, "*"); idx > 0 {
|
||||
suffix := strings.TrimSpace(line[idx+1:])
|
||||
if n, err := strconv.Atoi(suffix); err == nil && n > 0 {
|
||||
pn = strings.TrimSpace(line[:idx])
|
||||
qty = n
|
||||
}
|
||||
}
|
||||
if pn == "" {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, localdb.VendorSpecItem{
|
||||
SortOrder: sortOrder,
|
||||
VendorPartnumber: pn,
|
||||
Quantity: qty,
|
||||
})
|
||||
sortOrder += 10
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil, fmt.Errorf("Inspur BOM has no importable rows")
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(filepath.Base(sourceFileName), filepath.Ext(sourceFileName))
|
||||
if name == "" {
|
||||
name = "Inspur Import"
|
||||
}
|
||||
|
||||
return &importedWorkspace{
|
||||
SourceFormat: "Inspur",
|
||||
SourceFileName: sourceFileName,
|
||||
Configurations: []importedConfiguration{
|
||||
{
|
||||
GroupID: "inspur-0",
|
||||
Name: name,
|
||||
Line: 10,
|
||||
ServerCount: 1,
|
||||
Rows: rows,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user