material_manage_system/internal/repository/organization_defaults_test.go
文睿 张 3cc783b5ff 物料数据迁移,并让每个公司自带默认单位与仓库
数据迁移(北京 / 武汉两个 SQLite 物料库 → wmms)
- 北京进默认组织、武汉单独建组织:两套分类编码完全相同但含义不同(北京 A01=标准件,
  武汉 A01=机加工件),共存会撞 material_categories 的组织内编码唯一约束
- 199 条物料、37 个企业、45 条变更日志,以及各分类的 code_rules.next_sequence
  (对齐到已导入的最大序号 +1,否则用户新增物料会撞上已有编码)
- 迁移段并入 scripts/init.sql 末尾,空库初始化即得完整数据

公司默认数据
- 新建公司时种入 32 个物料单位 + 默认仓库 + 主库位,与建公司同事务,
  种一半失败会整体回滚(否则留下一个没单位、又占着公司编码的组织)
- 单位在后端按公司隔离(MaterialRepo.IsActiveUnit 要求物料引用的单位属于本公司),
  此前新建的公司连单位下拉都是空的,物料的 base_unit_id 也无处可指
- init.sql 的单位段补齐「块」「Pcs」,与代码里的清单保持逐条一致

默认组织更名:默认公司组织 → 北京
2026-10-14 16:05:36 +08:00

138 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"errors"
"os"
"path/filepath"
"regexp"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
// 与 organization.go / organization_defaults.go 里的语句逐字一致。
const (
createOrganizationSQL = `INSERT INTO organizations (code, name, status, default_timezone) VALUES (?, ?, ?, ?)`
seedUnitSQL = `INSERT INTO units (organization_id, code, name, precision_scale, status) VALUES (?, ?, ?, ?, 'active')`
seedWarehouseSQL = `INSERT INTO warehouses (organization_id, code, name, status) VALUES (?, 'DEFAULT', '默认仓库', 'active')`
seedLocationSQL = `INSERT INTO warehouse_locations (warehouse_id, code, name, status) VALUES (?, 'MAIN', '主库位', 'active')`
)
func newOrganizationSeedMock(t *testing.T) sqlmock.Sqlmock {
t.Helper()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("open sql mock: %v", err)
}
t.Cleanup(func() { database.DB = nil; db.Close() })
database.DB = db
return mock
}
// 单位是按公司隔离的,新公司没种单位的话,物料单位下拉是空的,base_unit_id 也无处可指。
func TestCreateOrganizationSeedsUnitsWarehouseAndLocation(t *testing.T) {
mock := newOrganizationSeedMock(t)
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(createOrganizationSQL)).
WithArgs("WUHAN", "武汉", "active", "Asia/Shanghai").
WillReturnResult(sqlmock.NewResult(7, 1))
for _, unit := range defaultMaterialUnits {
mock.ExpectExec(regexp.QuoteMeta(seedUnitSQL)).
WithArgs(int64(7), unit.Code, unit.Name, unit.PrecisionScale).
WillReturnResult(sqlmock.NewResult(1, 1))
}
mock.ExpectExec(regexp.QuoteMeta(seedWarehouseSQL)).
WithArgs(int64(7)).
WillReturnResult(sqlmock.NewResult(3, 1))
mock.ExpectExec(regexp.QuoteMeta(seedLocationSQL)).
WithArgs(int64(3)).
WillReturnResult(sqlmock.NewResult(5, 1))
mock.ExpectCommit()
item := &model.Organization{Code: "WUHAN", Name: "武汉", Status: "active", DefaultTimezone: "Asia/Shanghai"}
if err := NewOrganizationRepository().Create(item); err != nil {
t.Fatalf("Create: %v", err)
}
if item.ID != 7 {
t.Fatalf("回填的组织 ID = %d,期望 7", item.ID)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("有期望未被满足(漏种或多给了语句): %v", err)
}
}
// 种子失败必须整体回滚:留下一个没有单位的公司,用户重试还会撞公司编码的唯一键。
func TestCreateOrganizationRollsBackWhenSeedingFails(t *testing.T) {
mock := newOrganizationSeedMock(t)
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(createOrganizationSQL)).
WithArgs("WUHAN", "武汉", "active", "Asia/Shanghai").
WillReturnResult(sqlmock.NewResult(7, 1))
// 第一个单位就失败
mock.ExpectExec(regexp.QuoteMeta(seedUnitSQL)).
WithArgs(int64(7), defaultMaterialUnits[0].Code, defaultMaterialUnits[0].Name, defaultMaterialUnits[0].PrecisionScale).
WillReturnError(errors.New("boom"))
mock.ExpectRollback()
err := NewOrganizationRepository().Create(&model.Organization{
Code: "WUHAN", Name: "武汉", Status: "active", DefaultTimezone: "Asia/Shanghai",
})
if err == nil {
t.Fatal("种子失败时 Create 应当返回错误")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("有期望未被满足: %v", err)
}
}
// 单位表有 uk_units_org_code 唯一键,清单里出现重复编码会让建公司直接失败。
func TestDefaultMaterialUnitsHaveNoDuplicateCodes(t *testing.T) {
seen := make(map[string]bool, len(defaultMaterialUnits))
for _, unit := range defaultMaterialUnits {
if seen[unit.Code] {
t.Fatalf("默认单位清单里编码重复:%q", unit.Code)
}
seen[unit.Code] = true
}
}
// 新建公司种下的单位,必须和 scripts/init.sql 给默认组织种的那套一致;
// 两份清单各改各的,空库初始化与界面上新建的公司就会拿到不同的单位表。
func TestDefaultMaterialUnitsMatchInitSQL(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "scripts", "init.sql"))
if err != nil {
t.Fatalf("读取 init.sql: %v", err)
}
// units 段的行形如 (1,'个','个',0,'active');这个五字段模式在 init.sql 里
// 只匹配单位表(分类是四字段、仓库是五字段但首列是数字)。
row := regexp.MustCompile(`\(1,'([^']+)','[^']*',(\d+),'active'\)`)
fromSQL := make(map[string]bool)
for _, m := range row.FindAllStringSubmatch(string(raw), -1) {
fromSQL[m[1]] = true
}
if len(fromSQL) == 0 {
t.Fatalf("没从 init.sql 里解析出默认组织的单位,正则可能已失效")
}
fromGo := make(map[string]model.Unit, len(defaultMaterialUnits))
for _, unit := range defaultMaterialUnits {
fromGo[unit.Code] = unit
}
for code := range fromGo {
if !fromSQL[code] {
t.Errorf("Go 默认单位清单里有 %q,init.sql 里没有", code)
}
}
for code := range fromSQL {
if _, ok := fromGo[code]; !ok {
t.Errorf("init.sql 里有单位 %q,Go 默认清单里没有", code)
}
}
if len(fromGo) != len(fromSQL) {
t.Errorf("单位数量不一致:Go %d 个,init.sql %d 个", len(fromGo), len(fromSQL))
}
}