登记收款选择账单直接核销,附件上传支持拖动上传
This commit is contained in:
@@ -37,6 +37,9 @@ public class ArBalanceWriteoff extends BaseEntity
|
|||||||
/** 客户ID */
|
/** 客户ID */
|
||||||
private Long customerId;
|
private Long customerId;
|
||||||
|
|
||||||
|
/** 关联收款单ID(收款审核联动核销时写入,手工余额核销为空) */
|
||||||
|
private Long receiptId;
|
||||||
|
|
||||||
/** 客户账户ID */
|
/** 客户账户ID */
|
||||||
private Long accountId;
|
private Long accountId;
|
||||||
|
|
||||||
|
|||||||
+71
@@ -26,6 +26,10 @@ import com.bms.ar.mapper.ArBillMapper;
|
|||||||
import com.bms.ar.service.IArBalanceWriteoffService;
|
import com.bms.ar.service.IArBalanceWriteoffService;
|
||||||
import com.bms.ar.service.IArBillLogService;
|
import com.bms.ar.service.IArBillLogService;
|
||||||
import com.bms.common.exception.ServiceException;
|
import com.bms.common.exception.ServiceException;
|
||||||
|
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
||||||
|
import com.bms.customer.customermoney.domain.ReceiptWriteoffPlan;
|
||||||
|
import com.bms.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||||
|
import com.bms.customer.customermoney.mapper.ReceiptWriteoffPlanMapper;
|
||||||
import com.bms.common.utils.SecurityUtils;
|
import com.bms.common.utils.SecurityUtils;
|
||||||
import com.bms.common.utils.uuid.IdUtils;
|
import com.bms.common.utils.uuid.IdUtils;
|
||||||
|
|
||||||
@@ -49,6 +53,12 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IArBillLogService logService;
|
private IArBillLogService logService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ReceiptWriteoffPlanMapper receiptWriteoffPlanMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CustomerMoneyMapper customerMoneyMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按客户查询允许余额核销的未结清账单。
|
* 按客户查询允许余额核销的未结清账单。
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +127,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
|||||||
writeoff.setArBillId(bill.getArBillId());
|
writeoff.setArBillId(bill.getArBillId());
|
||||||
writeoff.setBillNo(bill.getBillNo());
|
writeoff.setBillNo(bill.getBillNo());
|
||||||
writeoff.setCustomerId(bill.getCustomerId());
|
writeoff.setCustomerId(bill.getCustomerId());
|
||||||
|
writeoff.setReceiptId(cmd.getReceiptId());
|
||||||
writeoff.setCurrency(currency);
|
writeoff.setCurrency(currency);
|
||||||
writeoff.setWriteoffAmount(amount);
|
writeoff.setWriteoffAmount(amount);
|
||||||
writeoff.setWriteoffDate(writeoffDate);
|
writeoff.setWriteoffDate(writeoffDate);
|
||||||
@@ -191,6 +202,66 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
|||||||
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||||
"反核销取消核销:" + cancelRemark,
|
"反核销取消核销:" + cancelRemark,
|
||||||
fromStatus, nextStatus, operator, requestId);
|
fromStatus, nextStatus, operator, requestId);
|
||||||
|
|
||||||
|
// 4. 若该核销单由收款审核联动产生(receipt_id 非空):计划置 REVERSED、
|
||||||
|
// 收款单已分配CNY回退为未分配;全程不回补余额、不动额度(收款审核时已全额入账)。
|
||||||
|
if (writeoff.getReceiptId() != null) {
|
||||||
|
reverseReceiptLink(writeoff, operator, cancelRemark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反核销收款联动核销单时的单据回写(与反核销同一事务):
|
||||||
|
* 计划 EFFECTIVE -> REVERSED;收款单 allocated_amount 减回、unallocated_amount 回补并重算类型。
|
||||||
|
* 不触碰客户余额与授信额度。
|
||||||
|
*/
|
||||||
|
private void reverseReceiptLink(ArBalanceWriteoff writeoff, String operator, String cancelRemark) {
|
||||||
|
ReceiptWriteoffPlan plan = receiptWriteoffPlanMapper
|
||||||
|
.selectEffectiveByWriteoffId(writeoff.getWriteoffId());
|
||||||
|
if (plan == null) {
|
||||||
|
// 无 EFFECTIVE 计划:手工余额核销(无计划)或计划已被处理,不做收款单联动
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (receiptWriteoffPlanMapper.updateReversedByWriteoffId(writeoff.getWriteoffId(), operator) != 1) {
|
||||||
|
throw new ServiceException("收款核销计划状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomerReceipt receipt = customerMoneyMapper.selectReceiptById(writeoff.getReceiptId());
|
||||||
|
if (receipt == null || !CustomerReceipt.STATUS_CONFIRMED.equals(receipt.getStatus())) {
|
||||||
|
throw new ServiceException("关联收款单不存在或不是已确认状态,无法联动反核销");
|
||||||
|
}
|
||||||
|
BigDecimal amount = nvl(writeoff.getWriteoffAmount());
|
||||||
|
BigDecimal base = nvl(receipt.getBaseAmount());
|
||||||
|
BigDecimal newAllocated = nvl(receipt.getAllocatedAmount()).subtract(amount);
|
||||||
|
if (newAllocated.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
newAllocated = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
BigDecimal newUnallocated = base.subtract(newAllocated);
|
||||||
|
if (newUnallocated.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
newUnallocated = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
String receiptType;
|
||||||
|
if (newAllocated.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
receiptType = CustomerReceipt.TYPE_PREPAY;
|
||||||
|
} else if (newAllocated.compareTo(base) >= 0) {
|
||||||
|
receiptType = CustomerReceipt.TYPE_REPAY;
|
||||||
|
} else {
|
||||||
|
receiptType = CustomerReceipt.TYPE_MIXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomerReceipt update = new CustomerReceipt();
|
||||||
|
update.setId(receipt.getId());
|
||||||
|
update.setAllocatedAmount(newAllocated);
|
||||||
|
update.setUnallocatedAmount(newUnallocated);
|
||||||
|
update.setReceiptType(receiptType);
|
||||||
|
update.setUpdateBy(operator);
|
||||||
|
if (customerMoneyMapper.updateReceiptAllocation(update) != 1) {
|
||||||
|
throw new ServiceException("关联收款单状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
recordLog(plan.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||||
|
"收款单联动核销反核销,回退收款单分配金额 " + amount.toPlainString()
|
||||||
|
+ " CNY(" + receipt.getReceiptNo() + ",原因:" + cancelRemark + ")",
|
||||||
|
null, null, operator, IdUtils.fastSimpleUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
-2
@@ -8,10 +8,12 @@ import lombok.Data;
|
|||||||
*
|
*
|
||||||
* <p>典型场景:</p>
|
* <p>典型场景:</p>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>余额核销账单/订单扣款 -> {@link ICustomerAccountApi#deductBalance}(余额减少,默认同步释放已用额度)</li>
|
* <li>订单扣款/账户余额支付 -> {@link ICustomerAccountApi#deductBalance}(余额减少)</li>
|
||||||
* <li>收款审核入账/反核销回补/退款 -> {@link ICustomerAccountApi#refundBalance}(余额增加)</li>
|
* <li>收款审核入账/退款 -> {@link ICustomerAccountApi#refundBalance}(余额增加)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
* <p>账单余额核销/反核销不使用本命令、不改动余额与额度。</p>
|
||||||
|
*
|
||||||
* @author bms
|
* @author bms
|
||||||
* @date 2026-09-17
|
* @date 2026-09-17
|
||||||
*/
|
*/
|
||||||
|
|||||||
+7
-4
@@ -14,12 +14,15 @@ import com.bms.customer.customermoney.vo.CustomerAccountStatusVO;
|
|||||||
* 调用方自身的事务会与本操作共享事务(默认 REQUIRED),任一失败整体回滚。</p>
|
* 调用方自身的事务会与本操作共享事务(默认 REQUIRED),任一失败整体回滚。</p>
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>单据形成应收/计费开单:{@link #occupyCredit} 占用授信(used_credit 增加)</li>
|
* <li>单据形成应收/计费开单:{@link #occupyCredit}(计费口径下为扣减账户余额,允许负余额欠费)</li>
|
||||||
* <li>单据作废/计费核减:{@link #releaseCredit} 释放授信(used_credit 减少)</li>
|
* <li>单据作废/计费核减:{@link #releaseCredit}(计费口径下为回补账户余额)</li>
|
||||||
* <li>用账户余额支付/核销:{@link #deductBalance} 扣余额(可同步释放已用额度)</li>
|
* <li>用账户余额支付:{@link #deductBalance} 扣余额</li>
|
||||||
* <li>收款入账/退款/反核销回补:{@link #refundBalance} 加余额(可同步重新占用)</li>
|
* <li>收款审核入账/退款:{@link #refundBalance} 加余额</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
* <p>注意(核销口径):应收账单的余额核销与反核销(含收款审核联动核销)<b>不调用本接口</b>,
|
||||||
|
* 只更新账单已核销/未核销金额与状态,客户余额与授信额度均不发生变动。</p>
|
||||||
|
*
|
||||||
* @author bms
|
* @author bms
|
||||||
* @date 2026-09-17
|
* @date 2026-09-17
|
||||||
*/
|
*/
|
||||||
|
|||||||
+3
@@ -125,6 +125,9 @@ public class CustomerReceipt extends BaseEntity
|
|||||||
/** 收款附件列表(存 ctr_customer_attachment,attachment_type='3',biz_id 关联本单) */
|
/** 收款附件列表(存 ctr_customer_attachment,attachment_type='3',biz_id 关联本单) */
|
||||||
private List<BmsCustomerAttachment> attachmentList;
|
private List<BmsCustomerAttachment> attachmentList;
|
||||||
|
|
||||||
|
/** 核销账单计划(登记时选择,审核通过后一并核销) */
|
||||||
|
private List<ReceiptWriteoffPlan> writeoffPlans;
|
||||||
|
|
||||||
/** 收款附件数量(列表查询统计) */
|
/** 收款附件数量(列表查询统计) */
|
||||||
private Integer attachmentCount;
|
private Integer attachmentCount;
|
||||||
}
|
}
|
||||||
|
|||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
package com.bms.customer.customermoney.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.bms.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收款单核销账单计划 ctr_receipt_writeoff_plan
|
||||||
|
*
|
||||||
|
* <p>登记收款时指定待核销的应收账单(PENDING);收款审核通过后在同一事务内逐张执行核销,
|
||||||
|
* 回写核销单ID并置 EFFECTIVE;待审核收款单被取消则置 CANCELLED;
|
||||||
|
* 已生效的核销单后续在“余额核销”页被反核销则置 REVERSED(核销/反核销均不改动客户余额与额度)。</p>
|
||||||
|
*
|
||||||
|
* @author bms
|
||||||
|
* @date 2026-10-08
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ReceiptWriteoffPlan extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 状态:待执行(收款单待审核) */
|
||||||
|
public static final String STATUS_PENDING = "PENDING";
|
||||||
|
/** 状态:已核销(收款审核通过并完成账单核销) */
|
||||||
|
public static final String STATUS_EFFECTIVE = "EFFECTIVE";
|
||||||
|
/** 状态:已作废(待审核收款单被取消,计划未执行) */
|
||||||
|
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||||
|
/** 状态:已反核销(审核联动核销后,核销单在余额核销页被反核销) */
|
||||||
|
public static final String STATUS_REVERSED = "REVERSED";
|
||||||
|
|
||||||
|
/** 主键 */
|
||||||
|
private Long planId;
|
||||||
|
|
||||||
|
/** 收款单ID */
|
||||||
|
private Long receiptId;
|
||||||
|
|
||||||
|
/** 应收账单ID */
|
||||||
|
private Long arBillId;
|
||||||
|
|
||||||
|
/** 账单号快照 */
|
||||||
|
private String billNo;
|
||||||
|
|
||||||
|
/** 客户ID */
|
||||||
|
private Long customerId;
|
||||||
|
|
||||||
|
/** 账单币种(当前仅支持CNY) */
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
/** 本次核销金额(本位币CNY) */
|
||||||
|
private BigDecimal writeoffAmount;
|
||||||
|
|
||||||
|
/** 状态:PENDING/EFFECTIVE/CANCELLED/REVERSED */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 审核后生成的余额核销单ID */
|
||||||
|
private Long writeoffId;
|
||||||
|
|
||||||
|
/** 核销日期(执行后展示) */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
private Date writeoffDate;
|
||||||
|
|
||||||
|
/* ===== 关联展示字段(来自 ar_bill) ===== */
|
||||||
|
|
||||||
|
/** 账单名称 */
|
||||||
|
private String billName;
|
||||||
|
|
||||||
|
/** 账单应收总额 */
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
/** 账单已核销金额 */
|
||||||
|
private BigDecimal writtenOffAmount;
|
||||||
|
|
||||||
|
/** 账单未核销金额 */
|
||||||
|
private BigDecimal unwrittenAmount;
|
||||||
|
|
||||||
|
/** 账单状态 */
|
||||||
|
private String billStatus;
|
||||||
|
}
|
||||||
+3
@@ -68,6 +68,9 @@ public interface CustomerMoneyMapper
|
|||||||
/** 取消收款单:PENDING -> CANCELLED */
|
/** 取消收款单:PENDING -> CANCELLED */
|
||||||
int cancelReceipt(@Param("id") Long id, @Param("updateBy") String updateBy);
|
int cancelReceipt(@Param("id") Long id, @Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
/** 反核销联动:回退已确认收款单的已分配金额、回补未分配金额并重算收款类型(不动余额) */
|
||||||
|
int updateReceiptAllocation(CustomerReceipt receipt);
|
||||||
|
|
||||||
/* ================= 余额流水 ================= */
|
/* ================= 余额流水 ================= */
|
||||||
|
|
||||||
/** 查询流水列表 */
|
/** 查询流水列表 */
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.bms.customer.customermoney.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import com.bms.customer.customermoney.domain.ReceiptWriteoffPlan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收款单核销账单计划 Mapper
|
||||||
|
*
|
||||||
|
* @author bms
|
||||||
|
* @date 2026-10-08
|
||||||
|
*/
|
||||||
|
public interface ReceiptWriteoffPlanMapper
|
||||||
|
{
|
||||||
|
/** 新增一条核销计划 */
|
||||||
|
int insertPlan(ReceiptWriteoffPlan plan);
|
||||||
|
|
||||||
|
/** 按收款单查询全部计划(关联账单展示字段,按计划ID升序) */
|
||||||
|
List<ReceiptWriteoffPlan> selectByReceiptId(@Param("receiptId") Long receiptId);
|
||||||
|
|
||||||
|
/** 执行成功回写:PENDING -> EFFECTIVE,记录核销单ID与核销日期 */
|
||||||
|
int updateEffective(@Param("planId") Long planId,
|
||||||
|
@Param("writeoffId") Long writeoffId,
|
||||||
|
@Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
/** 收款单取消:其全部 PENDING 计划置 CANCELLED */
|
||||||
|
int cancelPendingByReceiptId(@Param("receiptId") Long receiptId,
|
||||||
|
@Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
/** 按核销单ID查询 EFFECTIVE 计划(反核销联动用) */
|
||||||
|
ReceiptWriteoffPlan selectEffectiveByWriteoffId(@Param("writeoffId") Long writeoffId);
|
||||||
|
|
||||||
|
/** 反核销联动:EFFECTIVE -> REVERSED */
|
||||||
|
int updateReversedByWriteoffId(@Param("writeoffId") Long writeoffId,
|
||||||
|
@Param("updateBy") String updateBy);
|
||||||
|
}
|
||||||
+184
-4
@@ -5,8 +5,11 @@ import java.math.RoundingMode;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -15,8 +18,13 @@ import org.springframework.util.CollectionUtils;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import com.bms.common.exception.ServiceException;
|
import com.bms.common.exception.ServiceException;
|
||||||
import com.bms.common.utils.SecurityUtils;
|
import com.bms.common.utils.SecurityUtils;
|
||||||
|
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||||
import com.bms.ar.domain.ArBankAccount;
|
import com.bms.ar.domain.ArBankAccount;
|
||||||
|
import com.bms.ar.domain.ArBill;
|
||||||
|
import com.bms.ar.enums.ArBillStatus;
|
||||||
import com.bms.ar.mapper.ArBankAccountMapper;
|
import com.bms.ar.mapper.ArBankAccountMapper;
|
||||||
|
import com.bms.ar.mapper.ArBillMapper;
|
||||||
|
import com.bms.ar.service.IArBalanceWriteoffService;
|
||||||
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||||
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
|
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
|
||||||
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||||
@@ -28,8 +36,10 @@ import com.bms.customer.customermoney.domain.CustomerBalanceFlow;
|
|||||||
import com.bms.customer.customermoney.domain.CustomerCreditApply;
|
import com.bms.customer.customermoney.domain.CustomerCreditApply;
|
||||||
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
import com.bms.customer.customermoney.domain.CustomerCreditFlow;
|
||||||
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
import com.bms.customer.customermoney.domain.CustomerReceipt;
|
||||||
|
import com.bms.customer.customermoney.domain.ReceiptWriteoffPlan;
|
||||||
import com.bms.customer.customermoney.mapper.CustomerCreditApplyMapper;
|
import com.bms.customer.customermoney.mapper.CustomerCreditApplyMapper;
|
||||||
import com.bms.customer.customermoney.mapper.CustomerMoneyMapper;
|
import com.bms.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||||
|
import com.bms.customer.customermoney.mapper.ReceiptWriteoffPlanMapper;
|
||||||
import com.bms.customer.customermoney.service.ICreditQuotaService;
|
import com.bms.customer.customermoney.service.ICreditQuotaService;
|
||||||
import com.bms.customer.customermoney.service.ICustomerMoneyService;
|
import com.bms.customer.customermoney.service.ICustomerMoneyService;
|
||||||
import com.bms.price.service.IPrcOrderBillService;
|
import com.bms.price.service.IPrcOrderBillService;
|
||||||
@@ -79,6 +89,18 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ArBankAccountMapper arBankAccountMapper;
|
private ArBankAccountMapper arBankAccountMapper;
|
||||||
|
|
||||||
|
/** 应收账单:登记收款选择核销账单时校验账单状态/未核销额 */
|
||||||
|
@Autowired
|
||||||
|
private ArBillMapper arBillMapper;
|
||||||
|
|
||||||
|
/** 收款核销计划 */
|
||||||
|
@Autowired
|
||||||
|
private ReceiptWriteoffPlanMapper receiptWriteoffPlanMapper;
|
||||||
|
|
||||||
|
/** 余额核销:收款审核通过事务内逐张核销账单 */
|
||||||
|
@Autowired
|
||||||
|
private IArBalanceWriteoffService balanceWriteoffService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public List<CustomerAccount> selectAccountList(Long customerId)
|
public List<CustomerAccount> selectAccountList(Long customerId)
|
||||||
@@ -118,6 +140,7 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
query.setBizType(BmsCustomerAttachment.BIZ_TYPE_CUSTOMER_RECEIPT);
|
query.setBizType(BmsCustomerAttachment.BIZ_TYPE_CUSTOMER_RECEIPT);
|
||||||
query.setBizId(id);
|
query.setBizId(id);
|
||||||
receipt.setAttachmentList(bmsCustomerMapper.selectBmsCustomerAttachmentList(query));
|
receipt.setAttachmentList(bmsCustomerMapper.selectBmsCustomerAttachmentList(query));
|
||||||
|
receipt.setWriteoffPlans(receiptWriteoffPlanMapper.selectByReceiptId(id));
|
||||||
}
|
}
|
||||||
return receipt;
|
return receipt;
|
||||||
}
|
}
|
||||||
@@ -134,6 +157,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("收款金额必须大于0");
|
throw new ServiceException("收款金额必须大于0");
|
||||||
}
|
}
|
||||||
|
// 统一金额口径:入口即规范为两位小数,后续本位币、核销合计、余额分配均基于该值计算
|
||||||
|
receipt.setAmount(receipt.getAmount().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||||
if (!StringUtils.hasText(receipt.getReceiptType()))
|
if (!StringUtils.hasText(receipt.getReceiptType()))
|
||||||
{
|
{
|
||||||
receipt.setReceiptType(CustomerReceipt.TYPE_PREPAY);
|
receipt.setReceiptType(CustomerReceipt.TYPE_PREPAY);
|
||||||
@@ -231,11 +256,24 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
BigDecimal baseAmount = receipt.getAmount().multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
receipt.setBaseAmount(baseAmount);
|
receipt.setBaseAmount(baseAmount);
|
||||||
|
|
||||||
|
// 核销账单计划校验:当前账单均为人民币(CNY)账单,计划金额即本位币 CNY 金额,
|
||||||
|
// 上限为收款按汇率折算后的本位币金额 baseAmount;核销本身不增减客户余额。
|
||||||
|
List<ReceiptWriteoffPlan> plans = validateWriteoffPlans(receipt);
|
||||||
|
BigDecimal allocatedBase = plans.stream()
|
||||||
|
.map(ReceiptWriteoffPlan::getWriteoffAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
// 全额分配=还款核销,部分分配=混合,不选账单=预充值/保持所选类型(本位币口径比较)
|
||||||
|
if (!plans.isEmpty())
|
||||||
|
{
|
||||||
|
receipt.setReceiptType(allocatedBase.compareTo(baseAmount) == 0
|
||||||
|
? CustomerReceipt.TYPE_REPAY : CustomerReceipt.TYPE_MIXED);
|
||||||
|
}
|
||||||
|
|
||||||
receipt.setReceiptNo(generateNo("SK"));
|
receipt.setReceiptNo(generateNo("SK"));
|
||||||
receipt.setStatus(CustomerReceipt.STATUS_PENDING);
|
receipt.setStatus(CustomerReceipt.STATUS_PENDING);
|
||||||
receipt.setAllocatedAmount(BigDecimal.ZERO);
|
// 核销金额即本位币 CNY,无需再乘汇率
|
||||||
// 核销口径统一为本位币 CNY
|
receipt.setAllocatedAmount(allocatedBase);
|
||||||
receipt.setUnallocatedAmount(baseAmount);
|
receipt.setUnallocatedAmount(baseAmount.subtract(allocatedBase));
|
||||||
if (!StringUtils.hasText(receipt.getInvoiceStatus()))
|
if (!StringUtils.hasText(receipt.getInvoiceStatus()))
|
||||||
{
|
{
|
||||||
receipt.setInvoiceStatus("NONE");
|
receipt.setInvoiceStatus("NONE");
|
||||||
@@ -243,11 +281,146 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
receipt.setVersion(0);
|
receipt.setVersion(0);
|
||||||
customerMoneyMapper.insertReceipt(receipt);
|
customerMoneyMapper.insertReceipt(receipt);
|
||||||
|
|
||||||
|
// 核销计划落 PENDING,审核通过事务内再逐张执行
|
||||||
|
String operator = receipt.getCreateBy();
|
||||||
|
for (ReceiptWriteoffPlan plan : plans)
|
||||||
|
{
|
||||||
|
plan.setReceiptId(receipt.getId());
|
||||||
|
plan.setStatus(ReceiptWriteoffPlan.STATUS_PENDING);
|
||||||
|
plan.setCreateBy(operator);
|
||||||
|
receiptWriteoffPlanMapper.insertPlan(plan);
|
||||||
|
}
|
||||||
|
|
||||||
// 收款附件:复用客户附件表,业务模块挂 customer_receipt,biz_id 关联收款单
|
// 收款附件:复用客户附件表,业务模块挂 customer_receipt,biz_id 关联收款单
|
||||||
saveReceiptAttachments(receipt);
|
saveReceiptAttachments(receipt);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验登记收款携带的核销账单计划并规范化。
|
||||||
|
* 口径(暂不支持多币种账单):
|
||||||
|
* 账单必须属于当前客户、账单币种为人民币 CNY、状态可核销(已确认/部分核销);
|
||||||
|
* 每张本次核销额(CNY)大于0且不超过账单未核销额;
|
||||||
|
* 合计不超过收款按汇率折算后的本位币金额 baseAmount(CNY)。
|
||||||
|
* 审核时会再次锁账单强校验,防止登记后账单被别处抢先核销。
|
||||||
|
*/
|
||||||
|
private List<ReceiptWriteoffPlan> validateWriteoffPlans(CustomerReceipt receipt)
|
||||||
|
{
|
||||||
|
List<ReceiptWriteoffPlan> raw = receipt.getWriteoffPlans();
|
||||||
|
if (CollectionUtils.isEmpty(raw))
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
BigDecimal baseAmount = receipt.getBaseAmount() != null
|
||||||
|
? receipt.getBaseAmount().setScale(MONEY_SCALE, RoundingMode.HALF_UP)
|
||||||
|
: receipt.getAmount().multiply(receipt.getExchangeRate()).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
List<ReceiptWriteoffPlan> plans = new ArrayList<>();
|
||||||
|
Set<Long> billIds = new HashSet<>();
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (ReceiptWriteoffPlan item : raw)
|
||||||
|
{
|
||||||
|
if (item == null || item.getArBillId() == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal amount = item.getWriteoffAmount();
|
||||||
|
if (amount == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("请填写每张账单的本次核销金额");
|
||||||
|
}
|
||||||
|
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
if (amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("核销金额必须大于0");
|
||||||
|
}
|
||||||
|
if (!billIds.add(item.getArBillId()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("同一账单不能重复选择,请合并为一条并填写合计金额");
|
||||||
|
}
|
||||||
|
ArBill bill = arBillMapper.selectArBillById(item.getArBillId());
|
||||||
|
if (bill == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("所选应收账单不存在,请刷新后重新选择");
|
||||||
|
}
|
||||||
|
if (!receipt.getCustomerId().equals(bill.getCustomerId()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("账单【" + bill.getBillNo() + "】不属于当前客户");
|
||||||
|
}
|
||||||
|
// 现阶段仅支持人民币账单核销;外币账单暂不在此入口处理
|
||||||
|
String billCurrency = StringUtils.hasText(bill.getCurrency())
|
||||||
|
? bill.getCurrency().trim().toUpperCase() : BASE_CURRENCY;
|
||||||
|
if (!BASE_CURRENCY.equals(billCurrency))
|
||||||
|
{
|
||||||
|
throw new ServiceException("账单【" + bill.getBillNo() + "】币种为 " + billCurrency
|
||||||
|
+ ",当前仅支持人民币(CNY)账单核销");
|
||||||
|
}
|
||||||
|
ArBillStatus billStatus = ArBillStatus.of(bill.getStatus());
|
||||||
|
if (billStatus == null || !billStatus.canWriteOff())
|
||||||
|
{
|
||||||
|
throw new ServiceException("账单【" + bill.getBillNo() + "】当前状态不允许核销:"
|
||||||
|
+ (billStatus == null ? bill.getStatus() : billStatus.getLabel()));
|
||||||
|
}
|
||||||
|
BigDecimal unwritten = bill.getUnwrittenAmount() == null
|
||||||
|
? BigDecimal.ZERO : bill.getUnwrittenAmount();
|
||||||
|
if (amount.compareTo(unwritten) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("账单【" + bill.getBillNo() + "】本次核销金额 "
|
||||||
|
+ amount.toPlainString() + " 超过其未核销金额 " + unwritten.toPlainString());
|
||||||
|
}
|
||||||
|
total = total.add(amount);
|
||||||
|
if (total.compareTo(baseAmount) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("核销金额合计 " + total.toPlainString()
|
||||||
|
+ " CNY 超过本收款单折算本位币金额 " + baseAmount.toPlainString() + " CNY");
|
||||||
|
}
|
||||||
|
|
||||||
|
ReceiptWriteoffPlan plan = new ReceiptWriteoffPlan();
|
||||||
|
plan.setArBillId(bill.getArBillId());
|
||||||
|
plan.setBillNo(bill.getBillNo());
|
||||||
|
plan.setCustomerId(bill.getCustomerId());
|
||||||
|
plan.setCurrency(BASE_CURRENCY);
|
||||||
|
plan.setWriteoffAmount(amount);
|
||||||
|
plan.setRemark(StringUtils.hasText(item.getRemark()) ? item.getRemark().trim() : null);
|
||||||
|
plans.add(plan);
|
||||||
|
}
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收款审核通过后执行核销计划:同一事务内逐张锁账单核销,
|
||||||
|
* 任一账单已被别处抢先核销导致超额时,整笔审核回滚(含余额入账),不产生半截数据。
|
||||||
|
*/
|
||||||
|
private void executeReceiptWriteoffPlans(CustomerReceipt receipt, String operator)
|
||||||
|
{
|
||||||
|
List<ReceiptWriteoffPlan> plans = receiptWriteoffPlanMapper.selectByReceiptId(receipt.getId());
|
||||||
|
if (CollectionUtils.isEmpty(plans))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Date writeoffDate = new Date();
|
||||||
|
for (ReceiptWriteoffPlan plan : plans)
|
||||||
|
{
|
||||||
|
if (!ReceiptWriteoffPlan.STATUS_PENDING.equals(plan.getStatus()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ArBalanceWriteoff cmd = new ArBalanceWriteoff();
|
||||||
|
cmd.setArBillId(plan.getArBillId());
|
||||||
|
cmd.setCustomerId(plan.getCustomerId());
|
||||||
|
cmd.setReceiptId(receipt.getId());
|
||||||
|
cmd.setWriteoffAmount(plan.getWriteoffAmount());
|
||||||
|
cmd.setWriteoffDate(writeoffDate);
|
||||||
|
cmd.setRemark("收款单审核联动核销:" + receipt.getReceiptNo());
|
||||||
|
cmd.setCreateBy(operator);
|
||||||
|
// writeoff 内部锁账单行、复核状态与未核销额,冲突时抛异常由外层事务统一回滚
|
||||||
|
ArBalanceWriteoff done = balanceWriteoffService.writeoff(cmd);
|
||||||
|
if (receiptWriteoffPlanMapper.updateEffective(plan.getPlanId(), done.getWriteoffId(), operator) != 1)
|
||||||
|
{
|
||||||
|
throw new ServiceException("收款核销计划状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造汇率表来源说明:汇率来源(中国银行/央行等)+ 命中路径(正向/反向取倒数/经CNY交叉)。
|
* 构造汇率表来源说明:汇率来源(中国银行/央行等)+ 命中路径(正向/反向取倒数/经CNY交叉)。
|
||||||
* resolveRate 返回对象的 remark 带 __RATE_PATH__ 前缀标记命中路径。
|
* resolveRate 返回对象的 remark 带 __RATE_PATH__ 前缀标记命中路径。
|
||||||
@@ -343,6 +516,9 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("收款单状态已变更,请刷新后重试");
|
throw new ServiceException("收款单状态已变更,请刷新后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 登记时选择的账单在同一事务内一并核销;任一账单核销失败则整笔审核回滚
|
||||||
|
executeReceiptWriteoffPlans(receipt, username);
|
||||||
return receiptRows;
|
return receiptRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +535,11 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("仅待确认的收款单可以取消");
|
throw new ServiceException("仅待确认的收款单可以取消");
|
||||||
}
|
}
|
||||||
return customerMoneyMapper.cancelReceipt(id, SecurityUtils.getUsername());
|
String operator = SecurityUtils.getUsername();
|
||||||
|
int rows = customerMoneyMapper.cancelReceipt(id, operator);
|
||||||
|
// 尚未执行的核销计划一并作废
|
||||||
|
receiptWriteoffPlanMapper.cancelPendingByReceiptId(id, operator);
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="arBillId" column="ar_bill_id"/>
|
<result property="arBillId" column="ar_bill_id"/>
|
||||||
<result property="billNo" column="bill_no"/>
|
<result property="billNo" column="bill_no"/>
|
||||||
<result property="customerId" column="customer_id"/>
|
<result property="customerId" column="customer_id"/>
|
||||||
|
<result property="receiptId" column="receipt_id"/>
|
||||||
<result property="accountId" column="account_id"/>
|
<result property="accountId" column="account_id"/>
|
||||||
<result property="currency" column="currency"/>
|
<result property="currency" column="currency"/>
|
||||||
<result property="writeoffAmount" column="writeoff_amount"/>
|
<result property="writeoffAmount" column="writeoff_amount"/>
|
||||||
@@ -28,7 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectColumns">
|
<sql id="selectColumns">
|
||||||
select w.writeoff_id, w.writeoff_no, w.ar_bill_id, w.bill_no, w.customer_id, w.account_id,
|
select w.writeoff_id, w.writeoff_no, w.ar_bill_id, w.bill_no, w.customer_id, w.receipt_id, w.account_id,
|
||||||
w.currency, w.writeoff_amount, w.balance_flow_id, w.reverse_flow_id,
|
w.currency, w.writeoff_amount, w.balance_flow_id, w.reverse_flow_id,
|
||||||
w.writeoff_date, w.status, w.remark, w.create_by, w.create_time,
|
w.writeoff_date, w.status, w.remark, w.create_by, w.create_time,
|
||||||
w.cancel_by, w.cancel_time, w.cancel_remark,
|
w.cancel_by, w.cancel_time, w.cancel_remark,
|
||||||
@@ -40,11 +41,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<insert id="insertArBalanceWriteoff" parameterType="ArBalanceWriteoff"
|
<insert id="insertArBalanceWriteoff" parameterType="ArBalanceWriteoff"
|
||||||
useGeneratedKeys="true" keyProperty="writeoffId">
|
useGeneratedKeys="true" keyProperty="writeoffId">
|
||||||
insert into ar_balance_writeoff (
|
insert into ar_balance_writeoff (
|
||||||
writeoff_no, ar_bill_id, bill_no, customer_id, account_id, currency,
|
writeoff_no, ar_bill_id, bill_no, customer_id, receipt_id, account_id, currency,
|
||||||
writeoff_amount, balance_flow_id, reverse_flow_id, writeoff_date,
|
writeoff_amount, balance_flow_id, reverse_flow_id, writeoff_date,
|
||||||
status, remark, create_by, create_time
|
status, remark, create_by, create_time
|
||||||
) values (
|
) values (
|
||||||
#{writeoffNo}, #{arBillId}, #{billNo}, #{customerId}, #{accountId}, #{currency},
|
#{writeoffNo}, #{arBillId}, #{billNo}, #{customerId}, #{receiptId}, #{accountId}, #{currency},
|
||||||
#{writeoffAmount}, #{balanceFlowId}, #{reverseFlowId}, #{writeoffDate},
|
#{writeoffAmount}, #{balanceFlowId}, #{reverseFlowId}, #{writeoffDate},
|
||||||
#{status}, #{remark}, #{createBy}, sysdate()
|
#{status}, #{remark}, #{createBy}, sysdate()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -392,6 +392,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
where id = #{id} and status = 'PENDING' and del_flag = '0'
|
where id = #{id} and status = 'PENDING' and del_flag = '0'
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<!-- 反核销联动:仅回写分配口径金额与收款类型,客户余额/额度不在此变动 -->
|
||||||
|
<update id="updateReceiptAllocation" parameterType="com.bms.customer.customermoney.domain.CustomerReceipt">
|
||||||
|
update ctr_customer_receipt
|
||||||
|
set allocated_amount = #{allocatedAmount},
|
||||||
|
unallocated_amount = #{unallocatedAmount},
|
||||||
|
receipt_type = #{receiptType},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = now()
|
||||||
|
where id = #{id} and status = 'CONFIRMED' and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
<!-- ================= 余额流水 ================= -->
|
<!-- ================= 余额流水 ================= -->
|
||||||
<resultMap type="com.bms.customer.customermoney.domain.CustomerBalanceFlow" id="CustomerBalanceFlowResult">
|
<resultMap type="com.bms.customer.customermoney.domain.CustomerBalanceFlow" id="CustomerBalanceFlowResult">
|
||||||
<id property="id" column="id" />
|
<id property="id" column="id" />
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.bms.customer.customermoney.mapper.ReceiptWriteoffPlanMapper">
|
||||||
|
|
||||||
|
<resultMap id="ReceiptWriteoffPlanResult" type="com.bms.customer.customermoney.domain.ReceiptWriteoffPlan">
|
||||||
|
<id property="planId" column="plan_id" />
|
||||||
|
<result property="receiptId" column="receipt_id" />
|
||||||
|
<result property="arBillId" column="ar_bill_id" />
|
||||||
|
<result property="billNo" column="bill_no" />
|
||||||
|
<result property="customerId" column="customer_id" />
|
||||||
|
<result property="currency" column="currency" />
|
||||||
|
<result property="writeoffAmount" column="writeoff_amount" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="writeoffId" column="writeoff_id" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateBy" column="update_by" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
<result property="writeoffDate" column="writeoff_date" />
|
||||||
|
<result property="billName" column="bill_name" />
|
||||||
|
<result property="totalAmount" column="total_amount" />
|
||||||
|
<result property="writtenOffAmount" column="written_off_amount"/>
|
||||||
|
<result property="unwrittenAmount" column="unwritten_amount" />
|
||||||
|
<result property="billStatus" column="bill_status" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectColumns">
|
||||||
|
select p.plan_id, p.receipt_id, p.ar_bill_id, p.bill_no, p.customer_id, p.currency,
|
||||||
|
p.writeoff_amount, p.status, p.writeoff_id, p.remark,
|
||||||
|
p.create_by, p.create_time, p.update_by, p.update_time,
|
||||||
|
w.writeoff_date,
|
||||||
|
b.bill_name, b.total_amount, b.written_off_amount, b.unwritten_amount,
|
||||||
|
b.status as bill_status
|
||||||
|
from ctr_receipt_writeoff_plan p
|
||||||
|
left join ar_balance_writeoff w on w.writeoff_id = p.writeoff_id
|
||||||
|
left join ar_bill b on b.ar_bill_id = p.ar_bill_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<insert id="insertPlan" parameterType="com.bms.customer.customermoney.domain.ReceiptWriteoffPlan"
|
||||||
|
useGeneratedKeys="true" keyProperty="planId">
|
||||||
|
insert into ctr_receipt_writeoff_plan (
|
||||||
|
receipt_id, ar_bill_id, bill_no, customer_id, currency, writeoff_amount,
|
||||||
|
status, writeoff_id, remark, create_by, create_time, del_flag
|
||||||
|
) values (
|
||||||
|
#{receiptId}, #{arBillId}, #{billNo}, #{customerId}, #{currency}, #{writeoffAmount},
|
||||||
|
#{status}, #{writeoffId}, #{remark}, #{createBy}, sysdate(), '0'
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="selectByReceiptId" resultMap="ReceiptWriteoffPlanResult">
|
||||||
|
<include refid="selectColumns"/>
|
||||||
|
where p.receipt_id = #{receiptId} and p.del_flag = '0'
|
||||||
|
order by p.plan_id asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="updateEffective">
|
||||||
|
update ctr_receipt_writeoff_plan
|
||||||
|
set status = 'EFFECTIVE',
|
||||||
|
writeoff_id = #{writeoffId},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where plan_id = #{planId} and status = 'PENDING' and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="cancelPendingByReceiptId">
|
||||||
|
update ctr_receipt_writeoff_plan
|
||||||
|
set status = 'CANCELLED',
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where receipt_id = #{receiptId} and status = 'PENDING' and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="selectEffectiveByWriteoffId" resultMap="ReceiptWriteoffPlanResult">
|
||||||
|
<include refid="selectColumns"/>
|
||||||
|
where p.writeoff_id = #{writeoffId} and p.status = 'EFFECTIVE' and p.del_flag = '0'
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="updateReversedByWriteoffId">
|
||||||
|
update ctr_receipt_writeoff_plan
|
||||||
|
set status = 'REVERSED',
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where writeoff_id = #{writeoffId} and status = 'EFFECTIVE' and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 功能:登记收款时选择核销账单,审核通过后一并核销
|
||||||
|
-- 日期:2026-10-08
|
||||||
|
-- 说明:
|
||||||
|
-- 1) 新增收款核销计划表 ctr_receipt_writeoff_plan:登记时落 PENDING 计划,
|
||||||
|
-- 审核通过事务内逐张执行 ar_bill 核销并回写 EFFECTIVE/核销单ID,取消收款单则置 CANCELLED;
|
||||||
|
-- 联动核销单后续在余额核销页被反核销则置 REVERSED,并回退收款单 allocated/unallocated(不动余额/额度)。
|
||||||
|
-- 2) ar_balance_writeoff 增加 receipt_id:审核联动生成的核销单可溯源到收款单;
|
||||||
|
-- 手工余额核销保持为空。
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 1. 收款核销计划表
|
||||||
|
CREATE TABLE IF NOT EXISTS `ctr_receipt_writeoff_plan` (
|
||||||
|
`plan_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '计划ID',
|
||||||
|
`receipt_id` bigint(20) NOT NULL COMMENT '收款单ID(ctr_customer_receipt.id)',
|
||||||
|
`ar_bill_id` bigint(20) NOT NULL COMMENT '应收账单ID',
|
||||||
|
`bill_no` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账单号快照',
|
||||||
|
`customer_id` bigint(20) NOT NULL COMMENT '客户ID',
|
||||||
|
`currency` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'CNY' COMMENT '账单币种(当前仅支持CNY)',
|
||||||
|
`writeoff_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '本次核销金额(本位币CNY;当前仅支持人民币账单)',
|
||||||
|
`status` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待执行/EFFECTIVE已核销/CANCELLED已作废/REVERSED已反核销',
|
||||||
|
`writeoff_id` bigint(20) DEFAULT NULL COMMENT '审核后生成的余额核销单ID(ar_balance_writeoff.writeoff_id)',
|
||||||
|
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
||||||
|
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`del_flag` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
|
||||||
|
PRIMARY KEY (`plan_id`),
|
||||||
|
KEY `idx_crwp_receipt` (`receipt_id`,`status`),
|
||||||
|
KEY `idx_crwp_bill` (`ar_bill_id`),
|
||||||
|
KEY `idx_crwp_customer` (`customer_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收款单核销账单计划';
|
||||||
|
|
||||||
|
-- 2. 余额核销单关联收款单
|
||||||
|
ALTER TABLE `ar_balance_writeoff`
|
||||||
|
ADD COLUMN `receipt_id` bigint(20) DEFAULT NULL COMMENT '关联收款单ID(收款审核联动核销时写入)' AFTER `customer_id`,
|
||||||
|
ADD KEY `idx_abw_receipt` (`receipt_id`);
|
||||||
+265
-7
@@ -531,7 +531,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-table :data="writeoffList" border size="small" v-loading="writeoffLoading">
|
<el-table :data="writeoffList" border size="small" v-loading="writeoffLoading">
|
||||||
<el-table-column label="核销单号" align="center" prop="writeoffNo" width="180" />
|
<el-table-column label="核销单号" align="center" width="190">
|
||||||
|
<template #default="scope">
|
||||||
|
<div>{{ scope.row.writeoffNo }}</div>
|
||||||
|
<el-tag v-if="scope.row.receiptId" type="primary" size="small" effect="plain" style="margin-top: 2px;">收款联动</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
|
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="账单名称" align="center" prop="billName" min-width="140" show-overflow-tooltip />
|
<el-table-column label="账单名称" align="center" prop="billName" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column label="核销金额" align="center" width="120">
|
<el-table-column label="核销金额" align="center" width="120">
|
||||||
@@ -574,7 +579,7 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- ====== 登记收款弹窗 ====== -->
|
<!-- ====== 登记收款弹窗 ====== -->
|
||||||
<el-dialog title="登记收款" v-model="receiptDialog.visible" width="640px" append-to-body @closed="onReceiptDialogClosed">
|
<el-dialog title="登记收款" v-model="receiptDialog.visible" width="780px" append-to-body @closed="onReceiptDialogClosed">
|
||||||
<el-form ref="receiptFormRef" :model="receiptDialog.form" :rules="receiptRules" label-width="110px">
|
<el-form ref="receiptFormRef" :model="receiptDialog.form" :rules="receiptRules" label-width="110px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -665,6 +670,54 @@
|
|||||||
<el-input v-model="receiptDialog.form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
<el-input v-model="receiptDialog.form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="核销账单">
|
||||||
|
<div class="receipt-wo-toolbar">
|
||||||
|
<el-button type="primary" plain icon="Plus" @click="openBillPicker">选择账单核销</el-button>
|
||||||
|
<span class="receipt-wo-summary">
|
||||||
|
本次核销合计:<b class="money-out">{{ formatMoney(receiptWriteoffTotal) }} CNY</b>
|
||||||
|
,收款折算本位币:<b>{{ formatMoney(receiptBaseCny) }} CNY</b>
|
||||||
|
<!-- ,核销后剩余入账:<b :class="receiptWriteoffRemain < 0 ? 'money-out' : 'money-in'">-->
|
||||||
|
<!-- {{ formatMoney(receiptWriteoffRemain) }} CNY-->
|
||||||
|
<!-- </b>-->
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="receiptDialog.form.writeoffPlans" border size="small" max-height="220">
|
||||||
|
<el-table-column label="账单号" prop="billNo" align="center" width="170" show-overflow-tooltip />
|
||||||
|
<el-table-column label="账单名称" prop="billName" align="center" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="未核销金额" align="center" width="120">
|
||||||
|
<template #default="scope">{{ formatMoney(scope.row.unwrittenAmount) }} {{ scope.row.currency }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="本次核销金额(CNY)" align="center" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-input-number
|
||||||
|
v-model="scope.row.writeoffAmount"
|
||||||
|
:min="0"
|
||||||
|
:max="Number(scope.row.unwrittenAmount)"
|
||||||
|
:precision="2"
|
||||||
|
:step="1"
|
||||||
|
:controls="false"
|
||||||
|
size="small"
|
||||||
|
style="width: 130px;"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="70">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="danger" icon="Delete" @click="removeReceiptBill(scope.$index)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<span style="color: var(--el-text-color-secondary);">不选账单则整笔计入客户余额,可在“账单核销”页手工核销</span>
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<div class="upload-tip">
|
||||||
|
当前仅支持<strong>人民币(CNY)账单</strong>核销,账单行金额均按 CNY 填写;
|
||||||
|
所选账单在收款单<strong>审核通过</strong>后一并核销,核销合计不能超过收款金额按汇率折算的本位币金额
|
||||||
|
({{ formatMoney(receiptBaseCny) }} CNY);<strong>核销不扣减客户余额</strong>,正常计入客户余额。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="收款凭证">
|
<el-form-item label="收款凭证">
|
||||||
<el-upload
|
<el-upload
|
||||||
@@ -747,6 +800,27 @@
|
|||||||
<el-descriptions-item label="备注" :span="2">{{ viewDialog.data.remark || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="备注" :span="2">{{ viewDialog.data.remark || '-' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
|
<template v-if="viewDialog.data && viewDialog.data.writeoffPlans && viewDialog.data.writeoffPlans.length">
|
||||||
|
<div class="attach-title">关联核销账单({{ viewDialog.data.writeoffPlans.length }})</div>
|
||||||
|
<el-table :data="viewDialog.data.writeoffPlans" border size="small" max-height="220" style="margin-bottom: 12px;">
|
||||||
|
<el-table-column label="账单号" prop="billNo" align="center" width="170" show-overflow-tooltip />
|
||||||
|
<el-table-column label="账单名称" prop="billName" align="center" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="本次核销金额" align="center" width="140">
|
||||||
|
<template #default="scope">{{ formatMoney(scope.row.writeoffAmount) }} {{ scope.row.currency }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="receiptPlanStatusTag(scope.row.status)" size="small">
|
||||||
|
{{ receiptPlanStatusLabel(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="核销日期" align="center" width="110">
|
||||||
|
<template #default="scope">{{ scope.row.writeoffDate ? formatDate(scope.row.writeoffDate) : '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="attach-title">收款凭证附件</div>
|
<div class="attach-title">收款凭证附件</div>
|
||||||
<el-table :data="viewDialog.attachments" border size="small" max-height="240" v-loading="viewDialog.loading">
|
<el-table :data="viewDialog.attachments" border size="small" max-height="240" v-loading="viewDialog.loading">
|
||||||
<el-table-column label="序号" type="index" width="55" align="center" />
|
<el-table-column label="序号" type="index" width="55" align="center" />
|
||||||
@@ -913,6 +987,49 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- ====== 登记收款-选择核销账单弹窗 ====== -->
|
||||||
|
<el-dialog title="选择待核销账单" v-model="billPicker.visible" width="860px" append-to-body>
|
||||||
|
<el-alert type="info" :closable="false" class="writeoff-summary">
|
||||||
|
当前仅支持人民币(CNY)账单核销,以下为状态已确认/部分核销的 CNY 账单;
|
||||||
|
已选 {{ billPicker.selected.length }} 张,合计未核销
|
||||||
|
<b class="money-out">{{ formatMoney(billPickerSelectedTotal) }} CNY</b>,
|
||||||
|
本收款单折算可核销上限 <b>{{ formatMoney(receiptBaseCny) }} CNY</b>
|
||||||
|
</el-alert>
|
||||||
|
<el-table
|
||||||
|
ref="billPickerTableRef"
|
||||||
|
:data="billPicker.rows"
|
||||||
|
border
|
||||||
|
size="small"
|
||||||
|
max-height="380"
|
||||||
|
row-key="arBillId"
|
||||||
|
v-loading="billPicker.loading"
|
||||||
|
@selection-change="onPickerSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="42" align="center" reserve-selection />
|
||||||
|
<el-table-column label="账单号" prop="billNo" align="center" width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="账单名称" prop="billName" align="center" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="账单日期" prop="billDate" align="center" width="105">
|
||||||
|
<template #default="scope">{{ formatDate(scope.row.billDate) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="应收金额" align="center" width="110">
|
||||||
|
<template #default="scope">{{ formatMoney(scope.row.totalAmount) }} {{ scope.row.currency }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="已核销" align="center" width="100">
|
||||||
|
<template #default="scope"><span class="money-in">{{ formatMoney(scope.row.writtenOffAmount) }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="未核销" align="center" width="100">
|
||||||
|
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.unwrittenAmount) }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="该币种暂无可核销账单" :image-size="70" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="confirmBillPicker">确 定({{ billPicker.selected.length }})</el-button>
|
||||||
|
<el-button @click="billPicker.visible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- ====== 余额核销账单弹窗 ====== -->
|
<!-- ====== 余额核销账单弹窗 ====== -->
|
||||||
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body v-loading="woDialog.accountLoading">
|
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body v-loading="woDialog.accountLoading">
|
||||||
<el-descriptions :column="1" border size="small" v-if="woDialog.bill">
|
<el-descriptions :column="1" border size="small" v-if="woDialog.bill">
|
||||||
@@ -966,7 +1083,10 @@
|
|||||||
<el-alert type="warning" :closable="false" class="credit-tip">
|
<el-alert type="warning" :closable="false" class="credit-tip">
|
||||||
<span v-if="cancelWoDialog.row">
|
<span v-if="cancelWoDialog.row">
|
||||||
反核销后,账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销状态(核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }});
|
反核销后,账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销状态(核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }});
|
||||||
口径变更后不再回补账户余额、不再变动已用额度。
|
不回补账户余额、不变动已用额度。
|
||||||
|
<template v-if="cancelWoDialog.row.receiptId">
|
||||||
|
<br />该核销由收款单审核<strong>联动产生</strong>,反核销后收款单中本笔将回到“未分配”金额,可重新核销其他账单,已入账余额不受影响。
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<el-form label-width="100px" class="wo-form">
|
<el-form label-width="100px" class="wo-form">
|
||||||
@@ -1386,12 +1506,13 @@ function handleExportCreditFlow() {
|
|||||||
|
|
||||||
/** 审核通过 */
|
/** 审核通过 */
|
||||||
function handleConfirmReceipt(row) {
|
function handleConfirmReceipt(row) {
|
||||||
proxy.$modal.confirm(`是否确认审核通过收款单「${row.receiptNo}」?审核通过后金额将计入客户余额。`).then(() => {
|
proxy.$modal.confirm(`是否确认审核通过收款单「${row.receiptNo}」?审核通过后金额计入客户余额,登记时选择的账单将一并核销。`).then(() => {
|
||||||
return confirmReceipt(row.id)
|
return confirmReceipt(row.id)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
proxy.$modal.msgSuccess("审核通过,余额已更新")
|
proxy.$modal.msgSuccess("审核通过,余额与账单核销已更新")
|
||||||
getReceiptList()
|
getReceiptList()
|
||||||
loadAccounts()
|
loadAccounts()
|
||||||
|
loadBillWriteoffData()
|
||||||
if (activeTab.value === 'flow') getFlowList()
|
if (activeTab.value === 'flow') getFlowList()
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
@@ -1434,7 +1555,8 @@ function resetReceiptForm() {
|
|||||||
bankAccountNo: '',
|
bankAccountNo: '',
|
||||||
bankTransactionNo: '',
|
bankTransactionNo: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
attachmentList: []
|
attachmentList: [],
|
||||||
|
writeoffPlans: []
|
||||||
}
|
}
|
||||||
proxy.resetForm('receiptFormRef')
|
proxy.resetForm('receiptFormRef')
|
||||||
}
|
}
|
||||||
@@ -1476,7 +1598,7 @@ function fetchDefaultRate() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 切换币种:重置手工标记并重新取默认汇率 */
|
/** 切换币种:重置手工标记并重新取默认汇率;核销账单固定为CNY,只需按新汇率重算可核销上限,不清空已选 */
|
||||||
function handleCurrencyChange() {
|
function handleCurrencyChange() {
|
||||||
rateEdited.value = false
|
rateEdited.value = false
|
||||||
rateSourceInfo.value = { source: '', ratePath: '' }
|
rateSourceInfo.value = { source: '', ratePath: '' }
|
||||||
@@ -1502,6 +1624,130 @@ watch(() => receiptDialog.form.receivedAt, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* ============ 登记收款-选择核销账单 ============ */
|
||||||
|
const billPickerTableRef = ref(null)
|
||||||
|
const billPicker = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
rows: [],
|
||||||
|
selected: []
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 收款金额按汇率折算的本位币金额(CNY),即核销合计上限 */
|
||||||
|
const receiptBaseCny = computed(() => {
|
||||||
|
const amt = Number(receiptDialog.form.amount) || 0
|
||||||
|
const rate = Number(receiptDialog.form.exchangeRate) || 0
|
||||||
|
return Math.round(amt * rate * 100) / 100
|
||||||
|
})
|
||||||
|
/** 已分配合计(账单均为CNY,此值即 CNY) */
|
||||||
|
const receiptWriteoffTotal = computed(() => {
|
||||||
|
const list = receiptDialog.form.writeoffPlans || []
|
||||||
|
return list.reduce((s, r) => s + (Number(r.writeoffAmount) || 0), 0)
|
||||||
|
})
|
||||||
|
/** 核销后剩余入账金额(折算CNY - 已分配CNY) */
|
||||||
|
const receiptWriteoffRemain = computed(() => {
|
||||||
|
return Math.round((receiptBaseCny.value - receiptWriteoffTotal.value) * 100) / 100
|
||||||
|
})
|
||||||
|
/** 选择器中勾选账单的未核销额合计(仅提示) */
|
||||||
|
const billPickerSelectedTotal = computed(() => {
|
||||||
|
return billPicker.selected.reduce((s, r) => s + (Number(r.unwrittenAmount) || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const RECEIPT_PLAN_STATUS_MAP = {
|
||||||
|
PENDING: { label: '待核销', type: 'warning' },
|
||||||
|
EFFECTIVE: { label: '已核销', type: 'success' },
|
||||||
|
CANCELLED: { label: '已作废', type: 'info' },
|
||||||
|
REVERSED: { label: '已反核销', type: 'danger' }
|
||||||
|
}
|
||||||
|
function receiptPlanStatusLabel(s) {
|
||||||
|
return RECEIPT_PLAN_STATUS_MAP[s]?.label || s || '-'
|
||||||
|
}
|
||||||
|
function receiptPlanStatusTag(s) {
|
||||||
|
return RECEIPT_PLAN_STATUS_MAP[s]?.type || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开账单选择器:只列与收款同币种的未核销账单,并回显已选 */
|
||||||
|
function openBillPicker() {
|
||||||
|
const customerId = route.query.customerId
|
||||||
|
if (!customerId) {
|
||||||
|
proxy.$modal.msgError('缺少客户信息,请重新进入页面')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
billPicker.visible = true
|
||||||
|
billPicker.loading = true
|
||||||
|
billPicker.rows = []
|
||||||
|
billPicker.selected = []
|
||||||
|
listUnsettledBill(customerId).then(res => {
|
||||||
|
// 现阶段只核销人民币账单,与收款币种无关;核销上限为收款折算 CNY
|
||||||
|
billPicker.rows = (res.data || []).filter(r => (r.currency || 'CNY') === 'CNY')
|
||||||
|
nextTick(() => {
|
||||||
|
billPickerTableRef.value?.clearSelection()
|
||||||
|
const chosen = receiptDialog.form.writeoffPlans || []
|
||||||
|
billPicker.rows.forEach(r => {
|
||||||
|
if (chosen.some(p => p.arBillId === r.arBillId)) {
|
||||||
|
billPickerTableRef.value?.toggleRowSelection(r, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
billPicker.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPickerSelectionChange(rows) {
|
||||||
|
billPicker.selected = rows || []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认选择:新增行默认核销额=min(账单未核销额CNY, 收款折算CNY剩余可用),保留已填金额 */
|
||||||
|
function confirmBillPicker() {
|
||||||
|
const oldMap = new Map((receiptDialog.form.writeoffPlans || []).map(p => [p.arBillId, p]))
|
||||||
|
receiptDialog.form.writeoffPlans = billPicker.selected.map(r => {
|
||||||
|
const existed = oldMap.get(r.arBillId)
|
||||||
|
const unwritten = Number(r.unwrittenAmount) || 0
|
||||||
|
const usedByOthers = receiptDialog.form.writeoffPlans
|
||||||
|
.filter(p => p.arBillId !== r.arBillId)
|
||||||
|
.reduce((s, p) => s + (Number(p.writeoffAmount) || 0), 0)
|
||||||
|
const remainCny = Math.max(receiptBaseCny.value - usedByOthers, 0)
|
||||||
|
return {
|
||||||
|
arBillId: r.arBillId,
|
||||||
|
billNo: r.billNo,
|
||||||
|
billName: r.billName,
|
||||||
|
currency: 'CNY',
|
||||||
|
unwrittenAmount: unwritten,
|
||||||
|
writeoffAmount: existed ? Number(existed.writeoffAmount) : Math.round(Math.min(unwritten, remainCny) * 100) / 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
billPicker.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeReceiptBill(index) {
|
||||||
|
receiptDialog.form.writeoffPlans.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交前校验核销账单(金额口径均为 CNY) */
|
||||||
|
function validateReceiptWriteoffPlans() {
|
||||||
|
const cap = receiptBaseCny.value
|
||||||
|
const list = receiptDialog.form.writeoffPlans || []
|
||||||
|
let total = 0
|
||||||
|
for (const p of list) {
|
||||||
|
const amt = Number(p.writeoffAmount)
|
||||||
|
if (!amt || amt <= 0) {
|
||||||
|
proxy.$modal.msgError(`账单【${p.billNo}】本次核销金额必须大于0`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (amt > Number(p.unwrittenAmount)) {
|
||||||
|
proxy.$modal.msgError(`账单【${p.billNo}】核销金额超过其未核销金额`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
total += amt
|
||||||
|
}
|
||||||
|
if (Math.round(total * 100) > Math.round(cap * 100)) {
|
||||||
|
proxy.$modal.msgError(`核销金额合计 ${total.toFixed(2)} CNY 超过收款折算本位币金额 ${cap.toFixed(2)} CNY`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function submitReceipt() {
|
function submitReceipt() {
|
||||||
receiptFormRef.value.validate(valid => {
|
receiptFormRef.value.validate(valid => {
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
@@ -1511,6 +1757,7 @@ function submitReceipt() {
|
|||||||
proxy.$modal.msgError(`请录入 ${cur} 对 CNY 的汇率(汇率表未配置时可手工填写)`)
|
proxy.$modal.msgError(`请录入 ${cur} 对 CNY 的汇率(汇率表未配置时可手工填写)`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!validateReceiptWriteoffPlans()) return
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
addReceipt(receiptDialog.form).then(() => {
|
addReceipt(receiptDialog.form).then(() => {
|
||||||
proxy.$modal.msgSuccess("登记成功,等待审核")
|
proxy.$modal.msgSuccess("登记成功,等待审核")
|
||||||
@@ -2222,6 +2469,17 @@ watch(() => route.query.customerId, (newId, oldId) => {
|
|||||||
.writeoff-summary {
|
.writeoff-summary {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.receipt-wo-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.receipt-wo-summary {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
.wo-summary {
|
.wo-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user