diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java index 1d126239..6f973139 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/domain/CustomerReceipt.java @@ -35,6 +35,13 @@ public class CustomerReceipt extends BaseEntity /** 收款类型:混合 */ 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; @@ -65,6 +72,12 @@ public class CustomerReceipt extends BaseEntity /** 对CNY汇率(1原币=?CNY,CNY为1;默认取汇率表,可手工修改) */ private BigDecimal exchangeRate; + /** 汇率来源:SYSTEM系统默认(本币1:1)/RATE_TABLE汇率表/MANUAL手工修改 */ + private String rateSource; + + /** 汇率来源说明(汇率表来源及命中路径,或手工录入说明) */ + private String rateSourceDesc; + /** 本位币金额(CNY,原币金额×汇率) */ private BigDecimal baseAmount; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java index 92739fd2..e440cff5 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/service/impl/CustomerMoneyServiceImpl.java @@ -130,25 +130,65 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService { receipt.setCurrency(receipt.getCurrency().trim().toUpperCase()); } - // 汇率与本位币金额:CNY 固定 1:1;外币默认取汇率表即期汇率,也允许页面手工修改后传入 + // 汇率与本位币金额:CNY 固定 1:1;外币以收款日即期汇率表为基准,允许页面手工修改。 + // 汇率来源由后端权威判定(不信任前端标记):与汇率表一致=RATE_TABLE,不一致/表未配置=MANUAL。 BigDecimal rate = receipt.getExchangeRate(); if (BASE_CURRENCY.equals(receipt.getCurrency())) { rate = BigDecimal.ONE; + receipt.setRateSource(CustomerReceipt.RATE_SOURCE_SYSTEM); + receipt.setRateSourceDesc("系统默认(本位币1:1)"); } 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) { - Date bizDate = receipt.getReceivedAt() != null ? receipt.getReceivedAt() : new Date(); - BmsExchangeRate rateConfig = exchangeRateService.resolveRate( - receipt.getCurrency(), BASE_CURRENCY, BmsExchangeRate.RATE_TYPE_SPOT, bizDate); - rate = rateConfig.getExchangeRate(); + if (tableRate == null) + { + throw new ServiceException("汇率表未维护【" + receipt.getCurrency() + + " → CNY】收款日生效的即期汇率,请手工录入汇率"); + } + rate = tableRate.getExchangeRate(); } if (rate.compareTo(BigDecimal.ZERO) <= 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)); BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP); @@ -171,6 +211,39 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService 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 @Transactional(rollbackFor = Exception.class) public int confirmReceipt(Long id) diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java index 1fd4ae8a..f1ec4516 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BmsExchangeRateServiceImpl.java @@ -78,7 +78,7 @@ public class BmsExchangeRateServiceImpl implements IBmsExchangeRateService String from = normalizeCurrency(fromCurrency); String to = normalizeCurrency(toCurrency); 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)) @@ -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 */ diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml index 7eef3dc3..8021010a 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/customer/CustomerMoneyMapper.xml @@ -233,6 +233,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -253,7 +255,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 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.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, @@ -305,6 +308,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" unallocated_amount, currency, exchange_rate, + rate_source, + rate_source_desc, base_amount, pay_method, bank_transaction_no, @@ -328,6 +333,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{unallocatedAmount}, #{currency}, #{exchangeRate}, + #{rateSource}, + #{rateSourceDesc}, #{baseAmount}, #{payMethod}, #{bankTransactionNo}, diff --git a/bms-back/sql/customer+supplier/customer_receipt_rate_source.sql b/bms-back/sql/customer+supplier/customer_receipt_rate_source.sql new file mode 100644 index 00000000..b2ff3ef6 --- /dev/null +++ b/bms-back/sql/customer+supplier/customer_receipt_rate_source.sql @@ -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'; diff --git a/bms-back/sql/customer+supplier/客户管理相关所有表结构.sql b/bms-back/sql/customer+supplier/客户管理相关所有表结构.sql new file mode 100644 index 00000000..e69de29b diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue index 9847c1af..78e645a2 100644 --- a/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue +++ b/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue @@ -43,7 +43,7 @@
- 总额度 + 信用额度 调整
@@ -127,15 +127,27 @@ {{ scope.row.currency }} {{ formatMoney(scope.row.amount) }} - + + + + - + @@ -596,7 +608,14 @@ style="width: 100%;" @input="handleRateInput" /> -
1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY,默认取汇率表,可手工修改
+
+ 1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY + 系统默认 + 手工修改 + 汇率表{{ rateSourceInfo.source ? '·' + rateSourceInfo.source : '' }} + 汇率表未配置,请手工录入 + ,默认取汇率表,可手工修改 +
@@ -678,6 +697,14 @@ {{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }} {{ formatRate(viewDialog.data.exchangeRate) }} + + + {{ receiptRateSourceLabel(viewDialog.data.rateSource) }} + + + {{ viewDialog.data.rateSourceDesc }} + + CNY {{ formatMoney(viewDialog.data.baseAmount) }} @@ -1006,6 +1033,14 @@ function creditChangeTag(v) { function isCreditUsageFlow(changeType) { 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) { const num = Number(val) return isNaN(num) ? '0.00' : num.toFixed(2) @@ -1277,6 +1312,7 @@ const receiptRules = { function resetReceiptForm() { rateEdited.value = false + rateSourceInfo.value = { source: '', ratePath: '' } receiptDialog.form = { customerId: route.query.customerId || null, receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'), @@ -1299,12 +1335,15 @@ function handleAddReceipt() { /** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */ const rateEdited = ref(false) +/** 汇率表返回的来源信息(source: 中国银行等;ratePath: DIRECT/REVERSE/CROSS),仅用于表单实时展示 */ +const rateSourceInfo = ref({ source: '', ratePath: '' }) /** 按收款日期从汇率表取该币种对CNY即期汇率 */ function fetchDefaultRate() { const cur = receiptDialog.form.currency if (!cur || cur === 'CNY') { receiptDialog.form.exchangeRate = 1 + rateSourceInfo.value = { source: '', ratePath: 'SAME' } return } const bizDate = receiptDialog.form.receivedAt @@ -1312,11 +1351,15 @@ function fetchDefaultRate() { : proxy.parseTime(new Date(), '{y}-{m}-{d}') convertToBase({ amount: 1, currency: cur, rateType: '0', bizDate }) .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(() => { if (!rateEdited.value) { receiptDialog.form.exchangeRate = undefined + rateSourceInfo.value = { source: '', ratePath: '' } proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`) } }) @@ -1325,6 +1368,7 @@ function fetchDefaultRate() { /** 切换币种:重置手工标记并重新取默认汇率 */ function handleCurrencyChange() { rateEdited.value = false + rateSourceInfo.value = { source: '', ratePath: '' } fetchDefaultRate() } diff --git a/bms-front/src/views/data/exchangerate/index.vue b/bms-front/src/views/data/exchangerate/index.vue index f9396e62..dcbf56a7 100644 --- a/bms-front/src/views/data/exchangerate/index.vue +++ b/bms-front/src/views/data/exchangerate/index.vue @@ -214,10 +214,11 @@ const open = ref(false) const calcResult = ref(null) const rateTypeOptions = [ - { value: '0', label: '即期汇率' }, - { value: '1', label: '月初汇率' }, - { value: '2', label: '月末汇率' }, - { value: '3', label: '固定汇率' } + { value: '0', label: '即期汇率' } + // , + // { value: '1', label: '月初汇率' }, + // { value: '2', label: '月末汇率' }, + // { value: '3', label: '固定汇率' } ] /** 常用币种兜底 + 从国家资料(币种)去重加载 */