feat: allow empty fields on qc material month template rows

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-19 13:06:35 +08:00
1 parent f7ee984d04
commit 0a5bf28e58
7 files changed
+158 -120

No files matched your search

+13 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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="备注")
@@ -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"], "")
@@ -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 '默认过期日期';
@@ -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 '备注',
@@ -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>