bugs修复
This commit is contained in:
parent
d62d6777d1
commit
7f06b2ee58
|
|
@ -42,7 +42,7 @@ const SelectStub = {
|
|||
template: '<div class="select-stub"></div>'
|
||||
}
|
||||
|
||||
const OptionStub = { props: ['label', 'value'], template: '<span />' }
|
||||
const OptionStub = { props: ['label', 'value', 'disabled'], template: '<span />' }
|
||||
|
||||
const categories = [
|
||||
{ id: 1, code: 'A', name: '机械结构类', parent_id: null, status: 'active' },
|
||||
|
|
@ -380,3 +380,38 @@ describe('物料列表的作废入口文案', () => {
|
|||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('停用分类下的物料编辑', () => {
|
||||
test('保留原分类并显示停用状态,修改名称时不改变分类归属', async () => {
|
||||
materialCategoriesApi.list.mockResolvedValue({ data: [
|
||||
{ id: 1, code: 'A', name: '旧大类', parent_id: null, status: 'inactive' },
|
||||
{ id: 12, code: 'A01', name: '旧小类', parent_id: 1, status: 'inactive' },
|
||||
{ id: 2, code: 'B', name: '新大类', parent_id: null, status: 'active' },
|
||||
{ id: 21, code: 'B01', name: '新小类', parent_id: 2, status: 'active' }
|
||||
] })
|
||||
unitsApi.list.mockResolvedValue({ data: [] })
|
||||
materialsStore.updateMaterial.mockReset().mockResolvedValue({ data: {} })
|
||||
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
|
||||
|
||||
const wrapper = mountMaterials()
|
||||
await flushPromises()
|
||||
const state = setupState(wrapper)
|
||||
const row = { id: 7, category_id: 12, category: 'A01', material_code: 'MAT-7', name: '原名称' }
|
||||
|
||||
expect(state.getMajorCategoryDisplay(row)).toBe('A 旧大类')
|
||||
expect(state.getMinorCategoryDisplay(row)).toBe('旧小类')
|
||||
|
||||
state.showEditModal(row)
|
||||
await flushPromises()
|
||||
expect(state.form.major_category_id).toBe(1)
|
||||
expect(state.form.category_id).toBe(12)
|
||||
expect(state.majorCategories.map(category => category.id)).toContain(1)
|
||||
expect(state.minorCategories.map(category => category.id)).toContain(12)
|
||||
expect(state.categoryLabel(state.minorCategories.find(category => category.id === 12))).toBe('旧小类(已停用)')
|
||||
|
||||
state.form.name = '新名称'
|
||||
await state.handleSave()
|
||||
expect(materialsStore.updateMaterial).toHaveBeenCalledWith(7, expect.objectContaining({ category_id: 12, name: '新名称' }))
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -88,12 +88,12 @@
|
|||
</el-form-item>
|
||||
<el-form-item label="大类类型" required>
|
||||
<el-select v-model="form.major_category_id" style="width: 100%" @change="syncMajorCategory">
|
||||
<el-option v-for="category in majorCategories" :key="category.id" :label="`${category.code} ${category.name}`" :value="category.id" />
|
||||
<el-option v-for="category in majorCategories" :key="category.id" :label="categoryLabel(category, true)" :value="category.id" :disabled="category.status !== 'active'" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="小类类型" required>
|
||||
<el-select v-model="form.category_id" :disabled="!form.major_category_id" style="width: 100%" @change="syncCategory">
|
||||
<el-option v-for="category in minorCategories" :key="category.id" :label="category.name" :value="category.id" />
|
||||
<el-option v-for="category in minorCategories" :key="category.id" :label="categoryLabel(category)" :value="category.id" :disabled="category.status !== 'active'" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单位">
|
||||
|
|
@ -203,8 +203,21 @@ const codePlaceholder = computed(() => {
|
|||
if (!isCreating.value || form.category_id) return ''
|
||||
return '请先选择大类类型与小类类型'
|
||||
})
|
||||
const majorCategories = computed(() => getMajorCategories(categories.value))
|
||||
const minorCategories = computed(() => getMinorCategories(categories.value, form.major_category_id))
|
||||
const majorCategories = computed(() => {
|
||||
const active = getMajorCategories(categories.value)
|
||||
const selected = categories.value.find(item => item.id === form.major_category_id)
|
||||
return selected && !active.some(item => item.id === selected.id) ? [...active, selected] : active
|
||||
})
|
||||
const minorCategories = computed(() => {
|
||||
const active = getMinorCategories(categories.value, form.major_category_id)
|
||||
const selected = categories.value.find(item => item.id === form.category_id && item.parent_id === form.major_category_id)
|
||||
return selected && !active.some(item => item.id === selected.id) ? [...active, selected] : active
|
||||
})
|
||||
|
||||
function categoryLabel(category, withCode = false) {
|
||||
const name = withCode ? `${category.code} ${category.name}` : category.name
|
||||
return category.status === 'active' ? name : `${name}(已停用)`
|
||||
}
|
||||
|
||||
function handleImageUpload(e) {
|
||||
const file = e.target.files[0]
|
||||
|
|
@ -222,7 +235,7 @@ onMounted(async () => {
|
|||
// 业务单据页(入库/出库/申请)仍用只有有效物料的 materialOptions。
|
||||
materialsStore.fetchHistoricalMaterialOptions()
|
||||
const [categoryResponse, unitResponse] = await Promise.all([materialCategoriesApi.list(), unitsApi.list()])
|
||||
categories.value = (categoryResponse.data || []).filter(item => item.status === 'active')
|
||||
categories.value = categoryResponse.data || []
|
||||
units.value = (unitResponse.data || []).filter(item => item.status === 'active')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -137,9 +137,14 @@ func (s *MaterialService) Update(id int64, req *model.UpdateMaterialRequest, ope
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.CategoryID == nil || *req.CategoryID <= 0 {
|
||||
return nil, ErrInvalidMaterialCategory
|
||||
}
|
||||
if oldMaterial.CategoryID == nil || *req.CategoryID != *oldMaterial.CategoryID {
|
||||
if err := s.validateCategory(req.CategoryID, oldMaterial.OrganizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.repo.Update(id, req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,35 @@ func TestMaterialServiceTreatsObsoleteMaterialAsInactive(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMaterialServiceUpdatesWithoutChangingDisabledCategory(t *testing.T) {
|
||||
mock, cleanup := newMaterialServiceMock(t)
|
||||
t.Cleanup(cleanup)
|
||||
now := time.Date(2026, 9, 20, 9, 0, 0, 0, time.UTC)
|
||||
materialRows := func(name string) *sqlmock.Rows {
|
||||
return sqlmock.NewRows([]string{"id", "organization_id", "material_code", "name", "material_image", "spec", "manufacturer_model", "category_id", "base_unit_id", "unit", "status", "version", "created_at", "updated_at"}).
|
||||
AddRow(7, 1, "MAT-007", name, nil, "", nil, 12, nil, "pcs", "active", 1, now, now)
|
||||
}
|
||||
mock.ExpectQuery(regexp.QuoteMeta(getMaterialQuery)).WithArgs(int64(7)).WillReturnRows(materialRows("原名称"))
|
||||
mock.ExpectExec(regexp.QuoteMeta("UPDATE materials SET name = ?, category_id = ?, version = version + 1 WHERE id = ?")).
|
||||
WithArgs("新名称", int64(12), int64(7)).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(getMaterialQuery)).WithArgs(int64(7)).WillReturnRows(materialRows("新名称"))
|
||||
mock.ExpectExec(regexp.QuoteMeta(materialLogStmt)).
|
||||
WithArgs("UPDATE", int64(7), sqlmock.AnyArg(), sqlmock.AnyArg(), "tester", sqlmock.AnyArg()).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
categoryID := int64(12)
|
||||
material, err := NewMaterialService().Update(7, &model.UpdateMaterialRequest{Name: "新名称", CategoryID: &categoryID}, "tester")
|
||||
if err != nil {
|
||||
t.Fatalf("update material with unchanged disabled category: %v", err)
|
||||
}
|
||||
if material.CategoryID == nil || *material.CategoryID != 12 || material.Name != "新名称" {
|
||||
t.Fatalf("updated material = %+v, want category 12 and new name", material)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 以下常量必须与 repository 里的 SQL 字面量逐字一致,sqlmock 才匹配得上。
|
||||
const (
|
||||
activeSubcategoryQuery = `SELECT EXISTS(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user