diff --git a/backend/app/api/qc.py b/backend/app/api/qc.py index dc81458a..d01d5170 100644 --- a/backend/app/api/qc.py +++ b/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 ) diff --git a/backend/app/crud/qc_material_month_template.py b/backend/app/crud/qc_material_month_template.py index 636c7800..fe0ff8dc 100644 --- a/backend/app/crud/qc_material_month_template.py +++ b/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") diff --git a/backend/app/models/qc_material.py b/backend/app/models/qc_material.py index 8ef4852b..ab92f967 100644 --- a/backend/app/models/qc_material.py +++ b/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="备注") diff --git a/backend/tests/test_qc_material_month_template.py b/backend/tests/test_qc_material_month_template.py index 0d013447..5d53b8f6 100644 --- a/backend/tests/test_qc_material_month_template.py +++ b/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"], "") diff --git a/docs/sql/2026-08-19-qc-material-month-template-optional-fields.sql b/docs/sql/2026-08-19-qc-material-month-template-optional-fields.sql new file mode 100644 index 00000000..ce12e161 --- /dev/null +++ b/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 '默认过期日期'; diff --git a/docs/sql/2026-08-19-qc-material-month-template.sql b/docs/sql/2026-08-19-qc-material-month-template.sql index 19f1f6e2..1b372a90 100644 --- a/docs/sql/2026-08-19-qc-material-month-template.sql +++ b/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 '备注', diff --git a/web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue b/web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue index c6140192..4e941dfc 100644 --- a/web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue +++ b/web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue @@ -5,7 +5,7 @@ -
+
模板明细 共 {{ rows.length }} 项,保存后供「月初导入」使用
- + 新增一行 @@ -62,7 +62,7 @@ -
+
- - + + - - + + @@ -109,25 +109,25 @@ - - + + - - + + - - + + - + - + - + - + 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') } + +