应收账单功能实现

This commit is contained in:
caojia
2026-09-16 11:02:18 +08:00
parent 58d13904ec
commit 7172621fd3
64 changed files with 4103 additions and 4 deletions
@@ -0,0 +1,28 @@
package com.ruoyi.ar.domain;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 公司收款账户配置。 */
@Data
public class ArBankAccount implements Serializable
{
private static final long serialVersionUID = 1L;
private Long bankAccountId;
private String accountName;
private String accountNo;
private String bankName;
private String currency;
private String isDefault;
private String status;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -0,0 +1,48 @@
package com.ruoyi.ar.domain;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 正式应收账单。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ArBill extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long arBillId;
private String billNo;
private String billName;
private Long customerId;
private String customerCode;
private String customerName;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date billPeriodStart;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date billPeriodEnd;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date billDate;
private String currency;
private Integer orderCount;
private BigDecimal totalAmount;
private BigDecimal writtenOffAmount;
private BigDecimal unwrittenAmount;
private String status;
private Integer billVersion;
private String bankAccountName;
private String bankAccountNo;
private String bankName;
private String remark;
private String confirmedBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date confirmedTime;
private List<ArBillDetail> details;
private List<ArBillExport> exports;
private List<ArBillLog> logs;
}
@@ -0,0 +1,43 @@
package com.ruoyi.ar.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 账单订单快照。 */
@Data
public class ArBillDetail implements Serializable
{
private static final long serialVersionUID = 1L;
private Long detailId;
private Long arBillId;
private String prcBillId;
private Integer detailNo;
private String orderNo;
private String trackingNo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date receiveTime;
private String productCode;
private String productName;
private String goodsDescription;
private String countryCode;
private String countryName;
private String destPostcode;
private BigDecimal actualWeight;
private BigDecimal volumeWeight;
private BigDecimal chargeWeight;
private BigDecimal pkgLength;
private BigDecimal pkgWidth;
private BigDecimal pkgHeight;
private String currency;
private BigDecimal totalAmount;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private List<ArBillDetailFee> fees;
}
@@ -0,0 +1,33 @@
package com.ruoyi.ar.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 账单费用快照。 */
@Data
public class ArBillDetailFee implements Serializable
{
private static final long serialVersionUID = 1L;
private Long detailFeeId;
private Long detailId;
private String prcFeeId;
private Integer feeNo;
private String feeType;
private String feeName;
private Long scId;
private BigDecimal quantity;
private String unit;
private BigDecimal unitPrice;
private BigDecimal amount;
private String currency;
private String calculationFormula;
private String roundMode;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,29 @@
package com.ruoyi.ar.domain;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 账单导出文件元数据。 */
@Data
public class ArBillExport implements Serializable
{
private static final long serialVersionUID = 1L;
private Long exportId;
private Long arBillId;
private String fileName;
private String fileType;
private String templateCode;
private String templateVersion;
private String storageKey;
private Long fileSize;
private String fileHash;
private Integer rowCount;
private String exportStatus;
private String exportBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date exportTime;
private String remark;
}
@@ -0,0 +1,36 @@
package com.ruoyi.ar.domain;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应收模块追加式审计日志。 */
@Data
public class ArBillLog implements Serializable
{
private static final long serialVersionUID = 1L;
private Long logId;
private Long arBillId;
private String objectType;
private String objectId;
/** 对象业务标识,例如账单号、收款单号或核销单号。 */
private String objectDisplay;
private String operationType;
private String operationDesc;
private String fromStatus;
private String toStatus;
private String beforeData;
private String afterData;
private String operatorId;
private String operatorName;
private String operationSource;
private String requestId;
private String traceId;
private String result;
private String errorMessage;
private String clientIp;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,33 @@
package com.ruoyi.ar.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 应收真实收款。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ArReceipt extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long receiptId;
private String receiptNo;
private Long customerId;
/** 客户名称,仅用于列表展示。 */
private String customerName;
/** 客户编码,仅用于列表展示。 */
private String customerCode;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date receiptDate;
private String currency;
private BigDecimal receiptAmount;
private BigDecimal unallocatedAmount;
private String paymentMethod;
private String bankReference;
private String status;
private String remark;
}
@@ -0,0 +1,35 @@
package com.ruoyi.ar.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 收款与账单的核销关系。 */
@Data
public class ArReceiptWriteoff implements Serializable
{
private static final long serialVersionUID = 1L;
private Long writeoffId;
private Long receiptId;
private Long arBillId;
/** 关联账单号,仅用于核销记录展示。 */
private String billNo;
/** 关联收款单号,仅用于核销记录展示。 */
private String receiptNo;
private String writeoffNo;
private BigDecimal writeoffAmount;
private String currency;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date writeoffDate;
private String status;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String cancelBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date cancelTime;
}
@@ -0,0 +1,11 @@
package com.ruoyi.ar.domain.vo;
import lombok.Data;
/** 账单状态动作参数。 */
@Data
public class ArBillActionRequest
{
private String remark;
private String requestId;
}
@@ -0,0 +1,19 @@
package com.ruoyi.ar.domain.vo;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Data;
/** 生成应收账单前的订单计费结果查询条件。 */
@Data
public class ArBillGenerateQuery
{
private Long customerId;
private String customerCode;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date billPeriodEnd;
private String currency;
}
@@ -0,0 +1,34 @@
package com.ruoyi.ar.domain.vo;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/** 从订单计费结果生成正式账单的参数。 */
@Data
public class ArBillGenerateRequest
{
@NotNull(message = "客户不能为空")
private Long customerId;
@NotBlank(message = "账单名称不能为空")
private String billName;
private String customerCode;
private String customerName;
@NotNull(message = "账单截止日不能为空")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date billPeriodEnd;
@NotEmpty(message = "请至少选择一条订单")
private List<String> sourceBillIds;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date billDate;
private String currency = "CNY";
private String bankAccountName;
private String bankAccountNo;
private String bankName;
private String remark;
private String requestId;
}
@@ -0,0 +1,16 @@
package com.ruoyi.ar.domain.vo;
import java.util.Date;
import com.ruoyi.ar.domain.ArReceipt;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 收款查询参数。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ArReceiptQuery extends ArReceipt
{
private static final long serialVersionUID = 1L;
private Date receiptDateStart;
private Date receiptDateEnd;
}
@@ -0,0 +1,35 @@
package com.ruoyi.ar.domain.vo;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/** 批量收款核销参数。 */
@Data
public class ArReceiptWriteoffRequest
{
@NotNull(message = "收款ID不能为空")
private Long receiptId;
@NotEmpty(message = "至少选择一张账单")
@Valid
private List<Allocation> allocations;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date writeoffDate;
private String currency;
private String remark;
private String requestId;
@Data
public static class Allocation
{
@NotNull(message = "账单ID不能为空")
private Long arBillId;
@NotNull(message = "核销金额不能为空")
private BigDecimal amount;
}
}
@@ -0,0 +1,62 @@
package com.ruoyi.ar.enums;
/** 应收账单状态。数据库保存值使用 code,避免业务代码散落状态字符串。 */
public enum ArBillStatus
{
DRAFT("DRAFT", "草稿"),
CONFIRMED("CONFIRMED", "已确认"),
PARTIAL_WRITTEN_OFF("PARTIAL_WRITTEN_OFF", "部分核销"),
WRITTEN_OFF("WRITTEN_OFF", "已核销"),
VOID("VOID", "已作废");
private final String code;
private final String label;
ArBillStatus(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
public boolean canConfirm()
{
return this == DRAFT;
}
public boolean canVoid()
{
return this == DRAFT || this == CONFIRMED;
}
public boolean canWriteOff()
{
return this == CONFIRMED || this == PARTIAL_WRITTEN_OFF;
}
public static ArBillStatus of(String code)
{
for (ArBillStatus status : values())
{
if (status.code.equalsIgnoreCase(code))
{
return status;
}
}
return null;
}
public static boolean is(String code, ArBillStatus status)
{
return status != null && status.code.equalsIgnoreCase(code);
}
}
@@ -0,0 +1,21 @@
package com.ruoyi.ar.enums;
/** 应收账单导出任务状态。 */
public enum ArExportStatus
{
PROCESSING("PROCESSING"),
SUCCESS("SUCCESS"),
FAILED("FAILED");
private final String code;
ArExportStatus(String code)
{
this.code = code;
}
public String getCode()
{
return code;
}
}
@@ -0,0 +1,20 @@
package com.ruoyi.ar.enums;
/** 应收操作日志结果。 */
public enum ArLogResult
{
SUCCESS("SUCCESS"),
FAILED("FAILED");
private final String code;
ArLogResult(String code)
{
this.code = code;
}
public String getCode()
{
return code;
}
}
@@ -0,0 +1,20 @@
package com.ruoyi.ar.enums;
/** 计费订单的应收账单占用状态。 */
public enum ArOrderBillStatus
{
UNBILLED("UNBILLED"),
BILLED("BILLED");
private final String code;
ArOrderBillStatus(String code)
{
this.code = code;
}
public String getCode()
{
return code;
}
}
@@ -0,0 +1,22 @@
package com.ruoyi.ar.enums;
/** 应收收款状态。 */
public enum ArReceiptStatus
{
REGISTERED("REGISTERED"),
PARTIAL_WRITTEN_OFF("PARTIAL_WRITTEN_OFF"),
WRITTEN_OFF("WRITTEN_OFF"),
CANCELLED("CANCELLED");
private final String code;
ArReceiptStatus(String code)
{
this.code = code;
}
public String getCode()
{
return code;
}
}
@@ -0,0 +1,20 @@
package com.ruoyi.ar.enums;
/** 收款核销状态。 */
public enum ArWriteoffStatus
{
EFFECTIVE("EFFECTIVE"),
CANCELLED("CANCELLED");
private final String code;
ArWriteoffStatus(String code)
{
this.code = code;
}
public String getCode()
{
return code;
}
}
@@ -0,0 +1,9 @@
package com.ruoyi.ar.mapper;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.ar.domain.ArBankAccount;
public interface ArBankAccountMapper
{
ArBankAccount selectDefaultByCurrency(@Param("currency") String currency);
}
@@ -0,0 +1,15 @@
package com.ruoyi.ar.mapper;
import java.util.List;
import com.ruoyi.ar.domain.ArBillDetailFee;
public interface ArBillDetailFeeMapper
{
List<ArBillDetailFee> selectByDetailId(Long detailId);
List<ArBillDetailFee> selectByBillId(Long arBillId);
int insert(ArBillDetailFee fee);
int insertBatch(List<ArBillDetailFee> fees);
}
@@ -0,0 +1,16 @@
package com.ruoyi.ar.mapper;
import java.util.List;
import com.ruoyi.ar.domain.ArBillDetail;
public interface ArBillDetailMapper
{
List<ArBillDetail> selectByBillId(Long arBillId);
/** 查询账单明细及费用明细,避免详情页逐层查询。 */
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
int insert(ArBillDetail detail);
int insertBatch(List<ArBillDetail> details);
}
@@ -0,0 +1,15 @@
package com.ruoyi.ar.mapper;
import java.util.List;
import com.ruoyi.ar.domain.ArBillExport;
public interface ArBillExportMapper
{
List<ArBillExport> selectByBillId(Long arBillId);
ArBillExport selectById(Long exportId);
ArBillExport selectLatestSuccessByBillId(Long arBillId);
int insert(ArBillExport export);
}
@@ -0,0 +1,15 @@
package com.ruoyi.ar.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.ar.domain.ArBillLog;
public interface ArBillLogMapper
{
List<ArBillLog> selectList(ArBillLog query);
ArBillLog selectSuccessByRequestId(@Param("requestId") String requestId,
@Param("objectType") String objectType);
int insert(ArBillLog log);
}
@@ -0,0 +1,33 @@
package com.ruoyi.ar.mapper;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.ar.domain.ArBill;
public interface ArBillMapper
{
List<ArBill> selectArBillList(ArBill query);
ArBill selectArBillById(Long arBillId);
ArBill selectArBillByBillNo(String billNo);
Integer selectMaxBillVersion(@Param("customerId") Long customerId,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency);
ArBill selectArBillByIdForUpdate(Long arBillId);
int insertArBill(ArBill bill);
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
@Param("remark") String remark, @Param("operator") String operator,
@Param("fromStatus") String fromStatus);
int updateArBillAmounts(@Param("arBillId") Long arBillId,
@Param("writtenOffAmount") BigDecimal writtenOffAmount,
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
@Param("status") String status, @Param("operator") String operator,
@Param("fromStatus") String fromStatus);
}
@@ -0,0 +1,30 @@
package com.ruoyi.ar.mapper;
import java.math.BigDecimal;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.ar.domain.ArReceipt;
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
public interface ArReceiptMapper
{
List<ArReceipt> selectArReceiptList(ArReceiptQuery query);
ArReceipt selectArReceiptById(Long receiptId);
ArReceipt selectArReceiptByIdForUpdate(Long receiptId);
ArReceipt selectArReceiptByNo(String receiptNo);
ArReceipt selectArReceiptByReference(@Param("paymentMethod") String paymentMethod,
@Param("bankReference") String bankReference);
int insertArReceipt(ArReceipt receipt);
int updateBalanceAndStatus(@Param("receiptId") Long receiptId,
@Param("unallocatedAmount") BigDecimal unallocatedAmount,
@Param("status") String status, @Param("operator") String operator);
int updateCancelled(@Param("receiptId") Long receiptId, @Param("operator") String operator,
@Param("remark") String remark);
}
@@ -0,0 +1,26 @@
package com.ruoyi.ar.mapper;
import java.math.BigDecimal;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.ar.domain.ArReceiptWriteoff;
public interface ArReceiptWriteoffMapper
{
List<ArReceiptWriteoff> selectByBillId(Long arBillId);
List<ArReceiptWriteoff> selectByReceiptId(Long receiptId);
ArReceiptWriteoff selectById(Long writeoffId);
ArReceiptWriteoff selectByIdForUpdate(Long writeoffId);
int insert(ArReceiptWriteoff writeoff);
int updateCancelled(@Param("writeoffId") Long writeoffId, @Param("cancelBy") String cancelBy,
@Param("remark") String remark);
BigDecimal sumEffectiveByBillId(Long arBillId);
BigDecimal sumEffectiveByReceiptId(Long receiptId);
}
@@ -0,0 +1,14 @@
package com.ruoyi.ar.service;
import java.util.List;
import com.ruoyi.ar.domain.ArBillLog;
public interface IArBillLogService
{
List<ArBillLog> selectLogList(ArBillLog query);
void record(ArBillLog log);
void recordFailure(Long arBillId, String objectType, String objectId, String operationType,
String fromStatus, String message, String operator, String requestId);
}
@@ -0,0 +1,36 @@
package com.ruoyi.ar.service;
import java.util.List;
import com.ruoyi.ar.domain.ArBill;
import com.ruoyi.ar.domain.ArBankAccount;
import com.ruoyi.ar.domain.ArBillExport;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
import com.ruoyi.price.domain.PrcOrderBill;
public interface IArBillService
{
List<ArBill> selectArBillList(ArBill query);
ArBill selectArBillById(Long arBillId);
ArBankAccount getDefaultBankAccount(String currency);
List<PrcOrderBill> previewBill(ArBillGenerateQuery query);
ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId);
ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
ArBillExport exportBill(Long arBillId, String operator, String requestId);
List<ArBillExport> selectExports(Long arBillId);
void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception;
List<ArBillLog> selectLogList(ArBillLog query);
}
@@ -0,0 +1,22 @@
package com.ruoyi.ar.service;
import java.util.List;
import com.ruoyi.ar.domain.ArReceipt;
import com.ruoyi.ar.domain.ArReceiptWriteoff;
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest;
public interface IArReceiptService
{
List<ArReceipt> selectReceiptList(ArReceiptQuery query);
List<ArReceiptWriteoff> selectWriteoffs(Long receiptId);
ArReceipt registerReceipt(ArReceipt receipt, String operator, String requestId);
void cancelReceipt(Long receiptId, String remark, String operator, String requestId);
List<ArReceiptWriteoff> writeoff(ArReceiptWriteoffRequest request, String operator, String requestId);
void cancelWriteoff(Long writeoffId, String remark, String operator, String requestId);
}
@@ -0,0 +1,53 @@
package com.ruoyi.ar.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.enums.ArLogResult;
import com.ruoyi.ar.mapper.ArBillLogMapper;
import com.ruoyi.ar.service.IArBillLogService;
import com.ruoyi.common.utils.uuid.IdUtils;
@Service
public class ArBillLogServiceImpl implements IArBillLogService
{
@Autowired
private ArBillLogMapper logMapper;
@Override
public List<ArBillLog> selectLogList(ArBillLog query)
{
return logMapper.selectList(query);
}
@Override
public void record(ArBillLog log)
{
logMapper.insert(log);
}
/** 失败日志必须独立提交,不能随业务异常一起回滚。 */
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordFailure(Long arBillId, String objectType, String objectId, String operationType,
String fromStatus, String message, String operator, String requestId)
{
ArBillLog log = new ArBillLog();
log.setArBillId(arBillId);
log.setObjectType(objectType);
log.setObjectId(objectId);
log.setOperationType(operationType);
log.setOperationDesc(message);
log.setFromStatus(fromStatus);
log.setOperatorId(operator);
log.setOperatorName(operator);
log.setOperationSource("WEB");
log.setRequestId(requestId == null ? IdUtils.fastSimpleUUID() : requestId);
log.setResult(ArLogResult.FAILED.getCode());
log.setErrorMessage(message);
logMapper.insert(log);
}
}
@@ -0,0 +1,902 @@
package com.ruoyi.ar.service.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.ar.domain.ArBill;
import com.ruoyi.ar.domain.ArBankAccount;
import com.ruoyi.ar.domain.ArBillDetail;
import com.ruoyi.ar.domain.ArBillDetailFee;
import com.ruoyi.ar.domain.ArBillExport;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.enums.ArBillStatus;
import com.ruoyi.ar.enums.ArExportStatus;
import com.ruoyi.ar.enums.ArLogResult;
import com.ruoyi.ar.enums.ArOrderBillStatus;
import com.ruoyi.ar.mapper.ArBillDetailFeeMapper;
import com.ruoyi.ar.mapper.ArBillDetailMapper;
import com.ruoyi.ar.mapper.ArBillExportMapper;
import com.ruoyi.ar.mapper.ArBillLogMapper;
import com.ruoyi.ar.mapper.ArBillMapper;
import com.ruoyi.ar.mapper.ArBankAccountMapper;
import com.ruoyi.ar.service.IArBillLogService;
import com.ruoyi.ar.service.IArBillService;
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.price.domain.PrcIncomeBillFee;
import com.ruoyi.price.domain.PrcOrderBill;
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
import com.ruoyi.price.mapper.PrcOrderBillMapper;
@Service
public class ArBillServiceImpl implements IArBillService
{
private static final String EXPORT_TEMPLATE_RESOURCE = "templates/ar/ar_bill_template.xlsx";
private static final int TEMPLATE_DATA_START_ROW = 4;
private static final int TEMPLATE_FOOTER_START_ROW = 5;
private static final int TEMPLATE_COLUMN_COUNT = 20;
@Autowired private ArBillMapper billMapper;
@Autowired private ArBankAccountMapper bankAccountMapper;
@Autowired private ArBillDetailMapper detailMapper;
@Autowired private ArBillDetailFeeMapper detailFeeMapper;
@Autowired private ArBillExportMapper exportMapper;
@Autowired private ArBillLogMapper logMapper;
@Autowired private IArBillLogService logService;
@Autowired private PrcOrderBillMapper sourceBillMapper;
@Autowired private PrcIncomeBillFeeMapper sourceFeeMapper;
@Override
public List<ArBill> selectArBillList(ArBill query)
{
List<ArBill> bills = billMapper.selectArBillList(query);
for (ArBill bill : bills)
{
bill.setBankAccountNo(maskAccount(bill.getBankAccountNo()));
}
return bills;
}
@Override
public ArBill selectArBillById(Long arBillId)
{
ArBill bill = billMapper.selectArBillById(arBillId);
if (bill == null)
{
throw new ServiceException("应收账单不存在");
}
fillDetails(bill);
bill.setBankAccountNo(maskAccount(bill.getBankAccountNo()));
bill.setExports(exportMapper.selectByBillId(arBillId));
return bill;
}
@Override
public ArBankAccount getDefaultBankAccount(String currency)
{
ArBankAccount account = bankAccountMapper.selectDefaultByCurrency(
StringUtils.isEmpty(currency) ? "CNY" : currency);
if (account != null)
{
account.setAccountNo(maskAccount(account.getAccountNo()));
}
return account;
}
@Override
public List<PrcOrderBill> previewBill(ArBillGenerateQuery query)
{
validatePreviewQuery(query);
return sourceBillMapper.selectPrcOrderBillByCustomerPeriod(query.getCustomerId(),
query.getCustomerCode(), query.getBillPeriodEnd(), query.getCurrency(), ArOrderBillStatus.UNBILLED.getCode());
}
@Override
@Transactional(rollbackFor = Exception.class)
public ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId)
{
validatePeriod(request);
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "BILL");
if (previous != null && isNumber(previous.getObjectId()))
{
return selectArBillById(Long.valueOf(previous.getObjectId()));
}
List<String> requestedSourceBillIds = normalizeSourceBillIds(request.getSourceBillIds());
if (requestedSourceBillIds.isEmpty())
{
throw failure(null, "BILL", "", "GENERATE", null, "请至少选择一条有效订单", operator, requestId);
}
String requestedCurrency = StringUtils.isEmpty(request.getCurrency()) ? "CNY" : request.getCurrency();
List<PrcOrderBill> sources = sourceBillMapper.selectPrcOrderBillByCustomerCutoffAndIds(
request.getCustomerId(), request.getCustomerCode(), request.getBillPeriodEnd(), requestedCurrency,
requestedSourceBillIds, ArOrderBillStatus.UNBILLED.getCode());
if (sources == null || sources.isEmpty() || sources.size() != requestedSourceBillIds.size())
{
throw failure(null, "BILL", "", "GENERATE", null,
"部分所选订单已出账、已变更或不符合当前筛选条件,请重新查询后生成", operator, requestId);
}
String currency = requestedCurrency;
BigDecimal total = BigDecimal.ZERO;
for (PrcOrderBill source : sources)
{
if (source.getCurrency() != null && !currency.equalsIgnoreCase(source.getCurrency()))
{
throw failure(null, "BILL", "", "GENERATE", null, "截止日期内存在多个币种,不能合并为一张账单", operator, requestId);
}
total = total.add(nvl(source.getIncomeTotal()));
}
ArBankAccount bankAccount = bankAccountMapper.selectDefaultByCurrency(currency);
if (bankAccount == null)
{
throw failure(null, "BILL", "", "GENERATE", null,
"未配置 " + currency + " 币种的公司默认收款账户,请先完成收款账户配置", operator, requestId);
}
ArBill bill = new ArBill();
bill.setBillNo("AR" + new SimpleDateFormat("yyyyMM").format(new Date())
+ IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase());
bill.setBillName(request.getBillName().trim());
bill.setCustomerId(request.getCustomerId());
bill.setCustomerCode(StringUtils.isEmpty(request.getCustomerCode()) ? sources.get(0).getCustomerCode() : request.getCustomerCode());
bill.setCustomerName(StringUtils.isEmpty(request.getCustomerName()) ? sources.get(0).getCustomerName() : request.getCustomerName());
bill.setBillPeriodStart(earliestBizDate(sources));
bill.setBillPeriodEnd(request.getBillPeriodEnd());
bill.setBillDate(request.getBillDate() == null ? new Date() : request.getBillDate());
bill.setCurrency(currency);
bill.setOrderCount(sources.size());
bill.setTotalAmount(total);
bill.setWrittenOffAmount(BigDecimal.ZERO);
bill.setUnwrittenAmount(total);
bill.setStatus(ArBillStatus.DRAFT.getCode());
bill.setBankAccountName(bankAccount.getAccountName());
bill.setBankAccountNo(bankAccount.getAccountNo());
bill.setBankName(bankAccount.getBankName());
bill.setRemark(request.getRemark());
bill.setCreateBy(operator);
bill.setUpdateBy(operator);
List<String> sourceBillIds = new ArrayList<>();
for (PrcOrderBill source : sources)
{
sourceBillIds.add(source.getBillId());
}
int claimedCount = sourceBillMapper.claimArBillOrders(sourceBillIds, operator,
ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.BILLED.getCode());
if (claimedCount != sourceBillIds.size())
{
throw failure(null, "BILL", "", "GENERATE", null,
"部分计费订单已被其他账单占用,请重新查询后生成", operator, requestId);
}
// 订单占用成功后再锁定版本号,避免并发生成时先查到同一个版本。
Integer maxVersion = billMapper.selectMaxBillVersion(request.getCustomerId(), request.getBillPeriodEnd(), currency);
bill.setBillVersion((maxVersion == null ? 0 : maxVersion) + 1);
billMapper.insertArBill(bill);
List<ArBillDetail> details = new ArrayList<>();
int detailNo = 1;
for (PrcOrderBill source : sources)
{
details.add(toDetail(bill, source, detailNo++, operator));
}
detailMapper.insertBatch(details);
// Batch insert does not need to expose generated IDs to the caller. Re-read the
// persisted details once, then attach source fees in one batch query.
Map<String, ArBillDetail> detailsBySourceBill = new HashMap<>();
for (ArBillDetail detail : detailMapper.selectByBillId(bill.getArBillId()))
{
detailsBySourceBill.put(detail.getPrcBillId(), detail);
}
List<ArBillDetailFee> detailFees = new ArrayList<>();
List<PrcIncomeBillFee> sourceFees = sourceBillIds.isEmpty()
? new ArrayList<PrcIncomeBillFee>() : sourceFeeMapper.selectByBillIds(sourceBillIds);
Map<String, Integer> feeNumbers = new HashMap<>();
for (PrcIncomeBillFee sourceFee : sourceFees)
{
ArBillDetail detail = detailsBySourceBill.get(sourceFee.getBillId());
if (detail == null)
{
throw new ServiceException("来源费用找不到对应的账单明细:" + sourceFee.getBillId());
}
int feeNo = feeNumbers.merge(sourceFee.getBillId(), 1, Integer::sum);
detailFees.add(toDetailFee(detail, sourceFee, feeNo, operator));
}
if (!detailFees.isEmpty())
{
detailFeeMapper.insertBatch(detailFees);
}
recordSuccess(bill, "CREATE", "生成正式账单", null, ArBillStatus.DRAFT.getCode(), requestId, operator);
bill.setBankAccountNo(maskAccount(bill.getBankAccountNo()));
return bill;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId)
{
ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId);
if (bill == null)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", null, "应收账单不存在", operator, requestId);
}
ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus());
if (currentStatus == null || !currentStatus.canConfirm())
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
"只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId);
}
if (!validateDetails(arBillId, bill.getTotalAmount()))
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
"账单费用明细汇总与订单或账单总额不一致", operator, requestId);
}
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(),
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
operator, ArBillStatus.DRAFT.getCode()) != 1)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
"账单状态已发生变化,请刷新后重试", operator, requestId);
}
recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator);
return selectArBillById(arBillId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId)
{
String remark = request == null ? null : request.getRemark();
if (StringUtils.isEmpty(remark))
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", null, "作废必须填写原因", operator, requestId);
}
ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId);
if (bill == null)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", null, "应收账单不存在", operator, requestId);
}
ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus());
if (currentStatus == null || !currentStatus.canVoid())
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
"当前状态不允许作废", operator, requestId);
}
List<String> sourceBillIds = new ArrayList<>();
for (ArBillDetail detail : detailMapper.selectByBillId(arBillId))
{
if (!StringUtils.isEmpty(detail.getPrcBillId()))
{
sourceBillIds.add(detail.getPrcBillId());
}
}
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
"账单状态已发生变化,请刷新后重试", operator, requestId);
}
if (!sourceBillIds.isEmpty())
{
int releasedCount = sourceBillMapper.releaseArBillOrders(sourceBillIds, operator,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
if (releasedCount != sourceBillIds.size())
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
"部分来源订单状态异常,账单未作废,请刷新后重试", operator, requestId);
}
}
recordSuccess(bill, "VOID", "作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, operator);
return selectArBillById(arBillId);
}
@Override
public ArBillExport exportBill(Long arBillId, String operator, String requestId)
{
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "EXPORT");
if (previous != null && isNumber(previous.getObjectId()))
{
ArBillExport previousExport = exportMapper.selectById(Long.valueOf(previous.getObjectId()));
if (previousExport != null)
{
return previousExport;
}
}
ArBill bill = billMapper.selectArBillById(arBillId);
if (bill == null)
{
throw failure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", null, "应收账单不存在", operator, requestId);
}
if (ArBillStatus.is(bill.getStatus(), ArBillStatus.DRAFT) || ArBillStatus.is(bill.getStatus(), ArBillStatus.VOID))
{
throw failure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", bill.getStatus(),
"草稿或作废账单不可导出", operator, requestId);
}
fillDetails(bill);
String fileName = bill.getBillNo() + "_" + safeFilePart(bill.getCustomerName()) + "_"
+ new SimpleDateFormat("yyyyMM").format(bill.getBillPeriodEnd()) + ".xlsx";
ArBillExport export = new ArBillExport();
export.setArBillId(arBillId);
export.setFileName(fileName);
export.setFileType("XLSX");
export.setTemplateCode("AR_CUSTOMER_BILL");
export.setTemplateVersion("1.0");
export.setExportBy(operator);
export.setRowCount(bill.getOrderCount());
try
{
byte[] data = buildWorkbook(bill);
String relative = "ar/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + ".xlsx";
String downloadDir = RuoYiConfig.getProfile() + File.separator + "download";
File file = FileUploadUtils.getAbsoluteFile(downloadDir, relative);
Files.write(file.toPath(), data);
export.setStorageKey(FileUploadUtils.getPathFileName(downloadDir, relative));
export.setFileSize((long) data.length);
export.setFileHash(sha256(data));
export.setExportStatus(ArExportStatus.SUCCESS.getCode());
exportMapper.insert(export);
recordSuccess(bill, "EXPORT", "导出账单 XLSX", bill.getStatus(), bill.getStatus(), requestId, operator, export.getExportId());
return export;
}
catch (Exception e)
{
export.setStorageKey("");
export.setExportStatus(ArExportStatus.FAILED.getCode());
export.setRemark(e.getMessage());
exportMapper.insert(export);
logService.recordFailure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", bill.getStatus(),
e.getMessage(), operator, requestId);
throw new ServiceException("账单导出失败:" + e.getMessage());
}
}
@Override
public List<ArBillExport> selectExports(Long arBillId)
{
return exportMapper.selectByBillId(arBillId);
}
@Override
public void downloadExport(Long exportId, HttpServletResponse response) throws Exception
{
ArBillExport export = exportMapper.selectById(exportId);
if (export == null || !ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey()))
{
throw new ServiceException("导出文件不存在或尚未生成成功");
}
String resource = export.getStorageKey();
if (!FileUtils.checkAllowDownload(resource))
{
throw new ServiceException("导出文件路径非法");
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
FileUtils.setAttachmentResponseHeader(response, export.getFileName());
FileUtils.writeBytes(RuoYiConfig.getProfile() + FileUtils.stripPrefix(resource), response.getOutputStream());
}
@Override
public List<ArBillLog> selectLogList(ArBillLog query)
{
return logService.selectLogList(query);
}
private void fillDetails(ArBill bill)
{
List<ArBillDetail> details = detailMapper.selectByBillIdWithFees(bill.getArBillId());
for (ArBillDetail detail : details)
{
if (detail.getFees() == null)
{
detail.setFees(new ArrayList<>());
}
}
bill.setDetails(details);
}
private boolean validateDetails(Long arBillId, BigDecimal billTotal)
{
List<ArBillDetail> details = detailMapper.selectByBillIdWithFees(arBillId);
BigDecimal detailTotal = BigDecimal.ZERO;
for (ArBillDetail detail : details)
{
BigDecimal feeTotal = BigDecimal.ZERO;
List<ArBillDetailFee> fees = detail.getFees();
if (fees == null)
{
fees = new ArrayList<ArBillDetailFee>();
}
for (ArBillDetailFee fee : fees)
{
feeTotal = feeTotal.add(nvl(fee.getAmount()));
}
if (feeTotal.compareTo(nvl(detail.getTotalAmount())) != 0)
{
return false;
}
detailTotal = detailTotal.add(nvl(detail.getTotalAmount()));
}
return !details.isEmpty() && detailTotal.compareTo(nvl(billTotal)) == 0;
}
private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, int detailNo, String operator)
{
ArBillDetail detail = new ArBillDetail();
detail.setArBillId(bill.getArBillId());
detail.setPrcBillId(source.getBillId());
detail.setDetailNo(detailNo);
detail.setOrderNo(source.getOrderNo());
detail.setTrackingNo(source.getTrackingNo());
detail.setReceiveTime(source.getOrderTime());
detail.setProductCode(source.getProductCode());
detail.setProductName(source.getProductName());
detail.setGoodsDescription(source.getOrderRemark());
detail.setCountryCode(source.getCountryCode());
detail.setCountryName(source.getCountryName());
detail.setDestPostcode(source.getDestPostcode());
detail.setActualWeight(source.getActualWeight());
detail.setVolumeWeight(source.getVolumeWeight());
detail.setChargeWeight(source.getChargeWeight() == null ? source.getWeight() : source.getChargeWeight());
detail.setPkgLength(source.getPkgLength());
detail.setPkgWidth(source.getPkgWidth());
detail.setPkgHeight(source.getPkgHeight());
detail.setCurrency(source.getCurrency() == null ? bill.getCurrency() : source.getCurrency());
detail.setTotalAmount(nvl(source.getIncomeTotal()));
detail.setRemark(source.getOrderRemark());
detail.setCreateBy(operator);
return detail;
}
private ArBillDetailFee toDetailFee(ArBillDetail detail, PrcIncomeBillFee source, int feeNo, String operator)
{
ArBillDetailFee fee = new ArBillDetailFee();
fee.setDetailId(detail.getDetailId());
fee.setPrcFeeId(source.getFeeId());
fee.setFeeNo(feeNo);
fee.setFeeType(source.getFeeType());
fee.setFeeName(source.getFeeName());
fee.setScId(source.getScId());
fee.setAmount(nvl(source.getAmount()));
fee.setCurrency(source.getCurrency() == null ? detail.getCurrency() : source.getCurrency());
fee.setRoundMode(source.getRoundMode());
fee.setRemark(source.getRemark());
fee.setCreateBy(operator);
return fee;
}
private void recordSuccess(ArBill bill, String operationType, String desc, String fromStatus,
String toStatus, String requestId, String operator)
{
recordSuccess(bill, operationType, desc, fromStatus, toStatus, requestId, operator, null);
}
private void recordSuccess(ArBill bill, String operationType, String desc, String fromStatus,
String toStatus, String requestId, String operator, Long objectId)
{
ArBillLog log = new ArBillLog();
log.setArBillId(bill.getArBillId());
log.setObjectType("BILL".equals(operationType) ? "BILL" : operationType.equals("EXPORT") ? "EXPORT" : "BILL");
log.setObjectId(objectId == null ? String.valueOf(bill.getArBillId()) : String.valueOf(objectId));
log.setOperationType(operationType);
log.setOperationDesc(desc);
log.setFromStatus(fromStatus);
log.setToStatus(toStatus);
log.setBeforeData(safeStatusJson(fromStatus));
log.setAfterData(safeStatusJson(toStatus));
log.setOperatorId(operator);
log.setOperatorName(operator);
log.setOperationSource("WEB");
log.setRequestId(requestId);
log.setResult(ArLogResult.SUCCESS.getCode());
logService.record(log);
}
private ServiceException failure(Long arBillId, String objectType, String objectId, String operationType,
String fromStatus, String message, String operator, String requestId)
{
logService.recordFailure(arBillId, objectType, objectId, operationType, fromStatus, message, operator, requestId);
return new ServiceException(message);
}
private byte[] buildWorkbook(ArBill bill) throws IOException
{
ClassPathResource resource = new ClassPathResource(EXPORT_TEMPLATE_RESOURCE);
if (!resource.exists())
{
throw new IOException("应收账单导出模板不存在: " + EXPORT_TEMPLATE_RESOURCE);
}
try (InputStream input = resource.getInputStream(); XSSFWorkbook workbook = new XSSFWorkbook(input))
{
Sheet sheet = workbook.getSheet("8.31");
if (sheet == null)
{
sheet = workbook.getSheetAt(0);
}
Row templateDataRow = sheet.getRow(TEMPLATE_DATA_START_ROW);
List<ArBillDetail> details = bill.getDetails() == null ? new ArrayList<ArBillDetail>() : bill.getDetails();
CellStyle bodyStyle = createTemplateBodyStyle(workbook);
int footerStartRow = TEMPLATE_FOOTER_START_ROW + Math.max(details.size() - 1, 0);
int footerShift = footerStartRow - TEMPLATE_FOOTER_START_ROW;
if (footerShift != 0)
{
sheet.shiftRows(TEMPLATE_FOOTER_START_ROW, sheet.getLastRowNum(), footerShift);
}
set(sheet, 0, 0, StringUtils.isEmpty(bill.getBankAccountName()) ? "威狮国际供应链(深圳)有限公司" : bill.getBankAccountName());
set(sheet, 2, 2, bill.getCustomerName());
set(sheet, 2, 15, exportPeriod(bill));
for (int i = 0; i < details.size(); i++)
{
Row row = sheet.getRow(TEMPLATE_DATA_START_ROW + i);
if (row == null)
{
row = sheet.createRow(TEMPLATE_DATA_START_ROW + i);
copyRowStyle(templateDataRow, row);
}
writeTemplateDetail(row, details.get(i), bodyStyle);
}
int bankRow = footerStartRow;
set(sheet, bankRow, 1, "银行信息");
set(sheet, bankRow + 1, 1, "开户名");
set(sheet, bankRow + 1, 2, bill.getBankAccountName());
set(sheet, bankRow + 2, 1, "账号");
set(sheet, bankRow + 2, 2, exportBankAccountNo(bill.getBankAccountNo()));
set(sheet, bankRow + 3, 1, "开户行");
set(sheet, bankRow + 3, 2, bill.getBankName());
set(sheet, bankRow, 15, "本期应收金额");
set(sheet, bankRow, 18, bill.getTotalAmount());
set(sheet, bankRow + 1, 15, "本期已核销金额");
set(sheet, bankRow + 1, 18, bill.getWrittenOffAmount());
set(sheet, bankRow + 2, 15, "本期未核销金额");
set(sheet, bankRow + 2, 18, bill.getUnwrittenAmount());
return toBytes(workbook);
}
}
private byte[] toBytes(XSSFWorkbook workbook) throws IOException
{
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
workbook.write(out);
return out.toByteArray();
}
private void set(Row row, int index, Object value)
{
Cell cell = row.getCell(index, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
cell.setCellValue("");
if (value == null)
{
return;
}
if (value instanceof Number)
{
cell.setCellValue(((Number) value).doubleValue());
}
else
{
cell.setCellValue(String.valueOf(value));
}
}
private void set(Sheet sheet, int rowIndex, int columnIndex, Object value)
{
Row row = sheet.getRow(rowIndex);
if (row == null)
{
row = sheet.createRow(rowIndex);
}
set(row, columnIndex, value);
}
private void copyRowStyle(Row source, Row target)
{
if (source == null)
{
return;
}
target.setHeight(source.getHeight());
for (int index = 0; index < TEMPLATE_COLUMN_COUNT; index++)
{
Cell sourceCell = source.getCell(index);
if (sourceCell != null)
{
target.getCell(index, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellStyle(sourceCell.getCellStyle());
}
}
}
private CellStyle createTemplateBodyStyle(XSSFWorkbook workbook)
{
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setFontName("微软雅黑");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setWrapText(true);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
private void writeTemplateDetail(Row row, ArBillDetail detail, CellStyle bodyStyle)
{
row.setHeightInPoints(24);
Object[] values = {
detail.getDetailNo(),
detail.getReceiveTime() == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(detail.getReceiveTime()),
detail.getOrderNo(), detail.getTrackingNo(),
StringUtils.isEmpty(detail.getProductName()) ? detail.getProductCode() : detail.getProductName(),
StringUtils.isEmpty(detail.getCountryName()) ? detail.getCountryCode() : detail.getCountryName(),
detail.getDestPostcode(), decimal(detail.getActualWeight(), 3), decimal(detail.getPkgLength(), 2),
decimal(detail.getPkgHeight(), 2), decimal(detail.getPkgWidth(), 2), decimal(detail.getVolumeWeight(), 3),
decimal(detail.getChargeWeight(), 3), exportUnitPrice(detail), exportProcessingFee(detail),
exportFormula(detail), exportFeeTypes(detail), detail.getCurrency(), detail.getTotalAmount(), exportRemark(detail)
};
for (int index = 0; index < values.length; index++)
{
set(row, index, values[index]);
row.getCell(index, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellStyle(bodyStyle);
}
}
private String exportRemark(ArBillDetail detail)
{
return StringUtils.isEmpty(detail.getRemark()) ? detail.getGoodsDescription() : detail.getRemark();
}
private String exportPeriod(ArBill bill)
{
if (bill.getBillPeriodEnd() == null)
{
return "";
}
return "截至 " + new SimpleDateFormat("yyyy-MM-dd").format(bill.getBillPeriodEnd());
}
private BigDecimal exportUnitPrice(ArBillDetail detail)
{
BigDecimal direct = firstFeeUnitPrice(detail.getFees());
if (direct != null)
{
return direct;
}
BigDecimal freight = feeAmount(detail, "BASIC_FREIGHT");
if (freight == null || detail.getChargeWeight() == null || detail.getChargeWeight().compareTo(BigDecimal.ZERO) <= 0)
{
return null;
}
return freight.divide(detail.getChargeWeight(), 4, java.math.RoundingMode.HALF_UP);
}
private BigDecimal firstFeeUnitPrice(List<ArBillDetailFee> fees)
{
if (fees == null)
{
return null;
}
for (ArBillDetailFee fee : fees)
{
if (fee.getUnitPrice() != null)
{
return fee.getUnitPrice();
}
}
return null;
}
private BigDecimal exportProcessingFee(ArBillDetail detail)
{
BigDecimal total = BigDecimal.ZERO;
boolean found = false;
if (detail.getFees() != null)
{
for (ArBillDetailFee fee : detail.getFees())
{
String name = fee.getFeeName() == null ? "" : fee.getFeeName();
if ("REGISTRATION_FEE".equalsIgnoreCase(fee.getFeeType()) || name.contains("处理费"))
{
total = total.add(nvl(fee.getAmount()));
found = true;
}
}
}
return found ? total : null;
}
private String exportFormula(ArBillDetail detail)
{
String formula = joinFeeText(detail.getFees(), true);
if (!StringUtils.isEmpty(formula))
{
return formula;
}
boolean freight = feeAmount(detail, "BASIC_FREIGHT") != null;
boolean processing = exportProcessingFee(detail) != null;
boolean surcharge = hasFeeType(detail.getFees(), "SURCHARGE");
if (!freight)
{
return "";
}
return "结算重量*公斤价" + (processing ? "+处理费" : "") + (surcharge ? "+附加费" : "");
}
private String exportFeeTypes(ArBillDetail detail)
{
return joinFeeText(detail.getFees(), false);
}
private String joinFeeText(List<ArBillDetailFee> fees, boolean formula)
{
if (fees == null || fees.isEmpty())
{
return "";
}
StringBuilder result = new StringBuilder();
for (ArBillDetailFee fee : fees)
{
String value = formula ? fee.getCalculationFormula() : (StringUtils.isEmpty(fee.getFeeName()) ? fee.getFeeType() : fee.getFeeName());
if (StringUtils.isEmpty(value))
{
continue;
}
if (result.length() > 0)
{
result.append("、");
}
result.append(value);
}
return result.toString();
}
private BigDecimal feeAmount(ArBillDetail detail, String feeType)
{
if (detail.getFees() == null)
{
return null;
}
BigDecimal total = BigDecimal.ZERO;
boolean found = false;
for (ArBillDetailFee fee : detail.getFees())
{
if (feeType.equalsIgnoreCase(fee.getFeeType()))
{
total = total.add(nvl(fee.getAmount()));
found = true;
}
}
return found ? total : null;
}
private boolean hasFeeType(List<ArBillDetailFee> fees, String feeType)
{
if (fees == null)
{
return false;
}
for (ArBillDetailFee fee : fees)
{
if (feeType.equalsIgnoreCase(fee.getFeeType()))
{
return true;
}
}
return false;
}
private String exportBankAccountNo(String value)
{
if (StringUtils.isEmpty(value))
{
return "";
}
String normalized = value.replaceAll("\\s+", "");
StringBuilder result = new StringBuilder(normalized.length() + normalized.length() / 4);
for (int index = 0; index < normalized.length(); index++)
{
if (index > 0 && index % 4 == 0)
{
result.append(' ');
}
result.append(normalized.charAt(index));
}
return result.toString();
}
private BigDecimal decimal(BigDecimal value, int scale) { return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP); }
private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
private String safeFilePart(String value) { return StringUtils.isEmpty(value) ? "客户" : value.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); }
private String maskAccount(String value) { if (StringUtils.isEmpty(value)) return ""; return value.length() <= 4 ? "****" : "****" + value.substring(value.length() - 4); }
private String sha256(byte[] data) throws NoSuchAlgorithmException { byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); }
private boolean isNumber(String value) { if (StringUtils.isEmpty(value)) return false; try { Long.valueOf(value); return true; } catch (NumberFormatException e) { return false; } }
private String safeStatusJson(String status) { Map<String, String> data = new HashMap<>(); data.put("status", status); return JSON.toJSONString(data); }
private void validatePeriod(ArBillGenerateRequest request)
{
if (request == null || request.getCustomerId() == null || StringUtils.isEmpty(request.getBillName())
|| request.getBillName().trim().isEmpty()
|| request.getBillPeriodEnd() == null || request.getSourceBillIds() == null || request.getSourceBillIds().isEmpty())
{
throw new ServiceException("客户、账单名称、账单截止日和订单选择不能为空");
}
}
private void validatePreviewQuery(ArBillGenerateQuery query)
{
if (query == null || query.getCustomerId() == null || query.getBillPeriodEnd() == null)
{
throw new ServiceException("客户和账单截止日不能为空");
}
}
private Date earliestBizDate(List<PrcOrderBill> sources)
{
Date earliest = null;
for (PrcOrderBill source : sources)
{
if (source.getBizDate() != null && (earliest == null || source.getBizDate().before(earliest)))
{
earliest = source.getBizDate();
}
}
return earliest == null ? new Date() : earliest;
}
private List<String> normalizeSourceBillIds(List<String> sourceBillIds)
{
LinkedHashSet<String> uniqueIds = new LinkedHashSet<>();
if (sourceBillIds != null)
{
for (String billId : sourceBillIds)
{
if (!StringUtils.isEmpty(billId) && billId.trim().length() <= 36)
{
uniqueIds.add(billId.trim());
}
}
}
return new ArrayList<>(uniqueIds);
}
}
@@ -0,0 +1,275 @@
package com.ruoyi.ar.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.ar.domain.ArBill;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.domain.ArReceipt;
import com.ruoyi.ar.domain.ArReceiptWriteoff;
import com.ruoyi.ar.domain.vo.ArReceiptQuery;
import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest;
import com.ruoyi.ar.enums.ArBillStatus;
import com.ruoyi.ar.enums.ArLogResult;
import com.ruoyi.ar.enums.ArReceiptStatus;
import com.ruoyi.ar.enums.ArWriteoffStatus;
import com.ruoyi.ar.mapper.ArBillMapper;
import com.ruoyi.ar.mapper.ArBillLogMapper;
import com.ruoyi.ar.mapper.ArReceiptMapper;
import com.ruoyi.ar.mapper.ArReceiptWriteoffMapper;
import com.ruoyi.ar.service.IArBillLogService;
import com.ruoyi.ar.service.IArReceiptService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
@Service
public class ArReceiptServiceImpl implements IArReceiptService
{
@Autowired private ArReceiptMapper receiptMapper;
@Autowired private ArReceiptWriteoffMapper writeoffMapper;
@Autowired private ArBillMapper billMapper;
@Autowired private ArBillLogMapper logMapper;
@Autowired private IArBillLogService logService;
@Override
public List<ArReceipt> selectReceiptList(ArReceiptQuery query)
{
return receiptMapper.selectArReceiptList(query);
}
@Override
public List<ArReceiptWriteoff> selectWriteoffs(Long receiptId)
{
return writeoffMapper.selectByReceiptId(receiptId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ArReceipt registerReceipt(ArReceipt receipt, String operator, String requestId)
{
if (receipt == null || receipt.getCustomerId() == null || receipt.getReceiptDate() == null
|| receipt.getReceiptAmount() == null || receipt.getReceiptAmount().compareTo(BigDecimal.ZERO) <= 0)
{
throw failure(null, "RECEIPT", "REGISTER", "收款客户、日期和正数金额不能为空", operator, requestId);
}
if (StringUtils.isEmpty(receipt.getReceiptNo()))
{
receipt.setReceiptNo("RC" + DateUtils.dateTimeNow("yyyyMMddHHmmss") + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase());
}
if (StringUtils.isEmpty(receipt.getCurrency())) receipt.setCurrency("CNY");
if (StringUtils.isEmpty(receipt.getPaymentMethod())) receipt.setPaymentMethod("BANK");
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "RECEIPT");
if (previous != null && isNumber(previous.getObjectId()))
{
ArReceipt existed = receiptMapper.selectArReceiptById(Long.valueOf(previous.getObjectId()));
if (existed != null) return existed;
}
if (receiptMapper.selectArReceiptByNo(receipt.getReceiptNo()) != null)
{
throw failure(null, "RECEIPT", receipt.getReceiptNo(), "收款单号已存在", operator, requestId);
}
if (!StringUtils.isEmpty(receipt.getBankReference())
&& receiptMapper.selectArReceiptByReference(receipt.getPaymentMethod(), receipt.getBankReference()) != null)
{
throw failure(null, "RECEIPT", receipt.getReceiptNo(), "银行流水号已存在", operator, requestId);
}
receipt.setUnallocatedAmount(receipt.getReceiptAmount());
receipt.setStatus(ArReceiptStatus.REGISTERED.getCode());
receipt.setCreateBy(operator);
receipt.setUpdateBy(operator);
try
{
receiptMapper.insertArReceipt(receipt);
}
catch (DuplicateKeyException e)
{
ArReceipt existed = receiptMapper.selectArReceiptByNo(receipt.getReceiptNo());
if (existed != null) return existed;
throw e;
}
ArBillLog log = new ArBillLog();
log.setObjectType("RECEIPT"); log.setObjectId(String.valueOf(receipt.getReceiptId()));
log.setOperationType("RECEIPT"); log.setOperationDesc("登记应收收款");
log.setOperatorId(operator); log.setOperatorName(operator); log.setOperationSource("WEB");
log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log);
return receipt;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelReceipt(Long receiptId, String remark, String operator, String requestId)
{
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "RECEIPT");
if (previous != null && "CANCEL_RECEIPT".equals(previous.getOperationType())) return;
ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(receiptId);
if (receipt == null) throw failure(null, "RECEIPT", String.valueOf(receiptId), "收款不存在", operator, requestId);
if (!ArReceiptStatus.REGISTERED.getCode().equals(receipt.getStatus())
|| nvl(receipt.getUnallocatedAmount()).compareTo(nvl(receipt.getReceiptAmount())) != 0)
{
throw failure(null, "RECEIPT", String.valueOf(receiptId), "收款已发生核销,不能直接取消", operator, requestId);
}
receiptMapper.updateCancelled(receiptId, operator, StringUtils.isEmpty(remark) ? "取消收款" : remark);
ArBillLog log = new ArBillLog();
log.setObjectType("RECEIPT"); log.setObjectId(String.valueOf(receiptId)); log.setOperationType("CANCEL_RECEIPT");
log.setOperationDesc(StringUtils.isEmpty(remark) ? "取消收款" : remark); log.setFromStatus(ArReceiptStatus.REGISTERED.getCode());
log.setToStatus(ArReceiptStatus.CANCELLED.getCode()); log.setOperatorId(operator); log.setOperatorName(operator);
log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log);
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<ArReceiptWriteoff> writeoff(ArReceiptWriteoffRequest request, String operator, String requestId)
{
if (request == null || request.getReceiptId() == null || request.getAllocations() == null || request.getAllocations().isEmpty())
{
throw failure(null, "WRITEOFF", "", "核销收款和账单不能为空", operator, requestId);
}
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "WRITEOFF");
if (previous != null && isNumber(previous.getObjectId()))
{
ArReceiptWriteoff existed = writeoffMapper.selectById(Long.valueOf(previous.getObjectId()));
if (existed != null) return writeoffMapper.selectByReceiptId(existed.getReceiptId());
}
ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(request.getReceiptId());
if (receipt == null) throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款不存在", operator, requestId);
if (ArReceiptStatus.CANCELLED.getCode().equals(receipt.getStatus()))
{
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款已取消,不允许核销", operator, requestId);
}
String currency = StringUtils.isEmpty(request.getCurrency()) ? receipt.getCurrency() : request.getCurrency();
if (!currency.equalsIgnoreCase(receipt.getCurrency()))
{
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款与核销币种不一致", operator, requestId);
}
BigDecimal allocationTotal = BigDecimal.ZERO;
for (ArReceiptWriteoffRequest.Allocation allocation : request.getAllocations())
{
if (allocation.getAmount() == null || allocation.getAmount().compareTo(BigDecimal.ZERO) <= 0)
{
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "核销金额必须大于零", operator, requestId);
}
allocationTotal = allocationTotal.add(allocation.getAmount());
}
if (allocationTotal.compareTo(nvl(receipt.getUnallocatedAmount())) > 0)
{
throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "核销金额超过收款未分配余额", operator, requestId);
}
List<ArReceiptWriteoff> result = new ArrayList<>();
BigDecimal remainingReceipt = nvl(receipt.getUnallocatedAmount());
Date writeoffDate = request.getWriteoffDate() == null ? new Date() : request.getWriteoffDate();
for (ArReceiptWriteoffRequest.Allocation allocation : request.getAllocations())
{
ArBill bill = billMapper.selectArBillByIdForUpdate(allocation.getArBillId());
if (bill == null) throw failure(null, "WRITEOFF", String.valueOf(allocation.getArBillId()), "账单不存在", operator, requestId);
ArBillStatus billStatus = ArBillStatus.of(bill.getStatus());
if (billStatus == ArBillStatus.VOID) throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单已作废,不允许核销", operator, requestId);
if (billStatus == null || !billStatus.canWriteOff())
{
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单尚未确认,不允许核销", operator, requestId);
}
if (!currency.equalsIgnoreCase(bill.getCurrency())) throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "收款与账单币种不一致", operator, requestId);
BigDecimal amount = allocation.getAmount();
if (amount.compareTo(nvl(bill.getUnwrittenAmount())) > 0)
{
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "核销金额超过账单未核销余额", operator, requestId);
}
ArReceiptWriteoff writeoff = new ArReceiptWriteoff();
writeoff.setReceiptId(receipt.getReceiptId()); writeoff.setArBillId(bill.getArBillId());
writeoff.setWriteoffNo("WO" + DateUtils.dateTimeNow("yyyyMMddHHmmss") + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase());
writeoff.setWriteoffAmount(amount); writeoff.setCurrency(currency); writeoff.setWriteoffDate(writeoffDate);
writeoff.setStatus(ArWriteoffStatus.EFFECTIVE.getCode()); writeoff.setRemark(request.getRemark()); writeoff.setCreateBy(operator);
writeoffMapper.insert(writeoff);
remainingReceipt = remainingReceipt.subtract(amount);
BigDecimal written = nvl(bill.getWrittenOffAmount()).add(amount);
BigDecimal unwritten = nvl(bill.getUnwrittenAmount()).subtract(amount);
String nextBillStatus = billStatus(written, nvl(bill.getTotalAmount()), bill.getStatus());
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, unwritten,
nextBillStatus, operator, bill.getStatus()) != 1)
{
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单状态已发生变化,请刷新后重试", operator, requestId);
}
writeoffLog(writeoff, bill, operator, requestId);
result.add(writeoff);
}
receiptMapper.updateBalanceAndStatus(receipt.getReceiptId(), remainingReceipt,
remainingReceipt.compareTo(BigDecimal.ZERO) == 0 ? ArReceiptStatus.WRITTEN_OFF.getCode() : ArReceiptStatus.PARTIAL_WRITTEN_OFF.getCode(), operator);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelWriteoff(Long writeoffId, String remark, String operator, String requestId)
{
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "WRITEOFF");
if (previous != null && "CANCEL_WRITEOFF".equals(previous.getOperationType())) return;
ArReceiptWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
if (writeoff == null) throw failure(null, "WRITEOFF", String.valueOf(writeoffId), "核销记录不存在", operator, requestId);
if (!ArWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) throw failure(writeoff.getArBillId(), "WRITEOFF", String.valueOf(writeoffId), "只有有效核销可以取消", operator, requestId);
ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(writeoff.getReceiptId());
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
if (receipt == null || bill == null) throw failure(writeoff.getArBillId(), "WRITEOFF", String.valueOf(writeoffId), "核销关联的收款或账单不存在", operator, requestId);
writeoffMapper.updateCancelled(writeoffId, operator, StringUtils.isEmpty(remark) ? "取消核销" : remark);
BigDecimal receiptUnallocated = nvl(receipt.getUnallocatedAmount()).add(writeoff.getWriteoffAmount());
receiptMapper.updateBalanceAndStatus(receipt.getReceiptId(), receiptUnallocated,
receiptUnallocated.compareTo(nvl(receipt.getReceiptAmount())) == 0 ? ArReceiptStatus.REGISTERED.getCode() : ArReceiptStatus.PARTIAL_WRITTEN_OFF.getCode(), operator);
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(writeoff.getWriteoffAmount());
if (written.compareTo(BigDecimal.ZERO) < 0) written = BigDecimal.ZERO;
BigDecimal unwritten = nvl(bill.getTotalAmount()).subtract(written);
String nextBillStatus = billStatus(written, nvl(bill.getTotalAmount()), bill.getStatus());
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, unwritten,
nextBillStatus, operator, bill.getStatus()) != 1)
{
throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单状态已发生变化,请刷新后重试", operator, requestId);
}
ArBillLog log = new ArBillLog();
log.setArBillId(bill.getArBillId()); log.setObjectType("WRITEOFF"); log.setObjectId(String.valueOf(writeoffId));
log.setOperationType("CANCEL_WRITEOFF"); log.setOperationDesc(StringUtils.isEmpty(remark) ? "取消核销" : remark);
log.setFromStatus(ArWriteoffStatus.EFFECTIVE.getCode()); log.setToStatus(ArWriteoffStatus.CANCELLED.getCode()); log.setOperatorId(operator); log.setOperatorName(operator);
log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log);
}
private void writeoffLog(ArReceiptWriteoff writeoff, ArBill bill, String operator, String requestId)
{
ArBillLog log = new ArBillLog();
log.setArBillId(bill.getArBillId()); log.setObjectType("WRITEOFF"); log.setObjectId(String.valueOf(writeoff.getWriteoffId()));
log.setOperationType("WRITEOFF"); log.setOperationDesc("收款核销到账单"); log.setFromStatus(bill.getStatus());
log.setToStatus(billStatus(nvl(bill.getWrittenOffAmount()).add(writeoff.getWriteoffAmount()), nvl(bill.getTotalAmount()), bill.getStatus()));
log.setOperatorId(operator); log.setOperatorName(operator); log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode());
logService.record(log);
}
private String billStatus(BigDecimal written, BigDecimal total, String current)
{
if (written.compareTo(BigDecimal.ZERO) == 0)
{
return ArBillStatus.DRAFT.getCode().equals(current)
? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode();
}
return written.compareTo(total) >= 0
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
}
private ServiceException failure(Long billId, String objectType, String message, String operator, String requestId)
{
logService.recordFailure(billId, objectType, objectType, "RECEIPT", null, message, operator, requestId);
return new ServiceException(message);
}
private ServiceException failure(Long billId, String objectType, String objectId, String message, String operator, String requestId)
{
logService.recordFailure(billId, objectType, objectId,
"RECEIPT".equals(objectType) ? "RECEIPT" : "WRITEOFF", null, message, operator, requestId);
return new ServiceException(message);
}
private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
private boolean isNumber(String value) { if (StringUtils.isEmpty(value)) return false; try { Long.valueOf(value); return true; } catch (NumberFormatException e) { return false; } }
}
@@ -71,6 +71,8 @@ public class PrcOrderBill extends BaseEntity
/** payable total */
private BigDecimal payTotal;
private String currency;
/** 应收账单占用状态:UNBILLED/BILLED。 */
private String arBillStatus;
private String roundMode;
private List<PrcIncomeBillFee> incomeFees;
@@ -9,6 +9,8 @@ public interface PrcIncomeBillFeeMapper
{
public List<PrcIncomeBillFee> selectByBillId(String billId);
public List<PrcIncomeBillFee> selectByBillIds(@Param("billIds") List<String> billIds);
public int insertPrcIncomeBillFee(PrcIncomeBillFee fee);
public int insertBatch(@Param("list") List<PrcIncomeBillFee> fees);
@@ -1,6 +1,7 @@
package com.ruoyi.price.mapper;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.price.domain.PrcOrderBill;
@@ -14,6 +15,25 @@ public interface PrcOrderBillMapper
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
/** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
@Param("currency") String currency, @Param("arBillStatus") String arBillStatus);
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
@Param("currency") String currency, @Param("billIds") List<String> billIds,
@Param("arBillStatus") String arBillStatus);
/** 原子占用订单,防止同一订单被并发生成到多张应收账单。 */
public int claimArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
/** 作废账单后释放订单,允许后续重新生成新版本账单。 */
public int releaseArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
public int insertPrcOrderBill(PrcOrderBill bill);
public int updatePrcOrderBill(PrcOrderBill bill);