This commit is contained in:
wuweihua
2026-09-18 17:09:48 +08:00
9 changed files with 221 additions and 18 deletions
@@ -35,6 +35,13 @@ public class CustomerReceipt extends BaseEntity
/** 收款类型:混合 */ /** 收款类型:混合 */
public static final String TYPE_MIXED = "MIXED"; public static final String TYPE_MIXED = "MIXED";
/** 汇率来源:系统默认(本币CNY 1:1) */
public static final String RATE_SOURCE_SYSTEM = "SYSTEM";
/** 汇率来源:汇率表(收款日即期汇率自动带出) */
public static final String RATE_SOURCE_RATE_TABLE = "RATE_TABLE";
/** 汇率来源:手工修改/手工录入 */
public static final String RATE_SOURCE_MANUAL = "MANUAL";
/** 主键 */ /** 主键 */
private Long id; private Long id;
@@ -65,6 +72,12 @@ public class CustomerReceipt extends BaseEntity
/** 对CNY汇率(1原币=?CNY,CNY为1;默认取汇率表,可手工修改) */ /** 对CNY汇率(1原币=?CNY,CNY为1;默认取汇率表,可手工修改) */
private BigDecimal exchangeRate; private BigDecimal exchangeRate;
/** 汇率来源:SYSTEM系统默认(本币1:1)/RATE_TABLE汇率表/MANUAL手工修改 */
private String rateSource;
/** 汇率来源说明(汇率表来源及命中路径,或手工录入说明) */
private String rateSourceDesc;
/** 本位币金额(CNY,原币金额×汇率) */ /** 本位币金额(CNY,原币金额×汇率) */
private BigDecimal baseAmount; private BigDecimal baseAmount;
@@ -130,25 +130,65 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
{ {
receipt.setCurrency(receipt.getCurrency().trim().toUpperCase()); receipt.setCurrency(receipt.getCurrency().trim().toUpperCase());
} }
// 汇率与本位币金额:CNY 固定 1:1;外币默认取汇率表即期汇率,也允许页面手工修改后传入 // 汇率与本位币金额:CNY 固定 1:1;外币以收款日即期汇率表为基准,允许页面手工修改。
// 汇率来源由后端权威判定(不信任前端标记):与汇率表一致=RATE_TABLE,不一致/表未配置=MANUAL。
BigDecimal rate = receipt.getExchangeRate(); BigDecimal rate = receipt.getExchangeRate();
if (BASE_CURRENCY.equals(receipt.getCurrency())) if (BASE_CURRENCY.equals(receipt.getCurrency()))
{ {
rate = BigDecimal.ONE; rate = BigDecimal.ONE;
receipt.setRateSource(CustomerReceipt.RATE_SOURCE_SYSTEM);
receipt.setRateSourceDesc("系统默认(本位币1:1)");
} }
else else
{ {
Date bizDate = receipt.getReceivedAt() != null ? receipt.getReceivedAt() : new Date();
BmsExchangeRate tableRate = null;
try
{
tableRate = exchangeRateService.resolveRate(
receipt.getCurrency(), BASE_CURRENCY, BmsExchangeRate.RATE_TYPE_SPOT, bizDate);
}
catch (ServiceException ex)
{
// 汇率表未配置:若页面手工录了有效汇率则放行,否则继续走下面的非空校验拦截
tableRate = null;
}
if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0) if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0)
{ {
Date bizDate = receipt.getReceivedAt() != null ? receipt.getReceivedAt() : new Date(); if (tableRate == null)
BmsExchangeRate rateConfig = exchangeRateService.resolveRate( {
receipt.getCurrency(), BASE_CURRENCY, BmsExchangeRate.RATE_TYPE_SPOT, bizDate); throw new ServiceException("汇率表未维护【" + receipt.getCurrency()
rate = rateConfig.getExchangeRate(); + " → CNY】收款日生效的即期汇率,请手工录入汇率");
}
rate = tableRate.getExchangeRate();
} }
if (rate.compareTo(BigDecimal.ZERO) <= 0) if (rate.compareTo(BigDecimal.ZERO) <= 0)
{ {
throw new ServiceException("汇率必须大于0,请检查录入的汇率"); throw new ServiceException("汇率必须大于0,请检查录入的汇率");
} }
rate = rate.setScale(8, RoundingMode.HALF_UP);
// 与汇率表汇率一致(8位精度)即认定为汇率表来源,否则为手工修改
if (tableRate != null
&& rate.compareTo(tableRate.getExchangeRate().setScale(8, RoundingMode.HALF_UP)) == 0)
{
receipt.setRateSource(CustomerReceipt.RATE_SOURCE_RATE_TABLE);
receipt.setRateSourceDesc(buildRateTableSourceDesc(tableRate));
}
else
{
receipt.setRateSource(CustomerReceipt.RATE_SOURCE_MANUAL);
if (tableRate != null)
{
receipt.setRateSourceDesc("手工修改(汇率表即期汇率 "
+ tableRate.getExchangeRate().setScale(8, RoundingMode.HALF_UP).toPlainString() + ")");
}
else
{
receipt.setRateSourceDesc("手工录入(汇率表未维护该币种收款日汇率)");
}
}
} }
receipt.setExchangeRate(rate.setScale(8, RoundingMode.HALF_UP)); receipt.setExchangeRate(rate.setScale(8, RoundingMode.HALF_UP));
BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP); BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
@@ -171,6 +211,39 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
return 1; return 1;
} }
/**
* 构造汇率表来源说明:汇率来源(中国银行/央行等)+ 命中路径(正向/反向取倒数/经CNY交叉)。
* resolveRate 返回对象的 remark 带 __RATE_PATH__ 前缀标记命中路径。
*/
private String buildRateTableSourceDesc(BmsExchangeRate tableRate)
{
String source = StringUtils.hasText(tableRate.getSource()) ? tableRate.getSource() : "汇率表";
String path = "DIRECT";
String remark = tableRate.getRemark();
if (StringUtils.hasText(remark) && remark.startsWith("__RATE_PATH__"))
{
path = remark.substring("__RATE_PATH__".length());
}
String pathText;
switch (path)
{
case "REVERSE":
pathText = "反向配置取倒数";
break;
case "CROSS":
pathText = "经CNY交叉换算";
break;
case "SAME":
pathText = "同币种";
break;
case "DIRECT":
default:
pathText = "正向配置";
break;
}
return "汇率表·" + source + "(" + pathText + ")";
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int confirmReceipt(Long id) public int confirmReceipt(Long id)
@@ -78,7 +78,7 @@ public class BmsExchangeRateServiceImpl implements IBmsExchangeRateService
String from = normalizeCurrency(fromCurrency); String from = normalizeCurrency(fromCurrency);
String to = normalizeCurrency(toCurrency); String to = normalizeCurrency(toCurrency);
String type = StringUtils.isNotEmpty(rateType) ? rateType : BmsExchangeRate.RATE_TYPE_SPOT; String type = StringUtils.isNotEmpty(rateType) ? rateType : BmsExchangeRate.RATE_TYPE_SPOT;
Date date = bizDate != null ? bizDate : DateUtils.getNowDate(); Date date = truncateToDay(bizDate != null ? bizDate : DateUtils.getNowDate());
// 同币种 // 同币种
if (from.equals(to)) if (from.equals(to))
@@ -147,6 +147,18 @@ public class BmsExchangeRateServiceImpl implements IBmsExchangeRateService
/* ============================ 私有方法 ============================ */ /* ============================ 私有方法 ============================ */
/**
* 截断到日历日 00:00:00。
* 汇率表 effective_date/expire_date 为 date 类型(精确到天),业务时间(如收款时间带时分秒)
* 直接参与比较时,"失效日当天"会因 expire_date 被提升为当天 00:00:00 而提前失效,
* 故所有业务日统一按日历日落入 [生效日, 失效日] 闭区间。
*/
private Date truncateToDay(Date date)
{
return Date.from(date.toInstant().atZone(java.time.ZoneId.systemDefault())
.toLocalDate().atStartOfDay(java.time.ZoneId.systemDefault()).toInstant());
}
/** /**
* 查找单跳汇率(先正向,再反向取倒数),查不到返回 null * 查找单跳汇率(先正向,再反向取倒数),查不到返回 null
*/ */
@@ -233,6 +233,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="unallocatedAmount" column="unallocated_amount" /> <result property="unallocatedAmount" column="unallocated_amount" />
<result property="currency" column="currency" /> <result property="currency" column="currency" />
<result property="exchangeRate" column="exchange_rate" /> <result property="exchangeRate" column="exchange_rate" />
<result property="rateSource" column="rate_source" />
<result property="rateSourceDesc" column="rate_source_desc" />
<result property="baseAmount" column="base_amount" /> <result property="baseAmount" column="base_amount" />
<result property="payMethod" column="pay_method" /> <result property="payMethod" column="pay_method" />
<result property="bankTransactionNo" column="bank_transaction_no" /> <result property="bankTransactionNo" column="bank_transaction_no" />
@@ -253,7 +255,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectCustomerReceiptVo"> <sql id="selectCustomerReceiptVo">
select r.id, r.receipt_no, r.customer_id, r.account_id, r.receipt_type, r.amount, r.allocated_amount, select r.id, r.receipt_no, r.customer_id, r.account_id, r.receipt_type, r.amount, r.allocated_amount,
r.unallocated_amount, r.currency, r.exchange_rate, r.base_amount, r.pay_method, r.bank_transaction_no, r.unallocated_amount, r.currency, r.exchange_rate, r.rate_source, r.rate_source_desc,
r.base_amount, r.pay_method, r.bank_transaction_no,
r.received_at, r.status, r.received_at, r.status,
r.confirmed_at, r.confirmed_by, r.invoice_status, r.version, r.create_by, r.create_time, r.confirmed_at, r.confirmed_by, r.invoice_status, r.version, r.create_by, r.create_time,
r.update_by, r.update_time, r.remark, r.del_flag, r.update_by, r.update_time, r.remark, r.del_flag,
@@ -305,6 +308,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="unallocatedAmount != null">unallocated_amount,</if> <if test="unallocatedAmount != null">unallocated_amount,</if>
<if test="currency != null">currency,</if> <if test="currency != null">currency,</if>
<if test="exchangeRate != null">exchange_rate,</if> <if test="exchangeRate != null">exchange_rate,</if>
<if test="rateSource != null">rate_source,</if>
<if test="rateSourceDesc != null">rate_source_desc,</if>
<if test="baseAmount != null">base_amount,</if> <if test="baseAmount != null">base_amount,</if>
<if test="payMethod != null">pay_method,</if> <if test="payMethod != null">pay_method,</if>
<if test="bankTransactionNo != null">bank_transaction_no,</if> <if test="bankTransactionNo != null">bank_transaction_no,</if>
@@ -328,6 +333,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="unallocatedAmount != null">#{unallocatedAmount},</if> <if test="unallocatedAmount != null">#{unallocatedAmount},</if>
<if test="currency != null">#{currency},</if> <if test="currency != null">#{currency},</if>
<if test="exchangeRate != null">#{exchangeRate},</if> <if test="exchangeRate != null">#{exchangeRate},</if>
<if test="rateSource != null">#{rateSource},</if>
<if test="rateSourceDesc != null">#{rateSourceDesc},</if>
<if test="baseAmount != null">#{baseAmount},</if> <if test="baseAmount != null">#{baseAmount},</if>
<if test="payMethod != null">#{payMethod},</if> <if test="payMethod != null">#{payMethod},</if>
<if test="bankTransactionNo != null">#{bankTransactionNo},</if> <if test="bankTransactionNo != null">#{bankTransactionNo},</if>
@@ -0,0 +1,15 @@
-- ----------------------------
-- 客户收款单增加汇率来源留痕:
-- rate_source 汇率来源类型(SYSTEM系统默认本币1:1 / RATE_TABLE汇率表自动带出 / MANUAL手工修改)
-- rate_source_desc 来源说明(汇率表来源+命中路径,或手工录入说明)
-- 登记收款时由后端权威判定:CNY=SYSTEM;外币汇率与收款日即期汇率表一致=RATE_TABLE,否则=MANUAL。
-- ----------------------------
ALTER TABLE `customer_receipt`
ADD COLUMN `rate_source` VARCHAR(16) NOT NULL DEFAULT 'SYSTEM'
COMMENT '汇率来源:RATE_TABLE汇率表/MANUAL手工修改/SYSTEM系统默认(本币1:1)' AFTER `exchange_rate`,
ADD COLUMN `rate_source_desc` VARCHAR(100) DEFAULT NULL
COMMENT '汇率来源说明(汇率表来源及命中路径/手工录入等)' AFTER `rate_source`;
-- 存量数据订正:本币收款=系统默认;历史外币收款按汇率表取值口径登记,统一标记为汇率表
UPDATE `customer_receipt` SET `rate_source` = 'SYSTEM' WHERE `currency` = 'CNY' AND `rate_source` = 'SYSTEM';
UPDATE `customer_receipt` SET `rate_source` = 'RATE_TABLE' WHERE `currency` <> 'CNY';
+39 -1
View File
@@ -3,6 +3,7 @@
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" :default-params="defaultSearchParams" row-key="arBillId" :selection="false"> <basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" :default-params="defaultSearchParams" row-key="arBillId" :selection="false">
<template #toolbar> <template #toolbar>
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col> <el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col>
<el-col v-if="customerScoped" :span="1.5"><el-button type="info" plain icon="Close" @click="clearCustomerFilter">清除客户条件</el-button></el-col>
</template> </template>
<template #search-customerId="{ model }"> <template #search-customerId="{ model }">
<customer-select v-model="model.customerId" placeholder="请选择客户" :page-size="1000" /> <customer-select v-model="model.customerId" placeholder="请选择客户" :page-size="1000" />
@@ -16,6 +17,7 @@
<el-button v-if="row.status === AR_BILL_STATUS.DRAFT" link type="success" v-hasPermi="['ar:bill:confirm']" @click="confirmBill(row)">确认</el-button> <el-button v-if="row.status === AR_BILL_STATUS.DRAFT" link type="success" v-hasPermi="['ar:bill:confirm']" @click="confirmBill(row)">确认</el-button>
<el-button v-if="row.status === AR_BILL_STATUS.DRAFT || row.status === AR_BILL_STATUS.CONFIRMED" link type="warning" v-hasPermi="['ar:bill:void']" @click="voidBill(row)">作废</el-button> <el-button v-if="row.status === AR_BILL_STATUS.DRAFT || row.status === AR_BILL_STATUS.CONFIRMED" link type="warning" v-hasPermi="['ar:bill:void']" @click="voidBill(row)">作废</el-button>
<el-button v-if="row.status !== AR_BILL_STATUS.DRAFT && row.status !== AR_BILL_STATUS.VOID" link type="primary" v-hasPermi="['ar:bill:export']" @click="exportBill(row)">导出</el-button> <el-button v-if="row.status !== AR_BILL_STATUS.DRAFT && row.status !== AR_BILL_STATUS.VOID" link type="primary" v-hasPermi="['ar:bill:export']" @click="exportBill(row)">导出</el-button>
<el-button v-if="canGoWriteoff(row)" link type="primary" v-hasPermi="['customer:bill:writeoff']" @click="goWriteoff(row)">去核销</el-button>
</template> </template>
</basic-table> </basic-table>
@@ -33,6 +35,7 @@ import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const route = useRoute() const route = useRoute()
const router = useRouter()
const tableRef = ref() const tableRef = ref()
// 从客户余额页“查看账单”跳入时,按当前客户预筛选(重置后仍保留该条件) // 从客户余额页“查看账单”跳入时,按当前客户预筛选(重置后仍保留该条件)
const defaultSearchParams = computed(() => ({ const defaultSearchParams = computed(() => ({
@@ -43,12 +46,13 @@ const detailLoading = ref(false)
const detail = ref(null) const detail = ref(null)
const generateOpen = ref(false) const generateOpen = ref(false)
const statusOptions = AR_BILL_STATUS_OPTIONS const statusOptions = AR_BILL_STATUS_OPTIONS
const customerScoped = computed(() => Boolean(route.query.customerId))
const columns = [ const columns = [
{ label: '账单号', prop: 'billNo', minWidth: 180, showOverflowTooltip: true }, { label: '账单名称', prop: 'billName', minWidth: 180, showOverflowTooltip: true }, { label: '客户名称', prop: 'customerName', minWidth: 130, showOverflowTooltip: true }, { label: '客户编码', prop: 'customerCode', minWidth: 120, showOverflowTooltip: true }, { label: '账单号', prop: 'billNo', minWidth: 180, showOverflowTooltip: true }, { label: '账单名称', prop: 'billName', minWidth: 180, showOverflowTooltip: true }, { label: '客户名称', prop: 'customerName', minWidth: 130, showOverflowTooltip: true }, { label: '客户编码', prop: 'customerCode', minWidth: 120, showOverflowTooltip: true },
{ label: '账单截止日', prop: 'billPeriodEnd', minWidth: 120, formatter: row => `截至 ${date(row.billPeriodEnd)}` }, { label: '订单数', prop: 'orderCount', width: 80 }, { label: '账单截止日', prop: 'billPeriodEnd', minWidth: 120, formatter: row => `截至 ${date(row.billPeriodEnd)}` }, { label: '订单数', prop: 'orderCount', width: 80 },
{ label: '应收总额', prop: 'totalAmount', width: 120, align: 'right', slot: 'totalAmount' }, { label: '已核销', prop: 'writtenOffAmount', width: 110, align: 'right', slot: 'writtenOffAmount' }, { label: '应收总额', prop: 'totalAmount', width: 120, align: 'right', slot: 'totalAmount' }, { label: '已核销', prop: 'writtenOffAmount', width: 110, align: 'right', slot: 'writtenOffAmount' },
{ label: '未核销', prop: 'unwrittenAmount', width: 110, align: 'right', slot: 'unwrittenAmount' }, { label: '状态', prop: 'status', width: 110, slot: 'status' }, { label: '未核销', prop: 'unwrittenAmount', width: 110, align: 'right', slot: 'unwrittenAmount' }, { label: '状态', prop: 'status', width: 110, slot: 'status' },
{ label: '账单日期', prop: 'billDate', width: 120, formatter: row => date(row.billDate) }, { label: '操作', prop: 'action', width: 260, fixed: 'right', slot: 'action', showInToolbar: false } { label: '账单日期', prop: 'billDate', width: 120, formatter: row => date(row.billDate) }, { label: '操作', prop: 'action', width: 330, fixed: 'right', slot: 'action', showInToolbar: false }
] ]
const searchColumns = [ const searchColumns = [
{ label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户名称', prop: 'customerId', component: 'input', placeholder: '请选择客户', width: '220px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' }, { label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户名称', prop: 'customerId', component: 'input', placeholder: '请选择客户', width: '220px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' },
@@ -62,6 +66,40 @@ function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}
function voidBill(row) { proxy.$prompt(`请输入账单「${row.billNo}」的作废原因`, '作废账单', { inputPattern: /\S+/, inputErrorMessage: '作废原因不能为空' }).then(({ value }) => voidArBill(row.arBillId, { remark: value })).then(() => { proxy.$modal.msgSuccess('账单已作废'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) } function voidBill(row) { proxy.$prompt(`请输入账单「${row.billNo}」的作废原因`, '作废账单', { inputPattern: /\S+/, inputErrorMessage: '作废原因不能为空' }).then(({ value }) => voidArBill(row.arBillId, { remark: value })).then(() => { proxy.$modal.msgSuccess('账单已作废'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
function exportBill(row) { if (!row?.arBillId) return; exportArBill(row.arBillId).then(res => { proxy.$modal.msgSuccess('导出成功'); tableRef.value.refresh(); if (detailOpen.value) viewBill(row); if (res.data?.exportId) downloadExport(res.data) }) } function exportBill(row) { if (!row?.arBillId) return; exportArBill(row.arBillId).then(res => { proxy.$modal.msgSuccess('导出成功'); tableRef.value.refresh(); if (detailOpen.value) viewBill(row); if (res.data?.exportId) downloadExport(res.data) }) }
function downloadExport(row) { downloadArBillExport(row.exportId).then(blob => saveAs(blob, row.fileName || `账单_${row.exportId}.xlsx`)) } function downloadExport(row) { downloadArBillExport(row.exportId).then(blob => saveAs(blob, row.fileName || `账单_${row.exportId}.xlsx`)) }
function clearCustomerFilter() {
const query = { ...route.query }
delete query.customerId
router.replace({ path: route.path, query })
}
// 从客户余额页重复进入不同客户时,路由组件会被复用,需同步更新表格内部查询条件
watch(() => route.query.customerId, (newId, oldId) => {
if (String(newId || '') === String(oldId || '')) return
tableRef.value?.setQueryParams({
customerId: newId ? Number(newId) : undefined,
pageNum: 1
})
tableRef.value?.handleQuery()
})
function canGoWriteoff(row) {
if (!row?.customerId) return false
const canWriteoffStatus = row.status === AR_BILL_STATUS.CONFIRMED
|| row.status === AR_BILL_STATUS.PARTIAL_WRITTEN_OFF
return canWriteoffStatus && Number(row.unwrittenAmount) > 0
}
function goWriteoff(row) {
if (!canGoWriteoff(row)) return
router.push({
path: '/customersupplier/customer-money/index',
query: {
customerId: row.customerId,
customerName: row.customerName || '',
customerCode: row.customerCode || '',
tab: 'billWriteoff'
}
})
}
function date(value) { return value ? String(value).slice(0, 10) : '-' } function date(value) { return value ? String(value).slice(0, 10) : '-' }
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) } function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
function statusText(value) { return statusOptions.find(item => item.value === value)?.label || value || '-' } function statusText(value) { return statusOptions.find(item => item.value === value)?.label || value || '-' }
@@ -43,7 +43,7 @@
<el-col :xs="12" :sm="8" :md="4"> <el-col :xs="12" :sm="8" :md="4">
<el-card shadow="hover" class="balance-card"> <el-card shadow="hover" class="balance-card">
<div class="balance-label"> <div class="balance-label">
总额度 信用额度
<el-button link type="primary" size="small" @click="openAdjustDialog('CREDIT')" <el-button link type="primary" size="small" @click="openAdjustDialog('CREDIT')"
v-hasPermi="['customer:credit:adjust']">调整</el-button> v-hasPermi="['customer:credit:adjust']">调整</el-button>
</div> </div>
@@ -127,15 +127,27 @@
<span>{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span> <span>{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="汇率" align="center" width="110"> <el-table-column label="汇率" align="center" width="100">
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template> <template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="汇率来源" align="center" width="110">
<template #default="scope">
<el-tooltip v-if="scope.row.rateSourceDesc" :content="scope.row.rateSourceDesc" placement="top">
<el-tag :type="receiptRateSourceTag(scope.row.rateSource)" size="small">
{{ receiptRateSourceLabel(scope.row.rateSource) }}
</el-tag>
</el-tooltip>
<el-tag v-else :type="receiptRateSourceTag(scope.row.rateSource)" size="small">
{{ receiptRateSourceLabel(scope.row.rateSource) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="本位币金额(CNY)" align="center" width="140"> <el-table-column label="本位币金额(CNY)" align="center" width="140">
<template #default="scope"> <template #default="scope">
<span class="money-in">{{ formatMoney(scope.row.baseAmount) }}</span> <span class="money-in">{{ formatMoney(scope.row.baseAmount) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="待核销(CNY)" align="center" prop="unallocatedAmount" width="110" /> <!-- <el-table-column label="待核销(CNY)" align="center" prop="unallocatedAmount" width="110" />-->
<el-table-column label="收款方式" align="center" prop="payMethod" width="110"> <el-table-column label="收款方式" align="center" prop="payMethod" width="110">
<template #default="scope">{{ formatOption(payMethodOptions, scope.row.payMethod) }}</template> <template #default="scope">{{ formatOption(payMethodOptions, scope.row.payMethod) }}</template>
</el-table-column> </el-table-column>
@@ -596,7 +608,14 @@
style="width: 100%;" style="width: 100%;"
@input="handleRateInput" @input="handleRateInput"
/> />
<div class="rate-tip">1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY<span v-if="receiptDialog.form.currency !== 'CNY'">,默认取汇率表,可手工修改</span></div> <div class="rate-tip">
1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY
<el-tag v-if="receiptDialog.form.currency === 'CNY'" size="small" type="info" effect="plain" style="margin-left: 6px;">系统默认</el-tag>
<el-tag v-else-if="rateEdited" size="small" type="warning" effect="dark" style="margin-left: 6px;">手工修改</el-tag>
<el-tag v-else-if="receiptDialog.form.exchangeRate" size="small" type="success" effect="plain" style="margin-left: 6px;">汇率表{{ rateSourceInfo.source ? '·' + rateSourceInfo.source : '' }}</el-tag>
<span v-else style="color: var(--el-color-warning);"> 汇率表未配置,请手工录入</span>
<span v-if="receiptDialog.form.currency !== 'CNY'">,默认取汇率表,可手工修改</span>
</div>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@@ -678,6 +697,14 @@
<span class="money-in">{{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }}</span> <span class="money-in">{{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="对CNY汇率">{{ formatRate(viewDialog.data.exchangeRate) }}</el-descriptions-item> <el-descriptions-item label="对CNY汇率">{{ formatRate(viewDialog.data.exchangeRate) }}</el-descriptions-item>
<el-descriptions-item label="汇率来源">
<el-tag :type="receiptRateSourceTag(viewDialog.data.rateSource)" size="small">
{{ receiptRateSourceLabel(viewDialog.data.rateSource) }}
</el-tag>
<span v-if="viewDialog.data.rateSourceDesc" style="margin-left: 8px; color: var(--el-text-color-secondary);">
{{ viewDialog.data.rateSourceDesc }}
</span>
</el-descriptions-item>
<el-descriptions-item label="本位币金额"> <el-descriptions-item label="本位币金额">
<span class="money-in">CNY {{ formatMoney(viewDialog.data.baseAmount) }}</span> <span class="money-in">CNY {{ formatMoney(viewDialog.data.baseAmount) }}</span>
</el-descriptions-item> </el-descriptions-item>
@@ -1006,6 +1033,14 @@ function creditChangeTag(v) {
function isCreditUsageFlow(changeType) { function isCreditUsageFlow(changeType) {
return changeType === 'OCCUPY' || changeType === 'RELEASE' return changeType === 'OCCUPY' || changeType === 'RELEASE'
} }
/** 收款汇率来源文案:RATE_TABLE汇率表 / MANUAL手工修改 / SYSTEM系统默认(本币) */
function receiptRateSourceLabel(v) {
return { RATE_TABLE: '汇率表', MANUAL: '手工修改', SYSTEM: '系统默认' }[v] || '系统默认'
}
/** 收款汇率来源标签颜色 */
function receiptRateSourceTag(v) {
return { RATE_TABLE: 'success', MANUAL: 'warning', SYSTEM: 'info' }[v] || 'info'
}
function formatMoney(val) { function formatMoney(val) {
const num = Number(val) const num = Number(val)
return isNaN(num) ? '0.00' : num.toFixed(2) return isNaN(num) ? '0.00' : num.toFixed(2)
@@ -1277,6 +1312,7 @@ const receiptRules = {
function resetReceiptForm() { function resetReceiptForm() {
rateEdited.value = false rateEdited.value = false
rateSourceInfo.value = { source: '', ratePath: '' }
receiptDialog.form = { receiptDialog.form = {
customerId: route.query.customerId || null, customerId: route.query.customerId || null,
receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'), receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
@@ -1299,12 +1335,15 @@ function handleAddReceipt() {
/** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */ /** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */
const rateEdited = ref(false) const rateEdited = ref(false)
/** 汇率表返回的来源信息(source: 中国银行等;ratePath: DIRECT/REVERSE/CROSS),仅用于表单实时展示 */
const rateSourceInfo = ref({ source: '', ratePath: '' })
/** 按收款日期从汇率表取该币种对CNY即期汇率 */ /** 按收款日期从汇率表取该币种对CNY即期汇率 */
function fetchDefaultRate() { function fetchDefaultRate() {
const cur = receiptDialog.form.currency const cur = receiptDialog.form.currency
if (!cur || cur === 'CNY') { if (!cur || cur === 'CNY') {
receiptDialog.form.exchangeRate = 1 receiptDialog.form.exchangeRate = 1
rateSourceInfo.value = { source: '', ratePath: 'SAME' }
return return
} }
const bizDate = receiptDialog.form.receivedAt const bizDate = receiptDialog.form.receivedAt
@@ -1312,11 +1351,15 @@ function fetchDefaultRate() {
: proxy.parseTime(new Date(), '{y}-{m}-{d}') : proxy.parseTime(new Date(), '{y}-{m}-{d}')
convertToBase({ amount: 1, currency: cur, rateType: '0', bizDate }) convertToBase({ amount: 1, currency: cur, rateType: '0', bizDate })
.then(res => { .then(res => {
if (!rateEdited.value) receiptDialog.form.exchangeRate = Number(res.data.rate) if (!rateEdited.value) {
receiptDialog.form.exchangeRate = Number(res.data.rate)
rateSourceInfo.value = { source: res.data.source || '', ratePath: res.data.ratePath || 'DIRECT' }
}
}) })
.catch(() => { .catch(() => {
if (!rateEdited.value) { if (!rateEdited.value) {
receiptDialog.form.exchangeRate = undefined receiptDialog.form.exchangeRate = undefined
rateSourceInfo.value = { source: '', ratePath: '' }
proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`) proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`)
} }
}) })
@@ -1325,6 +1368,7 @@ function fetchDefaultRate() {
/** 切换币种:重置手工标记并重新取默认汇率 */ /** 切换币种:重置手工标记并重新取默认汇率 */
function handleCurrencyChange() { function handleCurrencyChange() {
rateEdited.value = false rateEdited.value = false
rateSourceInfo.value = { source: '', ratePath: '' }
fetchDefaultRate() fetchDefaultRate()
} }
@@ -1803,7 +1847,7 @@ function initPage() {
// 重置页面状态,避免上一个客户的数据残留 // 重置页面状态,避免上一个客户的数据残留
customer.value = {} customer.value = {}
accountList.value = [] accountList.value = []
activeTab.value = 'receipt' activeTab.value = route.query.tab === 'billWriteoff' ? 'billWriteoff' : 'receipt'
receiptDialog.visible = false receiptDialog.visible = false
viewDialog.visible = false viewDialog.visible = false
viewDialog.data = null viewDialog.data = null
@@ -214,10 +214,11 @@ const open = ref(false)
const calcResult = ref(null) const calcResult = ref(null)
const rateTypeOptions = [ const rateTypeOptions = [
{ value: '0', label: '即期汇率' }, { value: '0', label: '即期汇率' }
{ value: '1', label: '月初汇率' }, // ,
{ value: '2', label: '月末汇率' }, // { value: '1', label: '月初汇率' },
{ value: '3', label: '固定汇率' } // { value: '2', label: '月末汇率' },
// { value: '3', label: '固定汇率' }
] ]
/** 常用币种兜底 + 从国家资料(币种)去重加载 */ /** 常用币种兜底 + 从国家资料(币种)去重加载 */