diff --git a/src/api/settlement/formalSettlement.js b/src/api/settlement/formalSettlement.js
index 5b43fbb..49a23f7 100644
--- a/src/api/settlement/formalSettlement.js
+++ b/src/api/settlement/formalSettlement.js
@@ -49,3 +49,5 @@ export const applyPayments = data =>
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
export const claimInvoices = data =>
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
+export const queryInvoicePool = params =>
+ request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
diff --git a/src/api/vehicle/customer-archive.js b/src/api/vehicle/customer-archive.js
index 9d859f2..8cfdb5f 100644
--- a/src/api/vehicle/customer-archive.js
+++ b/src/api/vehicle/customer-archive.js
@@ -113,6 +113,26 @@ export const getScoreTemplate = quantificationId => {
});
};
+export const queryKingdeeCustomer = creditCode => {
+ return request({
+ url: '/blade-transport/bfi/customer/query-by-credit-code',
+ method: 'get',
+ params: {
+ creditCode,
+ },
+ });
+};
+
+export const syncKingdee = id => {
+ return request({
+ url: '/blade-transport/customer-archive/sync-kingdee',
+ method: 'post',
+ params: {
+ id,
+ },
+ });
+};
+
export const recognizeBusinessLicenseOcr = imageUrl => {
return request({
url: '/blade-transport/baidu-ocr/recognize-url',
diff --git a/src/axios.js b/src/axios.js
index cd29fa6..cbe611e 100644
--- a/src/axios.js
+++ b/src/axios.js
@@ -223,6 +223,14 @@ axios.interceptors.response.use(
},
error => {
NProgress.done();
+ // 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
+ // 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
+ if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) {
+ ElMessage({
+ message: error?.response?.data?.msg || '请求失败,请稍后重试',
+ type: 'error',
+ });
+ }
return Promise.reject(new Error(error));
}
);
diff --git a/src/views/payment/invoice-application-form.vue b/src/views/payment/invoice-application-form.vue
index 8c6d212..5a8df00 100644
--- a/src/views/payment/invoice-application-form.vue
+++ b/src/views/payment/invoice-application-form.vue
@@ -497,6 +497,7 @@ const emptyLine = () => ({
localId: ++localId,
goodsCategory: '',
goodsName: '',
+ taxClassificationCode: '',
unit: '吨',
quantity: 0,
unitPriceNoTax: 0,
@@ -724,9 +725,11 @@ export default {
const item = this.findInvoiceItem(line);
if (item) {
line.taxRate = Number(item.defaultTaxRate || 0);
+ line.taxClassificationCode = item.taxClassificationCode || '';
} else if (clearInvalid) {
line.goodsName = '';
line.taxRate = 0;
+ line.taxClassificationCode = '';
}
this.recalculateLine(line);
},
@@ -734,6 +737,7 @@ export default {
const names = this.invoiceItemNames(line.goodsCategory);
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
line.taxRate = 0;
+ line.taxClassificationCode = this.findInvoiceItem(line)?.taxClassificationCode || '';
if (names.length === 1) {
line.goodsName = names[0].shortName;
this.handleGoodsNameChange(line);
@@ -743,9 +747,11 @@ export default {
const item = this.findInvoiceItem(line);
if (!item) {
line.taxRate = 0;
+ line.taxClassificationCode = '';
return;
}
line.taxRate = Number(item.defaultTaxRate || 0);
+ line.taxClassificationCode = item.taxClassificationCode || '';
this.recalculateLine(line);
},
validateSettlements(rule, value, callback) {
@@ -1141,6 +1147,7 @@ export default {
lines: sheet.lines.map(line => ({
goodsCategory: line.goodsCategory,
goodsName: line.goodsName,
+ taxClassificationCode: line.taxClassificationCode,
unit: line.unit,
quantity: line.quantity,
unitPriceNoTax: line.unitPriceNoTax,
diff --git a/src/views/settlement/components/formal-settlement-editor.vue b/src/views/settlement/components/formal-settlement-editor.vue
index 496eb46..589f736 100644
--- a/src/views/settlement/components/formal-settlement-editor.vue
+++ b/src/views/settlement/components/formal-settlement-editor.vue
@@ -419,11 +419,69 @@
append-to-body
destroy-on-close
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 重置
+ 查询
+
+
-
-
-
+
+
+
@@ -941,6 +1001,7 @@ import {
getNextNo,
getReceiptClaims,
claimInvoices as claimInvoicesApi,
+ queryInvoicePool,
save,
} from '@/api/settlement/formalSettlement';
import {
@@ -1006,7 +1067,20 @@ export default {
rows: [],
selected: [],
page: { current: 1, size: 10, total: 0 },
+ query: {
+ invoiceNo: '',
+ invoiceDateRange: [],
+ salerTaxNo: '',
+ buyerTaxNo: '',
+ expenseStatus: '',
+ },
},
+ expenseStatusOptions: [
+ { value: '1', label: '未用' },
+ { value: '30', label: '在用' },
+ { value: '60', label: '已用' },
+ { value: '65', label: '已入账' },
+ ],
attachmentUploadFiles: [],
attachmentTypeOptions: [],
attachmentFileTypes: [
@@ -1148,11 +1222,6 @@ export default {
});
return Array.from(names);
},
- invoiceClaimPagedRows() {
- const { current, size } = this.invoiceClaimDialog.page;
- const start = (current - 1) * size;
- return this.invoiceClaimDialog.rows.slice(start, start + size);
- },
},
watch: {
modelValue: {
@@ -1233,6 +1302,13 @@ export default {
this.invoiceClaimDialog.visible = false;
this.invoiceClaimDialog.rows = [];
this.invoiceClaimDialog.selected = [];
+ this.invoiceClaimDialog.query = {
+ invoiceNo: '',
+ invoiceDateRange: [],
+ salerTaxNo: '',
+ buyerTaxNo: '',
+ expenseStatus: '',
+ };
this.attachmentUploadFiles = [];
this.billingRuleDialog = {
visible: false,
@@ -2223,79 +2299,51 @@ export default {
this.adjust.saving = false;
}
},
- claimInvoices() {
- this.invoiceClaimLoading = true;
- try {
- const keyword = String(this.invoiceClaimNo || '').trim();
- const defaultNo = `MOCK${this.$dayjs().format('YYYYMMDD')}`;
- const baseNo = (keyword || defaultNo).slice(0, 29);
- const numbers = [baseNo, `${baseNo}-02`, `${baseNo}-03`];
- const invoiceAmounts = [1000, 2000, 3000];
- const availableAmounts = [1000, 1000, 3000];
- const preferredMatchedAmounts = [1000, 1000, 2000];
- let remainingAmount = Math.max(
- Number(this.form.settlementAmount || this.summaryTotal || 0) -
- this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
- 0
- );
- const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
- const mockRows = numbers
- .map((invoiceNo, index) => {
- const matchedAmount = Math.min(
- preferredMatchedAmounts[index],
- availableAmounts[index],
- remainingAmount
- );
- remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2));
- return {
- invoiceNo,
- invoiceDate: this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD'),
- invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票',
- taxRate: [3, 6, 9][index],
- invoiceAmount: invoiceAmounts[index],
- availableInvoiceAmount: availableAmounts[index],
- matchedAmount,
- attachmentJson: '',
- mockData: true,
- };
- })
- .filter(item => !existingNumbers.has(item.invoiceNo));
- if (!mockRows.length) {
- this.$message.warning('当前发票号码的Mock数据已认领');
- return;
- }
- this.invoices = [...this.invoices, ...mockRows];
- this.$message.success(`已生成${mockRows.length}条Mock发票数据`);
- } finally {
- this.invoiceClaimLoading = false;
- }
- },
openInvoiceClaimDialog() {
- const stamp = Date.now();
- this.invoiceClaimDialog.loading = true;
- this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => {
- const amount = Math.floor(1000 + Math.random() * 49000);
- return {
- invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32),
- invoiceDate: this.$dayjs()
- .subtract(Math.floor(Math.random() * 90), 'day')
- .format('YYYY-MM-DD'),
- invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票',
- issuerName: this.form.payerName || 'Mock开票单位',
- receiverName: this.form.payeeName || 'Mock受票单位',
- invoiceAmount: amount,
- availableInvoiceAmount: amount,
- matchedAmount: amount,
- taxRate: [3, 6, 9, 13][index % 4],
- attachmentJson: '',
- mockData: true,
- };
- });
- this.invoiceClaimDialog.selected = [];
- this.invoiceClaimDialog.page.current = 1;
- this.invoiceClaimDialog.page.total = 80;
+ this.invoiceClaimDialog.query.invoiceNo = String(this.invoiceClaimNo || '').trim();
this.invoiceClaimDialog.visible = true;
- this.invoiceClaimDialog.loading = false;
+ this.handleInvoiceClaimQuery();
+ },
+ handleInvoiceClaimQuery() {
+ this.invoiceClaimDialog.page.current = 1;
+ this.loadInvoiceClaims();
+ },
+ resetInvoiceClaimQuery() {
+ this.invoiceClaimDialog.query = {
+ invoiceNo: '',
+ invoiceDateRange: [],
+ salerTaxNo: '',
+ buyerTaxNo: '',
+ expenseStatus: '',
+ };
+ this.handleInvoiceClaimQuery();
+ },
+ handleInvoiceClaimSizeChange() {
+ this.invoiceClaimDialog.page.current = 1;
+ this.loadInvoiceClaims();
+ },
+ async loadInvoiceClaims() {
+ this.invoiceClaimDialog.loading = true;
+ try {
+ const query = this.invoiceClaimDialog.query;
+ const range = query.invoiceDateRange || [];
+ const data = this.unwrapData(
+ await queryInvoicePool({
+ current: this.invoiceClaimDialog.page.current,
+ size: this.invoiceClaimDialog.page.size,
+ invoiceNo: query.invoiceNo || undefined,
+ invoiceDateStart: range[0] || undefined,
+ invoiceDateEnd: range[1] || undefined,
+ salerTaxNo: query.salerTaxNo || undefined,
+ buyerTaxNo: query.buyerTaxNo || undefined,
+ expenseStatus: query.expenseStatus || undefined,
+ })
+ );
+ this.invoiceClaimDialog.rows = data.records || [];
+ this.invoiceClaimDialog.page.total = Number(data.total || 0);
+ } finally {
+ this.invoiceClaimDialog.loading = false;
+ }
},
async confirmInvoiceClaims() {
if (!this.invoiceClaimDialog.selected.length) {
@@ -2321,7 +2369,11 @@ export default {
if (Number(this.form.settlementAmount || 0) > 0) {
remaining = Math.max(0, remaining - matchedAmount);
}
- return { ...row, matchedAmount };
+ return {
+ ...row,
+ invoiceType: row.invoiceTypeName || row.invoiceType || '',
+ matchedAmount,
+ };
})
.filter(row => row.matchedAmount > 0);
if (!invoices.length) {
@@ -2409,12 +2461,12 @@ export default {
},
viewInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
- if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
+ if (!url) return this.$message.warning('当前发票暂无可查看附件');
window.open(url, '_blank');
},
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
- if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
+ if (!url) return this.$message.warning('当前发票暂无可下载附件');
downloadFileByUrl(url, this.invoiceAttachmentName(row));
},
buildSavePayload() {
diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue
index 9c36d1a..639fbc4 100644
--- a/src/views/vehicle/customer-archive.vue
+++ b/src/views/vehicle/customer-archive.vue
@@ -109,6 +109,12 @@
{{ row.status === 1 ? '启用' : '停用' }}
+
+ 已推送
+ 金蝶已有
+ 推送失败
+ 未推送
+
{{ row.status === 1 ? '停用' : '启用' }}
+
+ 同步金蝶
+
@@ -304,12 +323,22 @@
-
+
-
+
handleDecimalInput('registeredCapital', value)"
/>
@@ -1492,6 +1522,8 @@ import {
remove,
getScoreTemplate,
recognizeBusinessLicenseOcr,
+ queryKingdeeCustomer,
+ syncKingdee,
} from '@/api/vehicle/customer-archive';
import {
getList as getCreditScoreQuantificationList,
@@ -1565,23 +1597,13 @@ export default {
const code = String(value || '')
.trim()
.toUpperCase();
- const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
- const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
if (!code) {
callback();
return;
}
- if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
- callback(new Error('统一社会信用代码必须为18位有效字符'));
- return;
- }
- const sum = [...code.slice(0, 17)].reduce(
- (total, char, index) => total + charset.indexOf(char) * weights[index],
- 0
- );
- const checkCode = charset[(31 - (sum % 31)) % 31];
- if (code[17] !== checkCode) {
- callback(new Error('统一社会信用代码校验码错误'));
+ const error = this.unifiedCreditCodeError(code);
+ if (error) {
+ callback(new Error(error));
return;
}
callback();
@@ -1621,6 +1643,8 @@ export default {
originalAccessType: '',
activeTab: 'scores',
archiveForm: this.emptyArchive(),
+ kingdeeBlocked: false,
+ kingdeeQueryTimer: null,
currentScore: { details: [] },
scoreRecordIndex: -1,
scoreAttachmentFiles: [],
@@ -1967,6 +1991,12 @@ export default {
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
},
+ {
+ label: '金蝶同步状态',
+ prop: 'kingdeeStatus',
+ slot: true,
+ minWidth: 110,
+ },
{
label: '状态',
prop: 'status',
@@ -2004,6 +2034,9 @@ export default {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
+ kingdeeLocked() {
+ return this.archiveForm.kingdeeStatus === 'exist';
+ },
permissionList() {
return {
addBtn: false,
@@ -2179,6 +2212,9 @@ export default {
},
},
watch: {
+ 'archiveForm.unifiedCreditCode'(val) {
+ this.handleKingdeeCreditCodeChange(val);
+ },
'archiveForm.registeredDetailAddress'() {
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
},
@@ -2190,6 +2226,119 @@ export default {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
+ unifiedCreditCodeError(value) {
+ const code = String(value || '')
+ .trim()
+ .toUpperCase();
+ const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
+ const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
+ if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
+ return '统一社会信用代码必须为18位有效字符';
+ }
+ const sum = [...code.slice(0, 17)].reduce(
+ (total, char, index) => total + charset.indexOf(char) * weights[index],
+ 0
+ );
+ const checkCode = charset[(31 - (sum % 31)) % 31];
+ if (code[17] !== checkCode) {
+ return '统一社会信用代码校验码错误';
+ }
+ return '';
+ },
+ resetKingdeeQueryState() {
+ this.kingdeeBlocked = false;
+ clearTimeout(this.kingdeeQueryTimer);
+ this.kingdeeQueryTimer = null;
+ },
+ // 新增态下重新输入统一社会信用代码后,此前金蝶回填的数据即失效,重置回填状态并允许对新代码重新查询
+ resetKingdeeBackfill() {
+ const form = this.archiveForm;
+ form.kingdeeStatus = 'none';
+ form.fullName = '';
+ form.legalPerson = '';
+ form.registeredCapital = '';
+ form.registeredDetailAddress = '';
+ form.businessScope = [];
+ form.businessTermType = '固定期限';
+ form.businessEndDate = '';
+ },
+ // 新增态下统一社会信用代码录入完成后自动查询金蝶(A902-1),防抖 500ms;
+ // 代码一经变化即重新查询(含改回历史代码),不做同码去重
+ handleKingdeeCreditCodeChange(val) {
+ this.kingdeeBlocked = false;
+ if (this.readonly || this.archiveForm.id) return;
+ if (this.archiveForm.kingdeeStatus === 'exist') {
+ this.resetKingdeeBackfill();
+ }
+ const code = String(val || '')
+ .trim()
+ .toUpperCase();
+ if (code.length !== 18 || this.unifiedCreditCodeError(code)) return;
+ clearTimeout(this.kingdeeQueryTimer);
+ this.kingdeeQueryTimer = setTimeout(() => this.fetchKingdeeCustomer(code), 500);
+ },
+ async fetchKingdeeCustomer(code) {
+ let customer = null;
+ try {
+ const res = await queryKingdeeCustomer(code);
+ customer = res?.data?.data || null;
+ } catch (error) {
+ this.$message.warning('金蝶查询失败,可继续填写,审批通过后系统将自动复查');
+ return;
+ }
+ // 防止响应返回时用户已修改代码
+ if (String(this.archiveForm.unifiedCreditCode || '').trim().toUpperCase() !== code) return;
+ // 金蝶不存在(rows 为空,data 为 null 或空对象):静默返回,正常填写,不做提示
+ if (!customer || !customer.status) return;
+ if (customer.status === 'C') {
+ this.applyKingdeeCustomer(customer);
+ this.$alert(
+ '金蝶已创建该客户,将获取金蝶的工商信息,相关字段已自动回填并锁定。如信用代码有误,可直接修改,系统将重新查询。',
+ '提示',
+ {
+ confirmButtonText: '确定',
+ }
+ );
+ } else {
+ this.kingdeeBlocked = true;
+ this.$alert('金蝶审核中,请稍后重新添加。', '提示', { confirmButtonText: '确定' });
+ }
+ },
+ applyKingdeeCustomer(customer) {
+ const form = this.archiveForm;
+ form.kingdeeStatus = 'exist';
+ // 工商信息以金蝶为准,直接覆盖(后续字段锁定)
+ if (customer.name) form.fullName = customer.name;
+ if (customer.artificialperson) form.legalPerson = customer.artificialperson;
+ if (customer.bizpartnerAddress) form.registeredDetailAddress = customer.bizpartnerAddress;
+ if (customer.regcapital) {
+ const capital = String(customer.regcapital).replace(/,/g, '').match(/\d+(?:\.\d+)?/);
+ if (capital) form.registeredCapital = capital[0];
+ }
+ if (customer.businessterm) this.applyBusinessTermRecognition(customer.businessterm);
+ if (customer.businessscope) {
+ const matchedScopes = this.businessScopeOptions
+ .filter(
+ item =>
+ customer.businessscope.includes(item.label) ||
+ customer.businessscope.includes(item.value)
+ )
+ .map(item => item.value);
+ if (matchedScopes.length) form.businessScope = matchedScopes;
+ }
+ if (!form.shortName && customer.simplename) form.shortName = customer.simplename;
+ this.$nextTick(() => {
+ [
+ 'unifiedCreditCode',
+ 'fullName',
+ 'legalPerson',
+ 'registeredCapital',
+ 'businessEndDate',
+ 'businessScope',
+ 'registeredDetailAddress',
+ ].forEach(prop => this.$refs.archiveForm?.validateField(prop));
+ });
+ },
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
checkOverflow(refName, flag) {
const inst = this.$refs[refName];
@@ -2219,6 +2368,9 @@ export default {
invoices: [],
scores: [],
changeRecords: [],
+ kingdeeStatus: 'none',
+ kingdeeFailReason: '',
+ kingdeeSyncTime: null,
};
},
getDetailList(type) {
@@ -3919,6 +4071,7 @@ export default {
}
this.archiveForm = this.emptyArchive();
this.originalAccessType = '';
+ this.resetKingdeeQueryState();
this.qualificationFiles = [];
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
@@ -3927,6 +4080,7 @@ export default {
},
resetArchive() {
this.readonly = false;
+ this.resetKingdeeQueryState();
this.changeRecordDetailVisible = false;
this.changeRecordDetail = null;
this.changeRecordDetailRows = [];
@@ -4047,6 +4201,10 @@ export default {
return archive;
},
saveArchive() {
+ if (this.kingdeeBlocked) {
+ this.$message.warning('金蝶审核中,请稍后重新添加');
+ return;
+ }
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
@@ -4060,6 +4218,10 @@ export default {
});
},
saveAndSubmitArchive() {
+ if (this.kingdeeBlocked) {
+ this.$message.warning('金蝶审核中,请稍后重新添加');
+ return;
+ }
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
@@ -4707,6 +4869,25 @@ export default {
this.$message({ type: 'success', message: '审核通过!' });
});
},
+ handleSyncKingdee(row) {
+ this.$confirm('是否确认推送金蝶?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning',
+ })
+ .then(() => syncKingdee(row.id))
+ .then(res => {
+ const archive = res?.data?.data || {};
+ if (archive.kingdeeStatus === 'synced') {
+ this.$message.success('推送成功,金蝶客户编码与客商编号一致');
+ } else if (archive.kingdeeStatus === 'exist') {
+ this.$message.info('金蝶已有该客商档案,无需推送');
+ } else {
+ this.$message.warning(`推送失败:${archive.kingdeeFailReason || '未知原因'}`);
+ }
+ this.onLoad(this.page);
+ });
+ },
handleReject(row) {
this.$confirm('是否确认审核不通过?', '提示', {
confirmButtonText: '确定',