提交
This commit is contained in:
1 parent
5fe53c5481
commit
5cf67f60d1
39 files changed
+18999
-15713
No files matched your search
@@ -419,7 +419,6 @@ body {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-if="isFrameworkContractType(detailContract.contractType)" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.price || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -230,7 +230,7 @@
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-if="isFrameworkContractType(detailContract.contractType)" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -242,7 +242,7 @@
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-if="isFrameworkContractType(detailContract.contractType)" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -265,7 +265,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detailContract.contractType === 'framework' && !hideProductPrice">
|
||||
<template v-if="isFrameworkContractType(detailContract.contractType) && !hideProductPrice">
|
||||
<el-divider content-position="left">框架合同调价明细</el-divider>
|
||||
<p class="text-muted" style="font-size: 13px; margin-bottom: 12px">按生效区间记录含税单价及调价函;后续结算可据此核对。</p>
|
||||
<div
|
||||
@@ -850,6 +850,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { isFrameworkContractType } from '../contractType'
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
@@ -1771,7 +1772,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
const typeLabel = dictStore.getLabel('crm_contract_type', contractData.contractType);
|
||||
return typeLabel || '销售合同';
|
||||
};
|
||||
const isFrameworkContract = getContractTypeName() === '框架合同';
|
||||
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
|
||||
|
||||
// 计算产品相关数据
|
||||
const productTotal = parseFloat(String(contractData.amount || 0));
|
||||
|
||||
@@ -11,8 +11,20 @@
|
||||
:close-on-click-modal="false"
|
||||
@opened="handleOpened"
|
||||
>
|
||||
<div class="contract-form-body">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
|
||||
<div
|
||||
class="contract-form-body"
|
||||
v-loading="formLoading"
|
||||
element-loading-text="正在加载合同数据..."
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="120px"
|
||||
class="crm-dialog-form"
|
||||
:disabled="formLoading"
|
||||
>
|
||||
<!-- 合同基础信息(一行 4 列) -->
|
||||
<div class="contract-basic-info">
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
@@ -420,7 +432,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 框架合同调价区间 -->
|
||||
<template v-if="form.contractType === 'framework'">
|
||||
<template v-if="isFrameworkContractType(form.contractType)">
|
||||
<el-divider content-position="left">框架合同调价区间</el-divider>
|
||||
<p class="text-muted mb-3" style="font-size: 13px">
|
||||
按产品维护生效日期区间与含税单价(可选);若填写区间则须完整(日期、单价、调价函)。合同总金额不与此联动,仍为 0。
|
||||
@@ -511,11 +523,18 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:loading="saveDraftLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSaveDraft"
|
||||
>
|
||||
<el-icon><FolderChecked /></el-icon>保存草稿
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -556,7 +575,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { ref, reactive, computed, nextTick, watch } from 'vue'
|
||||
import type { FormInstance, UploadRequestOptions } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
@@ -567,6 +586,7 @@ import {
|
||||
type ContractProductForm,
|
||||
type ContractProductPriceTierForm
|
||||
} from '@/api/crm/contract/index'
|
||||
import { isFrameworkContractType } from '../contractType'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { $modal } from '@/utils/modal'
|
||||
@@ -607,7 +627,7 @@ import {
|
||||
import { fetchProductDetail } from '@/api/crm/product'
|
||||
import ApprovalDialog from './ApprovalDialog.vue'
|
||||
import { omitEmptyExceptPartyFields } from '../partyFields'
|
||||
import { getContactByCustomerId } from '@/api/crm/customer'
|
||||
import { getContactByCustomerId, fetchCustomerDetail } from '@/api/crm/customer'
|
||||
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'
|
||||
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo'
|
||||
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo'
|
||||
@@ -744,8 +764,9 @@ const originalTemplateContent = ref<string>('')
|
||||
// 供方信息列表
|
||||
const supplierList = ref<SupplierInfoVO[]>([])
|
||||
|
||||
// 客户简称(用于按合同类型重建合同名称)
|
||||
// 客户简称 / 开票公司名(用于按供需方面板匹配简称)
|
||||
const customerShortName = ref('')
|
||||
const customerInvoiceName = ref('')
|
||||
|
||||
// 表单校验规则
|
||||
const rules = ref({
|
||||
@@ -773,12 +794,25 @@ const rules = ref({
|
||||
// 附件列表
|
||||
const attachmentList = ref<ContractAttachmentItem[]>([])
|
||||
|
||||
const isFrameworkContract = computed(() => form.value.contractType === 'framework')
|
||||
const isFrameworkContract = computed(() => isFrameworkContractType(form.value.contractType))
|
||||
|
||||
// 加载状态
|
||||
const formLoading = ref(false)
|
||||
const saveDraftLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
/** 编辑打开时立即进入加载态,避免空白表单可被误改 */
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible && props.editId) {
|
||||
formLoading.value = true
|
||||
} else if (!visible) {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 组件引用
|
||||
const formRef = ref<FormInstance>()
|
||||
const approvalDialogRef = ref()
|
||||
@@ -820,21 +854,35 @@ interface QuoteVO {
|
||||
/** 对话框打开后的初始化 */
|
||||
const handleOpened = async () => {
|
||||
resetPartyExpand()
|
||||
await Promise.all([loadSupplierList(), loadUserList()])
|
||||
const isEdit = !!props.editId
|
||||
if (isEdit) {
|
||||
formLoading.value = true
|
||||
}
|
||||
try {
|
||||
await Promise.all([loadSupplierList(), loadUserList()])
|
||||
|
||||
if (props.editId) {
|
||||
await loadContractDetail(props.editId)
|
||||
} else {
|
||||
resetForm()
|
||||
form.value.signDate = new Date().toISOString().split('T')[0]
|
||||
handleSignDateChange(form.value.signDate)
|
||||
if (props.editId) {
|
||||
await loadContractDetail(props.editId)
|
||||
} else {
|
||||
resetForm()
|
||||
form.value.signDate = new Date().toISOString().split('T')[0]
|
||||
handleSignDateChange(form.value.signDate)
|
||||
|
||||
// 设置默认供方主体为"SX"
|
||||
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
|
||||
if (defaultSupplier) {
|
||||
form.value.supplierId = defaultSupplier.id
|
||||
await handleSupplierChange(defaultSupplier.id)
|
||||
// 设置默认供方主体为"SX"
|
||||
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
|
||||
if (defaultSupplier) {
|
||||
form.value.supplierId = defaultSupplier.id
|
||||
await handleSupplierChange(defaultSupplier.id)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('合同表单初始化失败:', error)
|
||||
$modal.msgError(error?.message || '加载合同数据失败')
|
||||
if (isEdit) {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,6 +939,7 @@ const resetForm = () => {
|
||||
resetPartyExpand()
|
||||
originalTemplateContent.value = ''
|
||||
customerShortName.value = ''
|
||||
customerInvoiceName.value = ''
|
||||
contractDateRange.value = [] as any
|
||||
attachmentList.value = []
|
||||
selectedProducts.value = []
|
||||
@@ -972,7 +1021,7 @@ const loadContractDetail = async (id: number | string) => {
|
||||
|
||||
await enrichContractProductBrandMeta(form.value.products)
|
||||
|
||||
if (contractData.contractType === 'framework') {
|
||||
if (isFrameworkContractType(contractData.contractType)) {
|
||||
form.value.products.forEach((product: any) => {
|
||||
product.price = 0
|
||||
product.subtotal = 0
|
||||
@@ -995,7 +1044,25 @@ const loadContractDetail = async (id: number | string) => {
|
||||
await handleAttachmentData()
|
||||
}
|
||||
|
||||
customerShortName.value = inferCustomerShortNameFromContractName()
|
||||
customerShortName.value = ''
|
||||
customerInvoiceName.value = ''
|
||||
if (form.value.customerId) {
|
||||
try {
|
||||
const [invoiceRes, customerRes] = await Promise.all([
|
||||
getDefaultInvoiceInfoByCustomerId(form.value.customerId),
|
||||
fetchCustomerDetail({ id: String(form.value.customerId) })
|
||||
])
|
||||
customerInvoiceName.value = invoiceRes?.data?.companyName || ''
|
||||
customerShortName.value = customerRes?.data?.shortName || ''
|
||||
if (!form.value.customerName && customerRes?.data?.customerName) {
|
||||
form.value.customerName = customerRes.data.customerName
|
||||
}
|
||||
} catch {
|
||||
customerInvoiceName.value = ''
|
||||
}
|
||||
}
|
||||
syncPartiesSwappedFlagFromPanels()
|
||||
rebuildContractName()
|
||||
}
|
||||
|
||||
/** 选择客户 */
|
||||
@@ -1023,9 +1090,19 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
form.value.quoteNo = ''
|
||||
|
||||
try {
|
||||
const invoiceRes = await getDefaultInvoiceInfoByCustomerId(customer.id)
|
||||
const [invoiceRes, customerRes] = await Promise.all([
|
||||
getDefaultInvoiceInfoByCustomerId(customer.id),
|
||||
fetchCustomerDetail({ id: String(customer.id) })
|
||||
])
|
||||
const invoiceData = invoiceRes?.data
|
||||
// 简称以客户主数据为准(列表/选择器可能缺字段)
|
||||
customerShortName.value =
|
||||
(customerRes?.data?.shortName || customer.shortName || '').trim()
|
||||
if (customerRes?.data?.customerName) {
|
||||
form.value.customerName = customerRes.data.customerName
|
||||
}
|
||||
|
||||
customerInvoiceName.value = invoiceData?.companyName || ''
|
||||
if (invoiceData && invoiceData.companyName) {
|
||||
if (isPartiesSwapped.value) {
|
||||
form.value.partyA = invoiceData.companyName || ''
|
||||
@@ -1065,6 +1142,7 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户开票信息失败:', error)
|
||||
customerInvoiceName.value = ''
|
||||
if (isPartiesSwapped.value) {
|
||||
form.value.partyA = customer.customerName
|
||||
form.value.partyAAddress = fullAddress
|
||||
@@ -1116,7 +1194,9 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
console.error('获取客户联系人失败:', error)
|
||||
}
|
||||
|
||||
customerShortName.value = customer.shortName || ''
|
||||
if (!customerShortName.value) {
|
||||
customerShortName.value = (customer.shortName || '').trim()
|
||||
}
|
||||
rebuildContractName()
|
||||
}
|
||||
}
|
||||
@@ -1198,7 +1278,7 @@ const handleQuoteConfirm = (quote: QuoteVO) => {
|
||||
$modal
|
||||
.confirm('是否要从报价单中带入产品明细?')
|
||||
.then(async () => {
|
||||
const isFw = form.value.contractType === 'framework'
|
||||
const isFw = isFrameworkContractType(form.value.contractType)
|
||||
form.value.products = quote.productList!.map((item: any) => ({
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
@@ -1363,82 +1443,79 @@ const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
rebuildContractName()
|
||||
}
|
||||
|
||||
const COMMON_SUPPLIER_SHORT_NAMES = ['山峡', 'SX']
|
||||
/** 面板单位全称 → 匹配交易主体/客户主数据取简称;对不上用全称 */
|
||||
const resolvePartyShortName = (partyName?: string): string => {
|
||||
const party = (partyName || '').trim()
|
||||
if (!party) return ''
|
||||
|
||||
const extractContractNameParts = (contractName: string) => {
|
||||
const contractNoMatch = contractName.match(/[((](.+?)[))]$/)
|
||||
const contractNoInName = contractNoMatch ? contractNoMatch[1] : ''
|
||||
const nameBody = contractNoMatch ? contractName.substring(0, contractNoMatch.index).trim() : contractName.trim()
|
||||
return { contractNoInName, nameBody }
|
||||
}
|
||||
|
||||
const getSupplierShortName = (): string => {
|
||||
const supplier = form.value.supplierId ? supplierList.value.find((s) => s.id === form.value.supplierId) : undefined
|
||||
return supplier?.shortName || supplier?.supplierName || '山峡'
|
||||
const supplierName = (supplier?.supplierName || '').trim()
|
||||
const supplierShort = (supplier?.shortName || supplierName || '').trim()
|
||||
if (supplierName && party === supplierName) return supplierShort || party
|
||||
if (supplierShort && party === supplierShort) return supplierShort
|
||||
|
||||
const customerName = (form.value.customerName || '').trim()
|
||||
const customerShort = (customerShortName.value || '').trim()
|
||||
const invoiceName = (customerInvoiceName.value || '').trim()
|
||||
if (customerName && party === customerName) return customerShort || customerName
|
||||
if (customerShort && party === customerShort) return customerShort
|
||||
if (invoiceName && party === invoiceName) return customerShort || invoiceName
|
||||
|
||||
return party
|
||||
}
|
||||
|
||||
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 ''
|
||||
|
||||
const { nameBody } = extractContractNameParts(form.value.contractName)
|
||||
const parts = nameBody
|
||||
.split('-')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
if (parts.length === 0) return ''
|
||||
|
||||
const supplierShort = getSupplierShortName()
|
||||
if (parts.length === 1) {
|
||||
return isSupplierShortNamePart(parts[0], supplierShort) ? '' : parts[0]
|
||||
}
|
||||
|
||||
// 统一格式:供方(交易主体)-需方(客户);供方在前则取第二段为客户简称
|
||||
if (isSupplierShortNamePart(parts[0], supplierShort)) return parts[1]
|
||||
if (isSupplierShortNamePart(parts[1], supplierShort)) return parts[0]
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
/** 重建合同名称:统一为 {供方简称}-{需方简称}({合同编号}),不因采购反序 */
|
||||
/** 重建合同名称:始终 {供方简称}-{需方简称}({合同编号}),随面板调换 */
|
||||
const rebuildContractName = () => {
|
||||
if (!form.value.contractName) return
|
||||
|
||||
const customerShort = inferCustomerShortNameFromContractName()
|
||||
const supplierShort = getSupplierShortName()
|
||||
if (!customerShort && !supplierShort) return
|
||||
|
||||
const { contractNoInName } = extractContractNameParts(form.value.contractName)
|
||||
const contractNo = form.value.contractNo || contractNoInName
|
||||
const nameBody = [supplierShort, customerShort].filter(Boolean).join('-')
|
||||
|
||||
const partyAShort = resolvePartyShortName(form.value.partyA)
|
||||
const partyBShort = resolvePartyShortName(form.value.partyB)
|
||||
const nameBody = [partyAShort, partyBShort].filter(Boolean).join('-')
|
||||
if (!nameBody) return
|
||||
|
||||
const contractNo = (form.value.contractNo || '').trim()
|
||||
form.value.contractName = contractNo ? `${nameBody}(${contractNo})` : nameBody
|
||||
if (customerShort) {
|
||||
customerShortName.value = customerShort
|
||||
}
|
||||
|
||||
/** 根据面板与交易主体/客户推断是否已调换(编辑回显用) */
|
||||
const syncPartiesSwappedFlagFromPanels = () => {
|
||||
const partyA = (form.value.partyA || '').trim()
|
||||
const partyB = (form.value.partyB || '').trim()
|
||||
if (!partyA && !partyB) {
|
||||
isPartiesSwapped.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const supplier = form.value.supplierId
|
||||
? supplierList.value.find((s) => s.id === form.value.supplierId)
|
||||
: undefined
|
||||
const supplierName = (supplier?.supplierName || '').trim()
|
||||
|
||||
if (supplierName) {
|
||||
if (partyB === supplierName && partyA !== supplierName) {
|
||||
isPartiesSwapped.value = true
|
||||
return
|
||||
}
|
||||
if (partyA === supplierName) {
|
||||
isPartiesSwapped.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const customerName = (form.value.customerName || '').trim()
|
||||
const invoiceName = (customerInvoiceName.value || '').trim()
|
||||
if ((customerName && partyA === customerName) || (invoiceName && partyA === invoiceName)) {
|
||||
isPartiesSwapped.value = true
|
||||
return
|
||||
}
|
||||
if ((customerName && partyB === customerName) || (invoiceName && partyB === invoiceName)) {
|
||||
isPartiesSwapped.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 供需方调换 */
|
||||
/** 供需方调换:仅交换面板字段,勿再调 handleSupplierChange(会按交易主体覆盖一侧,导致两边都变成交易主体) */
|
||||
const handleSwapParties = async () => {
|
||||
const currentSupplierId = form.value.supplierId
|
||||
|
||||
const temp = {
|
||||
partyA: form.value.partyA,
|
||||
partyAAddress: form.value.partyAAddress,
|
||||
@@ -1471,17 +1548,14 @@ const handleSwapParties = async () => {
|
||||
form.value.partyBAccount = temp.partyAAccount
|
||||
|
||||
isPartiesSwapped.value = !isPartiesSwapped.value
|
||||
|
||||
if (currentSupplierId) {
|
||||
await handleSupplierChange(currentSupplierId)
|
||||
}
|
||||
rebuildContractName()
|
||||
|
||||
$modal.msgSuccess('供需方信息已调换')
|
||||
}
|
||||
|
||||
/** 合同类型变化处理 */
|
||||
const handleContractTypeChange = (value: string) => {
|
||||
if (value === 'framework') {
|
||||
if (isFrameworkContractType(value)) {
|
||||
form.value.amount = 0
|
||||
form.value.products?.forEach((p) => {
|
||||
p.price = 0
|
||||
@@ -1573,7 +1647,7 @@ const handleAddProduct = () => {
|
||||
|
||||
/** 产品选择确认 */
|
||||
const handleProductSelected = (products: any[]) => {
|
||||
const isFw = form.value.contractType === 'framework'
|
||||
const isFw = isFrameworkContractType(form.value.contractType)
|
||||
products.forEach((product) => {
|
||||
const brandMeta = initLineBrandMetaFromProduct(product)
|
||||
form.value.products.push({
|
||||
@@ -1718,7 +1792,7 @@ const handlePackageSpecChange = (product: ExtendedContractProductForm, value: st
|
||||
|
||||
/** 计算产品金额 */
|
||||
const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
||||
if (form.value.contractType === 'framework') {
|
||||
if (isFrameworkContractType(form.value.contractType)) {
|
||||
product.price = 0
|
||||
product.subtotal = 0
|
||||
product.taxAmount = 0
|
||||
@@ -1742,7 +1816,7 @@ const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
||||
|
||||
/** 计算合计金额 */
|
||||
const calculateTotalAmount = () => {
|
||||
if (form.value.contractType === 'framework') {
|
||||
if (isFrameworkContractType(form.value.contractType)) {
|
||||
form.value.amount = 0
|
||||
return
|
||||
}
|
||||
@@ -1822,7 +1896,7 @@ const buildProductsPayload = () => {
|
||||
if (product.id) {
|
||||
base.id = product.id
|
||||
}
|
||||
if (form.value.contractType === 'framework') {
|
||||
if (isFrameworkContractType(form.value.contractType)) {
|
||||
base.price = 0
|
||||
base.subtotal = 0
|
||||
base.taxAmount = 0
|
||||
@@ -1931,6 +2005,7 @@ const handleSaveDraft = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
rebuildContractName()
|
||||
saveDraftLoading.value = true
|
||||
try {
|
||||
const attachmentJson = serializeContractAttachments(attachmentList.value)
|
||||
@@ -2012,7 +2087,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || form.value.contractType === 'framework'
|
||||
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || isFrameworkContractType(form.value.contractType)
|
||||
|
||||
const validateProductRowCommon = (product: any, rowNum: number): boolean => {
|
||||
if (!product.productName || String(product.productName).trim() === '') {
|
||||
@@ -2044,7 +2119,7 @@ const handleSubmit = async () => {
|
||||
const product = form.value.products[i]
|
||||
const rowNum = i + 1
|
||||
if (!validateProductRowCommon(product, rowNum)) return
|
||||
const isFw = form.value.contractType === 'framework'
|
||||
const isFw = isFrameworkContractType(form.value.contractType)
|
||||
if (!isFw) {
|
||||
if (!product.price || Number(product.price) <= 0) {
|
||||
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`)
|
||||
@@ -2098,6 +2173,7 @@ const handleSubmit = async () => {
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
rebuildContractName()
|
||||
const attachmentJson = serializeContractAttachments(attachmentList.value)
|
||||
const data: any = { ...form.value }
|
||||
|
||||
@@ -2215,6 +2291,8 @@ const loadSupplierList = async () => {
|
||||
}
|
||||
|
||||
.contract-form-body {
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** 框架类合同类型(含历史 framework,兼容未刷完的数据) */
|
||||
export const FRAMEWORK_CONTRACT_TYPES = [
|
||||
'framework',
|
||||
'framework_sale',
|
||||
'framework_purchase'
|
||||
] as const
|
||||
|
||||
export type FrameworkContractType = (typeof FRAMEWORK_CONTRACT_TYPES)[number]
|
||||
|
||||
export function isFrameworkContractType(type?: string | null): boolean {
|
||||
return FRAMEWORK_CONTRACT_TYPES.includes((type || '').trim() as FrameworkContractType)
|
||||
}
|
||||
@@ -308,19 +308,19 @@
|
||||
<el-table-column label="数量" prop="quantity" width="110" align="right" />
|
||||
<el-table-column label="含税单价/元" prop="price" width="130" align="right">
|
||||
<template #default="scope">
|
||||
<span v-if="contract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-if="isFrameworkContractType(contract.contractType)" class="text-muted">随行就市</span>
|
||||
<template v-else>¥{{ parseFloat(scope.row.price || 0).toFixed(2) }}</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="含税总价/元" prop="subtotal" width="130" align="right">
|
||||
<template #default="scope">
|
||||
<span v-if="contract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-if="isFrameworkContractType(contract.contractType)" class="text-muted">随行就市</span>
|
||||
<span v-else class="text-success font-weight-bold">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税费/元" prop="taxAmount" width="120" align="right">
|
||||
<template #default="scope">
|
||||
<span v-if="contract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-if="isFrameworkContractType(contract.contractType)" class="text-muted">随行就市</span>
|
||||
<span v-else>¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -337,7 +337,7 @@
|
||||
</el-table>
|
||||
|
||||
<!-- 框架合同:各产品调价明细(仅详情页展示,不参与打印模板 contractContent) -->
|
||||
<template v-if="contract.contractType === 'framework'">
|
||||
<template v-if="isFrameworkContractType(contract.contractType)">
|
||||
<el-divider content-position="left">框架合同调价明细</el-divider>
|
||||
<p class="text-muted" style="font-size: 13px; margin-bottom: 12px">按生效区间记录含税单价及调价函;后续结算可据此核对。</p>
|
||||
<div
|
||||
@@ -1033,6 +1033,7 @@ import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||
import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
|
||||
import { getOpportunity } from '@/api/crm/opportunity';
|
||||
import { getQuote } from '@/api/crm/quote';
|
||||
import { isFrameworkContractType } from './contractType';
|
||||
import {
|
||||
parseCrmAttachmentField,
|
||||
resolveCrmAttachmentFiles,
|
||||
|
||||
@@ -374,7 +374,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template v-if="form.contractType === 'framework'">
|
||||
<template v-if="isFrameworkContractType(form.contractType)">
|
||||
<el-divider content-position="left">框架合同调价区间</el-divider>
|
||||
<p class="text-muted mb-3" style="font-size: 13px">
|
||||
按产品维护生效日期区间与含税单价(可选);若填写区间则须完整(日期、单价、调价函)。合同总金额不与此联动,仍为 0。
|
||||
@@ -587,6 +587,7 @@ import {
|
||||
type ContractProductForm,
|
||||
type ContractProductPriceTierForm
|
||||
} from '@/api/crm/contract/index';
|
||||
import { isFrameworkContractType } from './contractType';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { $modal } from '@/utils/modal';
|
||||
@@ -606,7 +607,7 @@ 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 { getContactByCustomerId, fetchCustomerDetail } from '@/api/crm/customer';
|
||||
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types';
|
||||
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo';
|
||||
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
|
||||
@@ -714,6 +715,10 @@ const originalTemplateContent = ref<string>('');
|
||||
// 供方信息列表
|
||||
const supplierList = ref<SupplierInfoVO[]>([]);
|
||||
|
||||
// 客户简称 / 开票公司名(用于按供需方面板匹配简称)
|
||||
const customerShortName = ref('');
|
||||
const customerInvoiceName = ref('');
|
||||
|
||||
// 表单校验规则
|
||||
const rules = ref({
|
||||
contractType: [{ required: true, message: '合同类型不能为空', trigger: 'change' }],
|
||||
@@ -758,7 +763,7 @@ const uploadDataTierLetter = computed(() => ({
|
||||
bizType: 'crm_contract_price_letter'
|
||||
}));
|
||||
|
||||
const isFrameworkContract = computed(() => form.value.contractType === 'framework');
|
||||
const isFrameworkContract = computed(() => isFrameworkContractType(form.value.contractType));
|
||||
|
||||
// 加载状态
|
||||
const saveDraftLoading = ref(false);
|
||||
@@ -877,7 +882,7 @@ const loadContractDetail = async (id: number | string) => {
|
||||
deliveryMethod: product.deliveryMethod || ''
|
||||
}));
|
||||
|
||||
if (contractData.contractType === 'framework') {
|
||||
if (isFrameworkContractType(contractData.contractType)) {
|
||||
form.value.products.forEach((product: any) => {
|
||||
product.price = 0;
|
||||
product.subtotal = 0;
|
||||
@@ -932,7 +937,10 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
// 自动获取客户的默认开票信息并填充到需方信息(乙方)
|
||||
try {
|
||||
// 注意:不要使用 Number() 转换,避免大整数精度丢失
|
||||
const invoiceRes = await getDefaultInvoiceInfoByCustomerId(customer.id);
|
||||
const [invoiceRes, customerRes] = await Promise.all([
|
||||
getDefaultInvoiceInfoByCustomerId(customer.id),
|
||||
fetchCustomerDetail({ id: String(customer.id) })
|
||||
]);
|
||||
console.log('========== 开票信息调试 ==========');
|
||||
console.log('客户ID(原始):', customer.id, '类型:', typeof customer.id);
|
||||
console.log('1. API返回完整数据:', JSON.stringify(invoiceRes, null, 2));
|
||||
@@ -940,6 +948,11 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
// 判断是否有开票信息数据(data可能为null或空对象)
|
||||
const invoiceData = invoiceRes?.data;
|
||||
console.log('2. 开票信息data:', invoiceData);
|
||||
customerShortName.value = (customerRes?.data?.shortName || customer.shortName || '').trim();
|
||||
if (customerRes?.data?.customerName) {
|
||||
form.value.customerName = customerRes.data.customerName;
|
||||
}
|
||||
customerInvoiceName.value = invoiceData?.companyName || '';
|
||||
|
||||
if (invoiceData && invoiceData.companyName) {
|
||||
console.log('3. 开票信息字段详情:');
|
||||
@@ -1017,6 +1030,7 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
console.log('====================================');
|
||||
} catch (error) {
|
||||
console.error('❌ 获取客户开票信息失败:', error);
|
||||
customerInvoiceName.value = '';
|
||||
// 如果获取开票信息失败,使用客户基本信息
|
||||
if (isPartiesSwapped.value) {
|
||||
// 已调换:客户名称对应供方信息区域(partyA)
|
||||
@@ -1082,8 +1096,11 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||||
// 即使获取联系人失败,也不影响客户选择流程
|
||||
}
|
||||
|
||||
// 更新合同名称中的客户简称
|
||||
updateContractNameByCustomer(customer.shortName);
|
||||
// 更新合同名称(按供需方面板);简称以客户主数据为准
|
||||
if (!customerShortName.value) {
|
||||
customerShortName.value = (customer.shortName || '').trim();
|
||||
}
|
||||
rebuildContractName();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1175,7 +1192,7 @@ const handleQuoteConfirm = (quote: QuoteVO) => {
|
||||
$modal
|
||||
.confirm('是否要从报价单中带入产品明细?')
|
||||
.then(() => {
|
||||
const isFw = form.value.contractType === 'framework';
|
||||
const isFw = isFrameworkContractType(form.value.contractType);
|
||||
form.value.products = quote.productList.map((item: any) => ({
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
@@ -1272,8 +1289,8 @@ const handleSupplierChange = async (supplierId: number | undefined) => {
|
||||
// 更新合同编号中的供方编码
|
||||
updateContractNoBySupplier(supplier.supplierCode);
|
||||
|
||||
// 更新合同名称中的供方简称
|
||||
updateContractNameBySupplier(supplier.shortName);
|
||||
// 更新合同名称(按供需方面板)
|
||||
rebuildContractName();
|
||||
|
||||
const allowToxic = Number((supplier as any).allowToxic || 0);
|
||||
const allowExplosive = Number((supplier as any).allowExplosive || 0);
|
||||
@@ -1344,101 +1361,41 @@ const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新合同名称中的供方简称
|
||||
* 统一格式:{供方简称}-{需方简称}({合同编号})
|
||||
* 面板单位全称 → 匹配交易主体/客户主数据取简称;对不上用全称
|
||||
*/
|
||||
const updateContractNameBySupplier = (newSupplierShortName: string | undefined) => {
|
||||
if (!form.value.contractName || !newSupplierShortName) {
|
||||
return;
|
||||
}
|
||||
const resolvePartyShortName = (partyName?: string): string => {
|
||||
const party = (partyName || '').trim();
|
||||
if (!party) return '';
|
||||
|
||||
const contractName = form.value.contractName;
|
||||
const supplier = form.value.supplierId ? supplierList.value.find((s) => s.id === form.value.supplierId) : undefined;
|
||||
const supplierName = (supplier?.supplierName || '').trim();
|
||||
const supplierShort = (supplier?.shortName || supplierName || '').trim();
|
||||
if (supplierName && party === supplierName) return supplierShort || party;
|
||||
if (supplierShort && party === supplierShort) return supplierShort;
|
||||
|
||||
// 提取合同编号(在括号中,支持中文和英文括号)
|
||||
const contractNoMatch = contractName.match(/[((](.+?)[))]$/);
|
||||
const contractNo = contractNoMatch ? contractNoMatch[1] : '';
|
||||
const customerName = (form.value.customerName || '').trim();
|
||||
const customerShort = (customerShortName.value || '').trim();
|
||||
const invoiceName = (customerInvoiceName.value || '').trim();
|
||||
if (customerName && party === customerName) return customerShort || customerName;
|
||||
if (customerShort && party === customerShort) return customerShort;
|
||||
if (invoiceName && party === invoiceName) return customerShort || invoiceName;
|
||||
|
||||
// 提取合同名称主体部分(去掉括号和合同编号)
|
||||
const nameBody = contractNoMatch ? contractName.substring(0, contractNoMatch.index).trim() : contractName.trim();
|
||||
|
||||
// 常见的供方简称列表(用于识别和替换)
|
||||
const commonSupplierNames = ['山峡', 'SX'];
|
||||
|
||||
// 统一格式:供方简称-需方简称(合同编号)
|
||||
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]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果合同编号已更新,同步更新合同名称中的合同编号
|
||||
if (contractNo && form.value.contractNo && contractNo !== form.value.contractNo) {
|
||||
// 替换合同名称中的旧合同编号
|
||||
form.value.contractName = form.value.contractName.replace(/[((].+?[))]$/, `(${form.value.contractNo})`);
|
||||
}
|
||||
return party;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新合同名称中的客户简称(需方)
|
||||
* 统一格式:{供方简称}-{需方简称}({合同编号})
|
||||
*/
|
||||
const updateContractNameByCustomer = (newCustomerShortName: string | undefined) => {
|
||||
if (!form.value.contractName || !newCustomerShortName) {
|
||||
return;
|
||||
}
|
||||
/** 重建合同名称:始终 {供方简称}-{需方简称}({合同编号}) */
|
||||
const rebuildContractName = () => {
|
||||
const partyAShort = resolvePartyShortName(form.value.partyA);
|
||||
const partyBShort = resolvePartyShortName(form.value.partyB);
|
||||
const nameBody = [partyAShort, partyBShort].filter(Boolean).join('-');
|
||||
if (!nameBody) return;
|
||||
|
||||
const contractName = form.value.contractName;
|
||||
|
||||
// 提取合同编号(在括号中,支持中文和英文括号)
|
||||
const contractNoMatch = contractName.match(/[((](.+?)[))]$/);
|
||||
const contractNo = contractNoMatch ? contractNoMatch[1] : '';
|
||||
|
||||
// 提取合同名称主体部分(去掉括号和合同编号)
|
||||
const nameBody = contractNoMatch ? contractName.substring(0, contractNoMatch.index).trim() : contractName.trim();
|
||||
|
||||
// 常见的供方简称列表(用于识别,避免误替换)
|
||||
const commonSupplierNames = ['山峡', 'SX'];
|
||||
|
||||
// 统一格式:供方简称-需方简称(合同编号)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果合同编号已更新,同步更新合同名称中的合同编号
|
||||
if (contractNo && form.value.contractNo && contractNo !== form.value.contractNo) {
|
||||
// 替换合同名称中的旧合同编号
|
||||
form.value.contractName = form.value.contractName.replace(/[((].+?[))]$/, `(${form.value.contractNo})`);
|
||||
}
|
||||
const contractNo = (form.value.contractNo || '').trim();
|
||||
form.value.contractName = contractNo ? `${nameBody}(${contractNo})` : nameBody;
|
||||
};
|
||||
|
||||
/** 供需方调换 */
|
||||
/** 供需方调换:仅交换面板字段,勿再调 handleSupplierChange(会按交易主体覆盖一侧) */
|
||||
const handleSwapParties = async () => {
|
||||
// 保存当前的供方主体ID(调换时保持不变)
|
||||
const currentSupplierId = form.value.supplierId;
|
||||
|
||||
// 交换供方和需方的所有信息(不包括supplierId)
|
||||
const temp = {
|
||||
partyA: form.value.partyA,
|
||||
partyAAddress: form.value.partyAAddress,
|
||||
@@ -1470,20 +1427,15 @@ const handleSwapParties = async () => {
|
||||
form.value.partyBBank = temp.partyABank;
|
||||
form.value.partyBAccount = temp.partyAAccount;
|
||||
|
||||
// 切换调换状态
|
||||
isPartiesSwapped.value = !isPartiesSwapped.value;
|
||||
|
||||
// 如果存在供方主体,根据新的调换状态重新填充对应的信息区域
|
||||
if (currentSupplierId) {
|
||||
await handleSupplierChange(currentSupplierId);
|
||||
}
|
||||
rebuildContractName();
|
||||
|
||||
$modal.msgSuccess('供需方信息已调换');
|
||||
};
|
||||
|
||||
/** 合同类型变化处理 */
|
||||
const handleContractTypeChange = (value: string) => {
|
||||
if (value === 'framework') {
|
||||
if (isFrameworkContractType(value)) {
|
||||
form.value.amount = 0;
|
||||
form.value.products?.forEach((p) => {
|
||||
p.price = 0;
|
||||
@@ -1582,7 +1534,7 @@ const handleAddProduct = () => {
|
||||
|
||||
/** 产品选择确认 */
|
||||
const handleProductSelected = (products: any[]) => {
|
||||
const isFw = form.value.contractType === 'framework';
|
||||
const isFw = isFrameworkContractType(form.value.contractType);
|
||||
products.forEach((product) => {
|
||||
// 允许重复添加相同产品:直接追加到 products 列表
|
||||
form.value.products.push({
|
||||
@@ -1697,7 +1649,7 @@ const handlePackageSpecChange = (product: ExtendedContractProductForm, value: st
|
||||
|
||||
/** 计算产品金额(参考报价单逻辑) */
|
||||
const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
||||
if (form.value.contractType === 'framework') {
|
||||
if (isFrameworkContractType(form.value.contractType)) {
|
||||
product.price = 0;
|
||||
product.subtotal = 0;
|
||||
product.taxAmount = 0;
|
||||
@@ -1725,7 +1677,7 @@ const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
||||
|
||||
/** 计算合计金额 */
|
||||
const calculateTotalAmount = () => {
|
||||
if (form.value.contractType === 'framework') {
|
||||
if (isFrameworkContractType(form.value.contractType)) {
|
||||
form.value.amount = 0;
|
||||
return;
|
||||
}
|
||||
@@ -1801,7 +1753,7 @@ const buildProductsPayload = () => {
|
||||
if (product.id) {
|
||||
base.id = product.id;
|
||||
}
|
||||
if (form.value.contractType === 'framework') {
|
||||
if (isFrameworkContractType(form.value.contractType)) {
|
||||
base.price = 0;
|
||||
base.subtotal = 0;
|
||||
base.taxAmount = 0;
|
||||
@@ -2145,7 +2097,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || form.value.contractType === 'framework';
|
||||
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || isFrameworkContractType(form.value.contractType);
|
||||
|
||||
const validateProductRowCommon = (product: any, rowNum: number): boolean => {
|
||||
if (!product.productName || String(product.productName).trim() === '') {
|
||||
@@ -2178,7 +2130,7 @@ const handleSubmit = async () => {
|
||||
if (!validateProductRowCommon(product, rowNum)) {
|
||||
return;
|
||||
}
|
||||
const isFw = form.value.contractType === 'framework';
|
||||
const isFw = isFrameworkContractType(form.value.contractType);
|
||||
if (!isFw) {
|
||||
if (!product.price || Number(product.price) <= 0) {
|
||||
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`);
|
||||
|
||||
Reference in new issue
Block a user