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:
1 parent
f01057d902
commit
594e4ce38e
5 files changed
+260
-202
No files matched your search
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user