Просмотр исходного кода

feat: allow empty fields on qc material month template rows

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
0a5bf28e58

+ 13 - 13
backend/app/api/qc.py

@@ -1568,17 +1568,17 @@ class BatchStockInBody(BaseModel):
 class QcMonthTemplateBody(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
     material_id: Optional[str] = Field(None, alias="materialId")
-    material_code: str = Field(..., alias="materialCode")
-    material_name: str = Field(..., alias="materialName")
+    material_code: Optional[str] = Field(None, alias="materialCode")
+    material_name: Optional[str] = Field(None, alias="materialName")
     spec_model: Optional[str] = Field(None, alias="specModel")
     unit: Optional[str] = None
-    catalog_no: str = Field(..., alias="catalogNo")
-    batch_no: str = Field(..., alias="batchNo")
-    manufacturer: str
-    unit_price: float = Field(..., alias="unitPrice")
-    amount: float
-    in_qty: float = Field(..., alias="inQty", gt=0)
-    expire_date: str = Field(..., alias="expireDate")
+    catalog_no: Optional[str] = Field(None, alias="catalogNo")
+    batch_no: Optional[str] = Field(None, alias="batchNo")
+    manufacturer: Optional[str] = None
+    unit_price: Optional[float] = Field(None, alias="unitPrice")
+    amount: Optional[float] = None
+    in_qty: Optional[float] = Field(None, alias="inQty")
+    expire_date: Optional[str] = Field(None, alias="expireDate")
     order_num: Optional[int] = Field(None, alias="orderNum")
     remark: Optional[str] = None
 
@@ -2140,8 +2140,8 @@ async def create_qc_month_template(
             db, user, body.unit_price, body.amount
         )
         data = body.model_dump(by_alias=False)
-        data["unit_price"] = unit_price
-        data["amount"] = amount
+        data["unit_price"] = unit_price if unit_price is not None else 0
+        data["amount"] = amount if amount is not None else 0
         row = month_tpl_svc.create_template(
             db, data=data, tenant_no=tenant_no, user_id=user_id
         )
@@ -2166,8 +2166,8 @@ async def update_qc_month_template(
             db, user, body.unit_price, body.amount
         )
         data = body.model_dump(by_alias=False)
-        data["unit_price"] = unit_price
-        data["amount"] = amount
+        data["unit_price"] = unit_price if unit_price is not None else 0
+        data["amount"] = amount if amount is not None else 0
         row = month_tpl_svc.update_template(
             db, row_id, data=data, tenant_no=tenant_no, user_id=user_id
         )

+ 37 - 22
backend/app/crud/qc_material_month_template.py

@@ -11,39 +11,54 @@ from app.core.utils import generate_uuid
 from app.crud import qc_material as mat_svc
 from app.crud import qc_material_stock_batch as batch_svc
 from app.crud.qc_material import _is_enabled_status
-from app.crud.qc_material_stock_batch import _fmt_date, _parse_date, _require_decimal, _require_str
+from app.crud.qc_material_stock_batch import _fmt_date, _parse_date, _require_str
 from app.crud.qc_material_stock_in_no import allocate_stock_in_nos
 from app.models.qc_material import BizQcMaterialMonthTemplate
 
 
+def _optional_str(v: Any) -> str:
+    return str(v or "").strip()
+
+
+def _optional_decimal(v: Any, *, default: Decimal = Decimal("0")) -> Decimal:
+    if v is None or str(v).strip() == "":
+        return default
+    try:
+        d = Decimal(str(v))
+    except Exception as e:
+        raise ValueError("数值格式无效") from e
+    if d < 0:
+        raise ValueError("数值不能为负数")
+    return d
+
+
+def _optional_expire_date(v: Any) -> Optional[date]:
+    s = str(v or "").strip()
+    if not s:
+        return None
+    return _parse_date(v, label="过期日期")
+
+
 def _require_template_fields(payload: dict) -> dict:
-    """校验并规范化模板写入字段;缺必填抛 ValueError。"""
+    """规范化模板写入字段;模板行字段均可空,确认入库时再校验必填。"""
     if not isinstance(payload, dict):
         raise ValueError("模板数据无效")
 
     material_id = (payload.get("material_id") or payload.get("materialId") or "").strip() or None
-    material_code = _require_str(
-        payload.get("material_code") or payload.get("materialCode"), "物料编码"
-    )
-    material_name = _require_str(
-        payload.get("material_name") or payload.get("materialName"), "物料名称"
-    )
-    catalog_no = _require_str(payload.get("catalog_no") or payload.get("catalogNo"), "货号")
-    batch_no = _require_str(payload.get("batch_no") or payload.get("batchNo"), "批号")
-    manufacturer = _require_str(payload.get("manufacturer"), "厂家")
-    unit_price = _require_decimal(payload.get("unit_price") or payload.get("unitPrice"), "单价")
-    amount = _require_decimal(payload.get("amount"), "金额")
+    material_code = _optional_str(payload.get("material_code") or payload.get("materialCode"))
+    material_name = _optional_str(payload.get("material_name") or payload.get("materialName"))
+    catalog_no = _optional_str(payload.get("catalog_no") or payload.get("catalogNo"))
+    batch_no = _optional_str(payload.get("batch_no") or payload.get("batchNo"))
+    manufacturer = _optional_str(payload.get("manufacturer"))
+    unit_price = _optional_decimal(payload.get("unit_price") or payload.get("unitPrice"))
+    amount = _optional_decimal(payload.get("amount"))
     in_qty_raw = payload.get("in_qty") if payload.get("in_qty") is not None else payload.get("inQty")
-    in_qty = _require_decimal(in_qty_raw, "入库数量")
-    if in_qty <= 0:
-        raise ValueError("入库数量必须大于0")
-    expire_date = _parse_date(
-        payload.get("expire_date") or payload.get("expireDate"), label="过期日期"
-    )
+    in_qty = _optional_decimal(in_qty_raw)
+    expire_date = _optional_expire_date(payload.get("expire_date") or payload.get("expireDate"))
 
-    spec_model = (payload.get("spec_model") or payload.get("specModel") or "").strip() or None
-    unit = (payload.get("unit") or "").strip() or None
-    remark = (payload.get("remark") or "").strip() or None
+    spec_model = _optional_str(payload.get("spec_model") or payload.get("specModel")) or None
+    unit = _optional_str(payload.get("unit")) or None
+    remark = _optional_str(payload.get("remark")) or None
     order_num = payload.get("order_num")
     if order_num is None:
         order_num = payload.get("orderNum")

+ 7 - 7
backend/app/models/qc_material.py

@@ -36,13 +36,13 @@ class BizQcMaterialMonthTemplate(Base):
     material_name = Column(String(200), nullable=False, comment="物料名称")
     spec_model = Column(String(200), nullable=True, comment="规格型号")
     unit = Column(String(32), nullable=True, comment="单位")
-    catalog_no = Column(String(128), nullable=False, comment="货号")
-    batch_no = Column(String(128), nullable=False, comment="批号")
-    manufacturer = Column(String(200), nullable=False, comment="厂家")
-    unit_price = Column(Numeric(18, 2), nullable=False, comment="单价(元)")
-    amount = Column(Numeric(18, 2), nullable=False, comment="金额(元)")
-    in_qty = Column(Numeric(18, 4), nullable=False, comment="默认入库数量")
-    expire_date = Column(Date, nullable=False, comment="默认过期日期")
+    catalog_no = Column(String(128), nullable=True, comment="货号")
+    batch_no = Column(String(128), nullable=True, comment="批号")
+    manufacturer = Column(String(200), nullable=True, comment="厂家")
+    unit_price = Column(Numeric(18, 2), nullable=False, default=0, comment="单价(元)")
+    amount = Column(Numeric(18, 2), nullable=False, default=0, comment="金额(元)")
+    in_qty = Column(Numeric(18, 4), nullable=False, default=0, comment="默认入库数量")
+    expire_date = Column(Date, nullable=True, comment="默认过期日期")
     tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
     order_num = Column(Integer, nullable=True, comment="排序号")
     remark = Column(String(512), nullable=True, comment="备注")

+ 18 - 15
backend/tests/test_qc_material_month_template.py

@@ -30,20 +30,23 @@ class TestRequireTemplateFields(unittest.TestCase):
         self.assertEqual(got["in_qty"], Decimal("10"))
         self.assertEqual(got["expire_date"], date(2027, 1, 1))
 
-    def test_missing_catalog(self):
-        with self.assertRaises(ValueError) as ctx:
-            _require_template_fields(self._base(catalogNo=""))
-        self.assertIn("货号", str(ctx.exception))
+    def test_all_optional_empty(self):
+        got = _require_template_fields({})
+        self.assertEqual(got["material_code"], "")
+        self.assertEqual(got["material_name"], "")
+        self.assertEqual(got["catalog_no"], "")
+        self.assertEqual(got["batch_no"], "")
+        self.assertEqual(got["manufacturer"], "")
+        self.assertEqual(got["unit_price"], Decimal("0"))
+        self.assertEqual(got["amount"], Decimal("0"))
+        self.assertEqual(got["in_qty"], Decimal("0"))
+        self.assertIsNone(got["expire_date"])
 
-    def test_qty_not_positive(self):
-        with self.assertRaises(ValueError) as ctx:
-            _require_template_fields(self._base(inQty=0))
-        self.assertIn("入库数量", str(ctx.exception))
+    def test_qty_zero_allowed(self):
+        got = _require_template_fields(self._base(inQty=0))
+        self.assertEqual(got["in_qty"], Decimal("0"))
 
-    def test_missing_code_name(self):
-        with self.assertRaises(ValueError) as ctx:
-            _require_template_fields(self._base(materialCode=""))
-        self.assertIn("物料编码", str(ctx.exception))
-        with self.assertRaises(ValueError) as ctx2:
-            _require_template_fields(self._base(materialName=""))
-        self.assertIn("物料名称", str(ctx2.exception))
+    def test_missing_code_name_allowed(self):
+        got = _require_template_fields(self._base(materialCode="", materialName=""))
+        self.assertEqual(got["material_code"], "")
+        self.assertEqual(got["material_name"], "")

+ 13 - 0
docs/sql/2026-08-19-qc-material-month-template-optional-fields.sql

@@ -0,0 +1,13 @@
+-- 质检物料月初模板:模板行字段改为可空(维护时可不全填,确认入库时再校验)
+-- MySQL 5.7;已建表环境执行本脚本
+
+ALTER TABLE `biz_qc_material_month_template`
+  MODIFY COLUMN `material_code` varchar(64) NOT NULL DEFAULT '' COMMENT '物料编码',
+  MODIFY COLUMN `material_name` varchar(200) NOT NULL DEFAULT '' COMMENT '物料名称',
+  MODIFY COLUMN `catalog_no` varchar(128) DEFAULT NULL COMMENT '货号',
+  MODIFY COLUMN `batch_no` varchar(128) DEFAULT NULL COMMENT '批号',
+  MODIFY COLUMN `manufacturer` varchar(200) DEFAULT NULL COMMENT '厂家',
+  MODIFY COLUMN `unit_price` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '单价(元)',
+  MODIFY COLUMN `amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '金额(元)',
+  MODIFY COLUMN `in_qty` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '默认入库数量',
+  MODIFY COLUMN `expire_date` date DEFAULT NULL COMMENT '默认过期日期';

+ 9 - 9
docs/sql/2026-08-19-qc-material-month-template.sql

@@ -4,17 +4,17 @@
 CREATE TABLE IF NOT EXISTS `biz_qc_material_month_template` (
   `id` varchar(64) NOT NULL COMMENT '主键ID',
   `material_id` varchar(64) DEFAULT NULL COMMENT '关联 biz_qc_material.id,可空',
-  `material_code` varchar(64) NOT NULL COMMENT '物料编码',
-  `material_name` varchar(200) NOT NULL COMMENT '物料名称',
+  `material_code` varchar(64) NOT NULL DEFAULT '' COMMENT '物料编码',
+  `material_name` varchar(200) NOT NULL DEFAULT '' COMMENT '物料名称',
   `spec_model` varchar(200) DEFAULT NULL COMMENT '规格型号',
   `unit` varchar(32) DEFAULT NULL COMMENT '单位',
-  `catalog_no` varchar(128) NOT NULL COMMENT '货号',
-  `batch_no` varchar(128) NOT NULL COMMENT '批号',
-  `manufacturer` varchar(200) NOT NULL COMMENT '厂家',
-  `unit_price` decimal(18,2) NOT NULL COMMENT '单价(元)',
-  `amount` decimal(18,2) NOT NULL COMMENT '金额(元)',
-  `in_qty` decimal(18,4) NOT NULL COMMENT '默认入库数量',
-  `expire_date` date NOT NULL COMMENT '默认过期日期',
+  `catalog_no` varchar(128) DEFAULT NULL COMMENT '货号',
+  `batch_no` varchar(128) DEFAULT NULL COMMENT '批号',
+  `manufacturer` varchar(200) DEFAULT NULL COMMENT '厂家',
+  `unit_price` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '单价(元)',
+  `amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '金额(元)',
+  `in_qty` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '默认入库数量',
+  `expire_date` date DEFAULT NULL COMMENT '默认过期日期',
   `tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
   `order_num` int(11) DEFAULT 0 COMMENT '排序号',
   `remark` varchar(512) DEFAULT NULL COMMENT '备注',

+ 61 - 54
web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue

@@ -5,7 +5,7 @@
   <el-dialog
     :model-value="modelValue"
     title="维护月初模板"
-    width="1080px"
+    width="1280px"
     class="dialog-form-layout material-batch-stock-dialog"
     align-center
     append-to-body
@@ -15,12 +15,12 @@
     @update:model-value="emit('update:modelValue', $event)"
     @open="handleOpen"
   >
-    <div class="batch-stock-move-lines-head" style="margin-bottom: 12px">
+    <div class="month-template-toolbar">
       <div class="batch-stock-move-lines-head-left">
         <span class="batch-stock-move-lines-title">模板明细</span>
         <span class="batch-stock-move-lines-hint">共 {{ rows.length }} 项,保存后供「月初导入」使用</span>
       </div>
-      <el-button class="btn-ghost" @click="openEdit()">
+      <el-button class="btn-main" type="primary" @click="openEdit()">
         <el-icon><Plus /></el-icon>
         新增一行
       </el-button>
@@ -62,7 +62,7 @@
   <el-dialog
     v-model="editVisible"
     :title="editForm.id ? '编辑模板行' : '新增模板行'"
-    width="720px"
+    width="960px"
     class="dialog-form-layout"
     align-center
     append-to-body
@@ -78,7 +78,7 @@
       scroll-to-error
       :scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
     >
-      <div class="form-grid-2">
+      <div class="form-grid-3">
         <el-form-item label="关联物资" class="form-col-span-2">
           <el-select
             v-model="editForm.materialId"
@@ -97,11 +97,11 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="物料编码" prop="materialCode">
-          <el-input v-model="editForm.materialCode" placeholder="必填" />
+        <el-form-item label="物料编码">
+          <el-input v-model="editForm.materialCode" placeholder="可选" />
         </el-form-item>
-        <el-form-item label="物料名称" prop="materialName">
-          <el-input v-model="editForm.materialName" placeholder="必填" />
+        <el-form-item label="物料名称">
+          <el-input v-model="editForm.materialName" placeholder="可选" />
         </el-form-item>
         <el-form-item label="规格型号">
           <el-input v-model="editForm.specModel" placeholder="可选" />
@@ -109,25 +109,25 @@
         <el-form-item label="单位">
           <el-input v-model="editForm.unit" placeholder="可选" />
         </el-form-item>
-        <el-form-item label="货号" prop="catalogNo">
-          <el-input v-model="editForm.catalogNo" placeholder="必填" />
+        <el-form-item label="货号">
+          <el-input v-model="editForm.catalogNo" placeholder="可选" />
         </el-form-item>
-        <el-form-item label="批号" prop="batchNo">
-          <el-input v-model="editForm.batchNo" placeholder="必填" />
+        <el-form-item label="批号">
+          <el-input v-model="editForm.batchNo" placeholder="可选" />
         </el-form-item>
-        <el-form-item label="厂家" prop="manufacturer">
-          <el-input v-model="editForm.manufacturer" placeholder="必填" />
+        <el-form-item label="厂家">
+          <el-input v-model="editForm.manufacturer" placeholder="可选" />
         </el-form-item>
-        <el-form-item label="过期日期" prop="expireDate">
+        <el-form-item label="过期日期">
           <el-date-picker
             v-model="editForm.expireDate"
             type="date"
             value-format="YYYY-MM-DD"
-            placeholder="选择过期日期"
+            placeholder="可选"
             style="width: 100%"
           />
         </el-form-item>
-        <el-form-item label="单价(元)" prop="unitPrice">
+        <el-form-item label="单价(元)">
           <el-input-number
             v-model="editForm.unitPrice"
             :min="0"
@@ -137,17 +137,17 @@
             @change="onPriceOrQtyChange"
           />
         </el-form-item>
-        <el-form-item label="数量" prop="inQty">
+        <el-form-item label="数量">
           <el-input-number
             v-model="editForm.inQty"
-            :min="0.0001"
+            :min="0"
             :precision="4"
             controls-position="right"
             style="width: 100%"
             @change="onPriceOrQtyChange"
           />
         </el-form-item>
-        <el-form-item label="金额(元)" prop="amount">
+        <el-form-item label="金额(元)">
           <el-input-number
             v-model="editForm.amount"
             :min="0"
@@ -217,22 +217,12 @@
     manufacturer: '',
     unitPrice: 0,
     amount: 0,
-    inQty: 1,
+    inQty: 0,
     expireDate: '',
     remark: ''
   })
 
-  const editRules: FormRules = {
-    materialCode: [{ required: true, message: '请输入物料编码', trigger: 'blur' }],
-    materialName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }],
-    catalogNo: [{ required: true, message: '请输入货号', trigger: 'blur' }],
-    batchNo: [{ required: true, message: '请输入批号', trigger: 'blur' }],
-    manufacturer: [{ required: true, message: '请输入厂家', trigger: 'blur' }],
-    expireDate: [{ required: true, message: '请选择过期日期', trigger: 'change' }],
-    unitPrice: [{ required: true, message: '请输入单价', trigger: 'blur' }],
-    amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
-    inQty: [{ required: true, message: '请输入数量', trigger: 'blur' }]
-  }
+  const editRules: FormRules = {}
 
   async function loadRows() {
     loading.value = true
@@ -272,7 +262,7 @@
     editForm.manufacturer = ''
     editForm.unitPrice = 0
     editForm.amount = 0
-    editForm.inQty = 1
+    editForm.inQty = 0
     editForm.expireDate = ''
     editForm.remark = ''
     amountTouched.value = false
@@ -318,32 +308,21 @@
   }
 
   async function handleSave() {
-    const form = editFormRef.value
-    if (!form) return
-    try {
-      await form.validate()
-    } catch {
-      return
-    }
-    if (!(Number(editForm.inQty) > 0)) {
-      ElMessage.warning('数量必须大于0')
-      return
-    }
     saving.value = true
     try {
       const payload = {
         materialId: editForm.materialId || undefined,
-        materialCode: editForm.materialCode.trim(),
-        materialName: editForm.materialName.trim(),
+        materialCode: (editForm.materialCode || '').trim(),
+        materialName: (editForm.materialName || '').trim(),
         specModel: editForm.specModel?.trim() || undefined,
         unit: editForm.unit?.trim() || undefined,
-        catalogNo: editForm.catalogNo.trim(),
-        batchNo: editForm.batchNo.trim(),
-        manufacturer: editForm.manufacturer.trim(),
-        unitPrice: Number(editForm.unitPrice),
-        amount: Number(editForm.amount),
-        inQty: Number(editForm.inQty),
-        expireDate: editForm.expireDate,
+        catalogNo: (editForm.catalogNo || '').trim(),
+        batchNo: (editForm.batchNo || '').trim(),
+        manufacturer: (editForm.manufacturer || '').trim(),
+        unitPrice: Number(editForm.unitPrice) || 0,
+        amount: Number(editForm.amount) || 0,
+        inQty: Number(editForm.inQty) || 0,
+        expireDate: editForm.expireDate || undefined,
         remark: editForm.remark?.trim() || undefined
       }
       if (editForm.id) {
@@ -377,3 +356,31 @@
     emit('saved')
   }
 </script>
+
+<style scoped lang="scss">
+  .month-template-toolbar {
+    display: flex;
+    align-items: flex-end;
+    justify-content: space-between;
+    gap: 16px;
+    margin: 16px 0;
+  }
+
+  .batch-stock-move-lines-head-left {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    min-width: 0;
+  }
+
+  .batch-stock-move-lines-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: var(--mint-text, #0f172a);
+  }
+
+  .batch-stock-move-lines-hint {
+    font-size: 12px;
+    color: var(--mint-text-muted, #64748b);
+  }
+</style>