From 594e4ce38e54a4dbbe2dfca552519b73d2b325d9 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 3 Aug 2026 11:46:11 +0800 Subject: [PATCH] feat(crm): sync contract number date segment with sign date Align standard contract nos on create/update (keep seq, bump on collision), preview the change in contract forms, and sync name parentheses. Co-authored-by: Cursor --- backend/app/crud/crm/contract.py | 90 +++++++ backend/tests/test_crm_contract_no_align.py | 63 +++++ ...crm-contract-no-follow-sign-date-design.md | 2 +- .../components/ContractFormDialog.vue | 66 +++-- web-admin/src/views/crm/contract/form.vue | 241 +++++------------- 5 files changed, 260 insertions(+), 202 deletions(-) create mode 100644 backend/tests/test_crm_contract_no_align.py diff --git a/backend/app/crud/crm/contract.py b/backend/app/crud/crm/contract.py index c25340ba0..06160f92d 100644 --- a/backend/app/crud/crm/contract.py +++ b/backend/app/crud/crm/contract.py @@ -625,6 +625,71 @@ def generate_contract_no( raise ValueError("生成合同编号失败,请稍后重试") +def contract_no_exists( + db: Session, + tenant_no: str, + contract_no: str, + *, + exclude_id: str = None, +) -> bool: + """租户内合同编号是否已占用(含软删,保证全局唯一)。""" + q = db.query(CrmContract.id).filter( + CrmContract.tenant_no == tenant_no, + CrmContract.contract_no == contract_no, + ) + if exclude_id: + q = q.filter(CrmContract.id != exclude_id) + return q.first() is not None + + +_CONTRACT_NO_STD_RE = re.compile(r"^([A-Z]+)/X-JT(\d{8})(\d{3})$") + + +def replace_contract_no_date(contract_no: str, sign_date: date) -> Optional[str]: + """标准编号替换日期段,保留序号;非标准返回 None。""" + if not contract_no or not sign_date: + return None + m = _CONTRACT_NO_STD_RE.match(contract_no.strip()) + if not m: + return None + supplier_code, _old_date, seq = m.group(1), m.group(2), m.group(3) + date_str = sign_date.strftime("%Y%m%d") + return f"{supplier_code}/X-JT{date_str}{seq}" + + +def align_contract_no_to_sign_date( + db: Session, + tenant_no: str, + contract_no: str, + sign_date: date, + *, + exclude_id: str = None, +) -> str: + """将标准合同编号日期段对齐签订日;撞号则序号顺延(含软删查重)。 + + 非标准编号原样返回。无签订日则原样返回。 + """ + if not (contract_no or "").strip(): + return contract_no or "" + if not sign_date: + return contract_no.strip() + + replaced = replace_contract_no_date(contract_no, sign_date) + if replaced is None: + return contract_no.strip() + + m = _CONTRACT_NO_STD_RE.match(replaced) + if not m: + return replaced + supplier_code, date_str, seq = m.group(1), m.group(2), m.group(3) + start_seq = int(seq) + for retry in range(50): + candidate = f"{supplier_code}/X-JT{date_str}{start_seq + retry:03d}" + if not contract_no_exists(db, tenant_no, candidate, exclude_id=exclude_id): + return candidate + raise ValueError("生成合同编号失败,请稍后重试") + + def generate_contract_name( contract_no: str, contract_type: str, @@ -929,6 +994,10 @@ def create_contract( contract_no = (_get_field(data, "contract_no", "contractNo") or "").strip() if not contract_no: contract_no = generate_contract_no(db, tenant_no, supplier_id=supplier_id, sign_date=sign_date) + else: + contract_no = align_contract_no_to_sign_date(db, tenant_no, contract_no, sign_date) + if contract_no_exists(db, tenant_no, contract_no): + raise ValueError(f"合同编号已存在:{contract_no}") customer = get_customer(db, customer_id) customer_short_name = (customer.short_name or "") if customer else "" @@ -1036,6 +1105,27 @@ def update_contract( if product_rows is not None: _resolve_contract_product_ids(db, tenant_no=tenant_no, products=product_rows, user_id=user_id) + sign_date = old.sign_date or date.today() + submitted_no = (_get_field(data, "contract_no", "contractNo") or "").strip() or ( + old.contract_no or "" + ).strip() + if submitted_no: + aligned_no = align_contract_no_to_sign_date( + db, + tenant_no, + submitted_no, + sign_date, + exclude_id=old.id, + ) + if aligned_no != (old.contract_no or ""): + if contract_no_exists(db, tenant_no, aligned_no, exclude_id=old.id): + raise ValueError(f"合同编号已存在:{aligned_no}") + old.contract_no = aligned_no + elif submitted_no != (old.contract_no or ""): + if contract_no_exists(db, tenant_no, submitted_no, exclude_id=old.id): + raise ValueError(f"合同编号已存在:{submitted_no}") + old.contract_no = submitted_no + supplier_short_name = _get_supplier_short_name(db, old.supplier_id or "", tenant_no) customer = get_customer(db, old.customer_id) if old.customer_id else None customer_short_name = (customer.short_name or "") if customer else (old.customer_name or "") diff --git a/backend/tests/test_crm_contract_no_align.py b/backend/tests/test_crm_contract_no_align.py new file mode 100644 index 000000000..cb0c7d8ce --- /dev/null +++ b/backend/tests/test_crm_contract_no_align.py @@ -0,0 +1,63 @@ +from datetime import date +from unittest.mock import MagicMock, patch + +from app.crud.crm.contract import align_contract_no_to_sign_date, replace_contract_no_date + + +def test_replace_keeps_seq_and_swaps_date(): + assert ( + replace_contract_no_date("SX/X-JT20250803001", date(2025, 8, 10)) + == "SX/X-JT20250810001" + ) + + +def test_replace_noop_when_same_date(): + assert replace_contract_no_date("SX/X-JT20250803001", date(2025, 8, 3)) == "SX/X-JT20250803001" + + +def test_replace_returns_none_for_nonstandard(): + assert replace_contract_no_date("CUSTOM-001", date(2025, 8, 10)) is None + assert replace_contract_no_date("", date(2025, 8, 10)) is None + assert replace_contract_no_date(None, date(2025, 8, 10)) is None + + +def test_align_returns_original_when_nonstandard(): + db = MagicMock() + assert ( + align_contract_no_to_sign_date( + db, "t1", "CUSTOM-001", date(2025, 8, 10) + ) + == "CUSTOM-001" + ) + + +def test_align_bumps_seq_when_occupied(): + db = MagicMock() + + def _exists(db_, tenant, no, exclude_id=None): + return no == "SX/X-JT20250810001" + + with patch("app.crud.crm.contract.contract_no_exists", side_effect=_exists): + out = align_contract_no_to_sign_date( + db, "t1", "SX/X-JT20250803001", date(2025, 8, 10) + ) + assert out == "SX/X-JT20250810002" + + +def test_align_exclude_id_skips_self(): + db = MagicMock() + + def _exists(db_, tenant, no, exclude_id=None): + if exclude_id == "self-id": + return False + return no == "SX/X-JT20250810001" + + with patch("app.crud.crm.contract.contract_no_exists", side_effect=_exists): + out = align_contract_no_to_sign_date( + db, + "t1", + "SX/X-JT20250803001", + date(2025, 8, 10), + exclude_id="self-id", + ) + assert out == "SX/X-JT20250810001" diff --git a/docs/superpowers/specs/2026-08-03-crm-contract-no-follow-sign-date-design.md b/docs/superpowers/specs/2026-08-03-crm-contract-no-follow-sign-date-design.md index c3d44d992..919ce9b91 100644 --- a/docs/superpowers/specs/2026-08-03-crm-contract-no-follow-sign-date-design.md +++ b/docs/superpowers/specs/2026-08-03-crm-contract-no-follow-sign-date-design.md @@ -2,7 +2,7 @@ **日期**:2026-08-03 **范围**:`web-admin` 合同管理新增/编辑;`backend` 合同 create/update(草稿) -**状态**:已评审,待实现 +**状态**:已实现 ## 背景 diff --git a/web-admin/src/views/crm/contract/components/ContractFormDialog.vue b/web-admin/src/views/crm/contract/components/ContractFormDialog.vue index 020f7baa8..0a31f697e 100644 --- a/web-admin/src/views/crm/contract/components/ContractFormDialog.vue +++ b/web-admin/src/views/crm/contract/components/ContractFormDialog.vue @@ -19,7 +19,12 @@ - + @@ -242,7 +247,7 @@
- + @@ -410,7 +415,7 @@ - +
@@ -427,7 +432,7 @@
- + - +
@@ -601,6 +606,7 @@ import { } from '@/utils/crm-product-brand' import { fetchProductDetail } from '@/api/crm/product' import ApprovalDialog from './ApprovalDialog.vue' +import { omitEmptyExceptPartyFields } from '../partyFields' import { getContactByCustomerId } from '@/api/crm/customer' import type { CustomerVO, ContactVO } from '@/api/crm/customer/types' import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo' @@ -1348,6 +1354,31 @@ const updateContractNoBySupplier = (newSupplierCode: string | undefined) => { } } +const CONTRACT_NO_STD_RE = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/ + +/** 签订日变更时:标准编号只换日期段,保留序号;并同步名称括号内编号 */ +const syncContractNoDateFromSignDate = (signDateVal: string | null) => { + if (!signDateVal || !form.value.contractNo) return + const m = String(form.value.contractNo).match(CONTRACT_NO_STD_RE) + if (!m) return + + const dateStr = signDateVal.replace(/-/g, '') + if (dateStr.length !== 8) return + if (m[2] === dateStr) return + + const nextNo = `${m[1]}/X-JT${dateStr}${m[3]}` + form.value.contractNo = nextNo + + if (form.value.contractName) { + const replaced = form.value.contractName.replace(/[((][^))]*[))]$/, `(${nextNo})`) + if (replaced !== form.value.contractName) { + form.value.contractName = replaced + } else { + rebuildContractName() + } + } +} + const COMMON_SUPPLIER_SHORT_NAMES = ['山峡', 'SX'] const extractContractNameParts = (contractName: string) => { @@ -1366,7 +1397,7 @@ const isSupplierShortNamePart = (part: string, supplierShort: string) => { return part === supplierShort || COMMON_SUPPLIER_SHORT_NAMES.some((name) => part.includes(name)) } -/** 从现有合同名称推断客户简称(切换合同类型时不能依赖新类型解析旧格式) */ +/** 从现有合同名称推断客户简称(统一格式:供方简称-需方简称) */ const inferCustomerShortNameFromContractName = (): string => { if (customerShortName.value) return customerShortName.value if (!form.value.contractName) return '' @@ -1383,14 +1414,13 @@ const inferCustomerShortNameFromContractName = (): string => { return isSupplierShortNamePart(parts[0], supplierShort) ? '' : parts[0] } + // 统一格式:供方(交易主体)-需方(客户);供方在前则取第二段为客户简称 if (isSupplierShortNamePart(parts[0], supplierShort)) return parts[1] if (isSupplierShortNamePart(parts[1], supplierShort)) return parts[0] - - if (form.value.contractType === 'purchase') return parts[0] return parts[1] } -/** 按合同类型重建合同名称:销售/框架=供方简称-客户简称,采购=客户简称-供方简称 */ +/** 重建合同名称:统一为 {供方简称}-{需方简称}({合同编号}),不因采购反序 */ const rebuildContractName = () => { if (!form.value.contractName) return @@ -1400,10 +1430,7 @@ const rebuildContractName = () => { const { contractNoInName } = extractContractNameParts(form.value.contractName) const contractNo = form.value.contractNo || contractNoInName - const isPurchase = form.value.contractType === 'purchase' - const nameBody = isPurchase - ? [customerShort, supplierShort].filter(Boolean).join('-') - : [supplierShort, customerShort].filter(Boolean).join('-') + const nameBody = [supplierShort, customerShort].filter(Boolean).join('-') if (!nameBody) return @@ -1534,6 +1561,7 @@ const handleSignDateChange = (val: string | null) => { form.value.startDate = '' form.value.endDate = '' } + syncContractNoDateFromSignDate(val) nextTick(() => { formRef.value?.validateField('startDate') }) @@ -1919,11 +1947,7 @@ const handleSaveDraft = async () => { data.attachmentFiles = attachmentJson } - Object.keys(data).forEach((key) => { - if (data[key] === undefined || data[key] === '') { - delete data[key] - } - }) + omitEmptyExceptPartyFields(data) if (data.contactsName) { data.contactName = data.contactsName @@ -2086,11 +2110,7 @@ const handleSubmit = async () => { data.attachmentFiles = attachmentJson } - Object.keys(data).forEach((key) => { - if (data[key] === undefined || data[key] === '') { - delete data[key] - } - }) + omitEmptyExceptPartyFields(data) if (data.contactsName) { data.contactName = data.contactsName diff --git a/web-admin/src/views/crm/contract/form.vue b/web-admin/src/views/crm/contract/form.vue index b200affbe..081d00b36 100644 --- a/web-admin/src/views/crm/contract/form.vue +++ b/web-admin/src/views/crm/contract/form.vue @@ -19,7 +19,12 @@ - + @@ -600,6 +605,7 @@ import ContactSelector from '@/components/ContactSelector.vue'; import QuoteSelector from '@/components/QuoteSelector.vue'; import UserSelector from '@/components/UserSelector.vue'; import ApprovalDialog from './components/ApprovalDialog.vue'; +import { omitEmptyExceptPartyFields } from './partyFields'; import { getContactByCustomerId } from '@/api/crm/customer'; import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'; import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo'; @@ -1324,11 +1330,29 @@ const updateContractNoBySupplier = (newSupplierCode: string | undefined) => { } }; +const CONTRACT_NO_STD_RE = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/; + +/** 签订日变更时:标准编号只换日期段,保留序号;并同步名称括号内编号 */ +const syncContractNoDateFromSignDate = (signDateVal: string | null) => { + if (!signDateVal || !form.value.contractNo) return; + const m = String(form.value.contractNo).match(CONTRACT_NO_STD_RE); + if (!m) return; + + const dateStr = signDateVal.replace(/-/g, ''); + if (dateStr.length !== 8) return; + if (m[2] === dateStr) return; + + const nextNo = `${m[1]}/X-JT${dateStr}${m[3]}`; + form.value.contractNo = nextNo; + + if (form.value.contractName) { + form.value.contractName = form.value.contractName.replace(/[((][^))]*[))]$/, `(${nextNo})`); + } +}; + /** * 更新合同名称中的供方简称 - * 合同名称格式: - * - 销售合同、框架合同:供方简称-客户简称(合同编号) - * - 采购合同:客户简称-供方简称(合同编号) + * 统一格式:{供方简称}-{需方简称}({合同编号}) */ const updateContractNameBySupplier = (newSupplierShortName: string | undefined) => { if (!form.value.contractName || !newSupplierShortName) { @@ -1336,7 +1360,6 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined) } const contractName = form.value.contractName; - const contractType = form.value.contractType; // 提取合同编号(在括号中,支持中文和英文括号) const contractNoMatch = contractName.match(/[((](.+?)[))]$/); @@ -1348,78 +1371,21 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined) // 常见的供方简称列表(用于识别和替换) const commonSupplierNames = ['山峡', 'SX']; - // 根据合同类型解析和替换 - if (contractType === 'sale' || contractType === 'framework') { - // 销售/框架合同格式:供方简称-客户简称(合同编号) - // 例如:山峡-客户A(SX/X-JT20250926002) - const parts = nameBody.split('-').map((p) => p.trim()); - if (parts.length >= 2) { - // 替换第一个部分(供方简称) - parts[0] = newSupplierShortName; - const newNameBody = parts.join('-'); - form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; - } else if (parts.length === 1) { - // 如果只有一个部分,判断是否是供方简称 - const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name)); - if (isSupplierName) { - // 是供方简称,替换它 - form.value.contractName = contractNo ? `${newSupplierShortName}(${contractNo})` : newSupplierShortName; - } else { - // 可能是客户简称,保持原样,在前面添加供方简称 - form.value.contractName = contractNo ? `${newSupplierShortName}-${parts[0]}(${contractNo})` : `${newSupplierShortName}-${parts[0]}`; - } + // 统一格式:供方简称-需方简称(合同编号) + const parts = nameBody.split('-').map((p) => p.trim()); + if (parts.length >= 2) { + parts[0] = newSupplierShortName; + const newNameBody = parts.join('-'); + form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; + } else if (parts.length === 1) { + const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name)); + if (isSupplierName) { + form.value.contractName = contractNo ? `${newSupplierShortName}(${contractNo})` : newSupplierShortName; + } else { + form.value.contractName = contractNo + ? `${newSupplierShortName}-${parts[0]}(${contractNo})` + : `${newSupplierShortName}-${parts[0]}`; } - } else if (contractType === 'purchase') { - // 采购合同格式:客户简称-供方简称(合同编号) - // 例如:客户A-山峡(SX/X-JT20250926002) - const parts = nameBody.split('-').map((p) => p.trim()); - if (parts.length >= 2) { - // 替换最后一个部分(供方简称) - parts[parts.length - 1] = newSupplierShortName; - const newNameBody = parts.join('-'); - form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; - } else if (parts.length === 1) { - // 如果只有一个部分,判断是否是供方简称 - const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name)); - if (isSupplierName) { - // 是供方简称,替换它 - form.value.contractName = contractNo ? `${newSupplierShortName}(${contractNo})` : newSupplierShortName; - } else { - // 可能是客户简称,保持原样,在后面添加供方简称 - form.value.contractName = contractNo ? `${parts[0]}-${newSupplierShortName}(${contractNo})` : `${parts[0]}-${newSupplierShortName}`; - } - } - } else { - // 其他类型,尝试替换所有可能的供方简称 - let updatedName = contractName; - let replaced = false; - - for (const oldName of commonSupplierNames) { - if (updatedName.includes(oldName)) { - // 使用正则替换,保持格式 - updatedName = updatedName.replace(new RegExp(oldName, 'g'), newSupplierShortName); - replaced = true; - break; - } - } - - // 如果没有找到已知的供方简称,尝试在名称开头或结尾查找 - if (!replaced && nameBody) { - // 检查是否以常见供方简称开头或结尾 - for (const oldName of commonSupplierNames) { - if (nameBody.startsWith(oldName)) { - updatedName = contractName.replace(oldName, newSupplierShortName); - replaced = true; - break; - } else if (nameBody.endsWith(oldName)) { - updatedName = contractName.replace(oldName, newSupplierShortName); - replaced = true; - break; - } - } - } - - form.value.contractName = updatedName; } // 如果合同编号已更新,同步更新合同名称中的合同编号 @@ -1430,10 +1396,8 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined) }; /** - * 更新合同名称中的客户简称 - * 合同名称格式: - * - 销售合同、框架合同:供方简称-客户简称(合同编号) - * - 采购合同:客户简称-供方简称(合同编号) + * 更新合同名称中的客户简称(需方) + * 统一格式:{供方简称}-{需方简称}({合同编号}) */ const updateContractNameByCustomer = (newCustomerShortName: string | undefined) => { if (!form.value.contractName || !newCustomerShortName) { @@ -1441,7 +1405,6 @@ const updateContractNameByCustomer = (newCustomerShortName: string | undefined) } const contractName = form.value.contractName; - const contractType = form.value.contractType; // 提取合同编号(在括号中,支持中文和英文括号) const contractNoMatch = contractName.match(/[((](.+?)[))]$/); @@ -1453,92 +1416,21 @@ const updateContractNameByCustomer = (newCustomerShortName: string | undefined) // 常见的供方简称列表(用于识别,避免误替换) const commonSupplierNames = ['山峡', 'SX']; - // 根据合同类型解析和替换 - if (contractType === 'sale' || contractType === 'framework') { - // 销售/框架合同格式:供方简称-客户简称(合同编号) - // 例如:山峡-客户A(SX/X-JT20250926002) - const parts = nameBody.split('-').map((p) => p.trim()); - if (parts.length >= 2) { - // 替换第二个部分(客户简称) - parts[1] = newCustomerShortName; - const newNameBody = parts.join('-'); - form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; - } else if (parts.length === 1) { - // 如果只有一个部分,判断是否是供方简称 - const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name)); - if (isSupplierName) { - // 是供方简称,在后面添加客户简称 - form.value.contractName = contractNo ? `${parts[0]}-${newCustomerShortName}(${contractNo})` : `${parts[0]}-${newCustomerShortName}`; - } else { - // 可能是客户简称,替换它,但需要添加供方简称 - // 这里保持原样,只替换客户简称部分(如果当前没有供方简称,则只更新客户简称) - form.value.contractName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName; - } + // 统一格式:供方简称-需方简称(合同编号) + const parts = nameBody.split('-').map((p) => p.trim()); + if (parts.length >= 2) { + parts[1] = newCustomerShortName; + const newNameBody = parts.join('-'); + form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; + } else if (parts.length === 1) { + const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name)); + if (isSupplierName) { + form.value.contractName = contractNo + ? `${parts[0]}-${newCustomerShortName}(${contractNo})` + : `${parts[0]}-${newCustomerShortName}`; + } else { + form.value.contractName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName; } - } else if (contractType === 'purchase') { - // 采购合同格式:客户简称-供方简称(合同编号) - // 例如:客户A-山峡(SX/X-JT20250926002) - const parts = nameBody.split('-').map((p) => p.trim()); - if (parts.length >= 2) { - // 替换第一个部分(客户简称) - parts[0] = newCustomerShortName; - const newNameBody = parts.join('-'); - form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; - } else if (parts.length === 1) { - // 如果只有一个部分,判断是否是供方简称 - const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name)); - if (isSupplierName) { - // 是供方简称,在前面添加客户简称 - form.value.contractName = contractNo ? `${newCustomerShortName}-${parts[0]}(${contractNo})` : `${newCustomerShortName}-${parts[0]}`; - } else { - // 可能是客户简称,替换它 - form.value.contractName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName; - } - } - } else { - // 其他类型,尝试智能替换客户简称 - // 策略:如果名称中包含已知的供方简称,则替换另一个部分;否则尝试替换非供方简称的部分 - let updatedName = contractName; - let replaced = false; - - // 先检查是否包含供方简称 - let supplierNameInBody = ''; - for (const supplierName of commonSupplierNames) { - if (nameBody.includes(supplierName)) { - supplierNameInBody = supplierName; - break; - } - } - - if (supplierNameInBody) { - // 如果包含供方简称,替换另一个部分 - const parts = nameBody.split('-').map((p) => p.trim()); - if (parts.length >= 2) { - // 找到供方简称的位置并替换另一个部分 - const supplierIndex = parts.findIndex((p) => p.includes(supplierNameInBody)); - const customerIndex = supplierIndex === 0 ? 1 : 0; - parts[customerIndex] = newCustomerShortName; - const newNameBody = parts.join('-'); - updatedName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; - replaced = true; - } - } - - // 如果没有替换成功,尝试直接替换非供方简称的部分 - if (!replaced && nameBody) { - const parts = nameBody.split('-').map((p) => p.trim()); - if (parts.length >= 2) { - // 替换第一个部分(假设是客户简称) - parts[0] = newCustomerShortName; - const newNameBody = parts.join('-'); - updatedName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody; - } else if (parts.length === 1 && !commonSupplierNames.some((name) => parts[0].includes(name))) { - // 如果只有一个部分且不是供方简称,则替换它 - updatedName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName; - } - } - - form.value.contractName = updatedName; } // 如果合同编号已更新,同步更新合同名称中的合同编号 @@ -1679,6 +1571,7 @@ const handleSignDateChange = (val: string | null) => { form.value.startDate = ''; form.value.endDate = ''; } + syncContractNoDateFromSignDate(val); // 触发合同期限字段的验证 nextTick(() => { formRef.value?.validateField('startDate'); @@ -2184,12 +2077,8 @@ const handleSaveDraft = async () => { data.attachmentFiles = JSON.stringify(attachmentsWithCategory); } - // 清理未填写的字段,避免发送 undefined 到后端 - Object.keys(data).forEach((key) => { - if (data[key] === undefined || data[key] === '') { - delete data[key]; - } - }); + // 清理未填写的字段;供/需方空字符串保留,以便清空落库 + omitEmptyExceptPartyFields(data); // 联系人字段映射(前端contactsName/contactsPhone映射回后端contactName/contactMobile) if (data.contactsName) { @@ -2366,12 +2255,8 @@ const handleSubmit = async () => { data.attachmentFiles = JSON.stringify(attachmentsWithCategory); } - // 清理未填写的字段,避免发送 undefined 到后端 - Object.keys(data).forEach((key) => { - if (data[key] === undefined || data[key] === '') { - delete data[key]; - } - }); + // 清理未填写的字段;供/需方空字符串保留,以便清空落库 + omitEmptyExceptPartyFields(data); // 联系人字段映射(前端contactsName/contactsPhone映射回后端contactName/contactMobile) if (data.contactsName) {