168 lines
5.3 KiB
Go
168 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
func TestParseMaterialImportFileReadsRowsWithExcelRowNumbers(t *testing.T) {
|
|
data := buildTestWorkbook(t, [][]string{
|
|
{"内六角螺栓", "M8x30", "个", "机械结构类", "标准件"},
|
|
{"", "", "", "", ""}, // 整行空,应被跳过
|
|
{"急停按钮", "XB2", "个", "电气元器件类", "成品件"},
|
|
})
|
|
|
|
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("rows = %d, want 2", len(rows))
|
|
}
|
|
if rows[0].Row != 2 || rows[0].Name != "内六角螺栓" || rows[0].MinorName != "标准件" {
|
|
t.Fatalf("first row = %+v", rows[0])
|
|
}
|
|
// 第 3 行是空行被跳过,所以第二条数据是 Excel 第 4 行
|
|
if rows[1].Row != 4 || rows[1].Name != "急停按钮" {
|
|
t.Fatalf("second row = %+v", rows[1])
|
|
}
|
|
}
|
|
|
|
func TestParseMaterialImportFileKeepsRowsWithAnyContent(t *testing.T) {
|
|
// 名称填了、其余为空:不是空行,必须保留交给校验去报错
|
|
data := buildTestWorkbook(t, [][]string{
|
|
{"只有名称", "", "", "", ""},
|
|
})
|
|
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Name != "只有名称" {
|
|
t.Fatalf("rows = %+v", rows)
|
|
}
|
|
}
|
|
|
|
func TestParseMaterialImportFileRejectsMissingRequiredColumn(t *testing.T) {
|
|
// 只有「物料名称」和「规格」,缺必填的大类、小类
|
|
data := buildTestWorkbookWithHeaders(t, []string{"物料名称", "规格"}, [][]string{
|
|
{"内六角螺栓", "M8x30"},
|
|
})
|
|
|
|
_, err := ParseMaterialImportFile(bytes.NewReader(data))
|
|
if !errors.Is(err, ErrImportTemplateInvalid) {
|
|
t.Fatalf("err = %v, want ErrImportTemplateInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestParseMaterialImportFileAcceptsMissingOptionalColumnsAndReorderedColumns(t *testing.T) {
|
|
// 删掉「规格」,并把「单位」挪到「物料名称」前面
|
|
headers := []string{"单位", "物料名称", "大类", "小类"}
|
|
data := buildTestWorkbookWithHeaders(t, headers, [][]string{
|
|
{"个", "内六角螺栓", "机械结构类", "标准件"},
|
|
})
|
|
|
|
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("rows = %d, want 1", len(rows))
|
|
}
|
|
if rows[0].Name != "内六角螺栓" || rows[0].Unit != "个" || rows[0].MinorName != "标准件" {
|
|
t.Fatalf("row = %+v", rows[0])
|
|
}
|
|
if rows[0].Spec != "" {
|
|
t.Fatalf("缺失的规格列应解析为空,得到 %q", rows[0].Spec)
|
|
}
|
|
}
|
|
|
|
func TestParseMaterialImportFileIgnoresUnknownColumns(t *testing.T) {
|
|
headers := []string{"物料名称", "备注", "大类", "小类"}
|
|
data := buildTestWorkbookWithHeaders(t, headers, [][]string{
|
|
{"内六角螺栓", "与导入无关", "机械结构类", "标准件"},
|
|
})
|
|
|
|
rows, err := ParseMaterialImportFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Name != "内六角螺栓" {
|
|
t.Fatalf("rows = %+v", rows)
|
|
}
|
|
}
|
|
|
|
func TestParseMaterialImportFileRejectsTooManyRows(t *testing.T) {
|
|
rows := make([][]string, 0, MaterialImportMaxRows+1)
|
|
for i := 0; i < MaterialImportMaxRows+1; i++ {
|
|
rows = append(rows, []string{"物料", "", "", "机械结构类", "标准件"})
|
|
}
|
|
data := buildTestWorkbook(t, rows)
|
|
|
|
_, err := ParseMaterialImportFile(bytes.NewReader(data))
|
|
if !errors.Is(err, ErrImportTooManyRows) {
|
|
t.Fatalf("err = %v, want ErrImportTooManyRows", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildMaterialImportTemplateRoundTrips(t *testing.T) {
|
|
data, err := BuildMaterialImportTemplate()
|
|
if err != nil {
|
|
t.Fatalf("build template: %v", err)
|
|
}
|
|
f, err := excelize.OpenReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("open template: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if idx, _ := f.GetSheetIndex(MaterialImportSheetData); idx < 0 {
|
|
t.Fatal("模板缺少「物料数据」工作表")
|
|
}
|
|
if idx, _ := f.GetSheetIndex(MaterialImportSheetGuide); idx < 0 {
|
|
t.Fatal("模板缺少「填写说明」工作表")
|
|
}
|
|
headers, err := f.GetRows(MaterialImportSheetData)
|
|
if err != nil {
|
|
t.Fatalf("read rows: %v", err)
|
|
}
|
|
if len(headers) == 0 || headers[0][0] != "物料名称" || headers[0][4] != "小类" {
|
|
t.Fatalf("表头 = %v", headers)
|
|
}
|
|
// 模板必须能被自己的解析器接受(表头一致)
|
|
if _, err := ParseMaterialImportFile(bytes.NewReader(data)); err != nil {
|
|
t.Fatalf("模板无法被解析器接受: %v", err)
|
|
}
|
|
}
|
|
|
|
// buildTestWorkbook 用标准表头 + 给定数据行构造一个 xlsx 字节流。
|
|
func buildTestWorkbook(t *testing.T, dataRows [][]string) []byte {
|
|
t.Helper()
|
|
return buildTestWorkbookWithHeaders(t, materialImportHeaders, dataRows)
|
|
}
|
|
|
|
// buildTestWorkbookWithHeaders 允许指定表头,用来覆盖缺列、乱序、多余列的解析分支。
|
|
func buildTestWorkbookWithHeaders(t *testing.T, headers []string, dataRows [][]string) []byte {
|
|
t.Helper()
|
|
f := excelize.NewFile()
|
|
defer f.Close()
|
|
f.SetSheetName("Sheet1", MaterialImportSheetData)
|
|
for i, header := range headers {
|
|
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
|
f.SetCellValue(MaterialImportSheetData, cell, header)
|
|
}
|
|
for r, row := range dataRows {
|
|
for c, value := range row {
|
|
cell, _ := excelize.CoordinatesToCellName(c+1, r+2)
|
|
f.SetCellValue(MaterialImportSheetData, cell, value)
|
|
}
|
|
}
|
|
buf, err := f.WriteToBuffer()
|
|
if err != nil {
|
|
t.Fatalf("write workbook: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|