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 <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-03 11:46:11 +08:00
1 parent f01057d902
commit 594e4ce38e
5 files changed
+260 -202

No files matched your search

+90
View File
@@ -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 "")
@@ -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"
@@ -2,7 +2,7 @@
**日期**:2026-08-03
**范围**:`web-admin` 合同管理新增/编辑;`backend` 合同 create/update(草稿)
**状态**:已评审,待实现
**状态**:已实现
## 背景
@@ -19,7 +19,12 @@
<el-input v-model="form.contractNo" placeholder="系统自动生成,格式:SX/X-JT20250926002" maxlength="50" />
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="form.contractName" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
<el-input
v-model="form.contractName"
placeholder="可不填,自动生成:供方简称-需方简称(合同编号)"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="合同类型" prop="contractType" required>
<el-select v-model="form.contractType" placeholder="请选择合同类型" style="width: 100%" @change="handleContractTypeChange">
@@ -242,7 +247,7 @@
</div>
</div>
<div class="list-body list-wrap">
<el-table :data="form.products" class="list-table" stripe @selection-change="handleProductSelectionChange">
<ListTable table-key="crm-contract-components-contract-form-dialog" :data="form.products" class="list-table" stripe @selection-change="handleProductSelectionChange">
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column label="序号" type="index" min-width="60" align="center" header-align="center" />
<el-table-column label="产品名称" min-width="160" :show-overflow-tooltip="true">
@@ -410,7 +415,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
@@ -427,7 +432,7 @@
</el-button>
<div class="panel dialog-list-panel mb-3">
<div class="list-body list-wrap dialog-list-body-only">
<el-table :data="prod.priceTiers || []" class="list-table" stripe empty-text="暂无调价区间">
<ListTable table-key="crm-contract-components-contract-form-dialog-2" :data="prod.priceTiers || []" class="list-table" stripe empty-text="暂无调价区间">
<el-table-column type="index" label="#" min-width="50" align="center" header-align="center" />
<el-table-column label="开始日期" min-width="150">
<template #default="scope">
@@ -468,7 +473,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
</div>
@@ -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
+63 -178
View File
@@ -19,7 +19,12 @@
</el-col>
<el-col :span="6">
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="form.contractName" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
<el-input
v-model="form.contractName"
placeholder="可不填,自动生成:供方简称-需方简称(合同编号)"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="6">
@@ -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) {