应付配置增加附加费配置,应付账单新增核销、对账、凭证、审核功能

This commit is contained in:
liuminghui
2026-11-12 18:20:32 +08:00
parent 1ba431540d
commit 6675808dfc
55 changed files with 5898 additions and 185 deletions
@@ -33,9 +33,23 @@ public class PayBill
private BigDecimal totalAmount;
/** DRAFT CONFIRMED PAID VOID */
private BigDecimal supplierAmount;
private BigDecimal diffAmount;
private BigDecimal paidAmount;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date dueDate;
/** DRAFT PENDING CONFIRMED PARTIAL PAID RED VOID */
private String status;
/** BLUE 蓝字,RED 红字 */
private String billKind;
private Long originalBillId;
private String voidReason;
private String voidBy;
@@ -43,6 +57,29 @@ public class PayBill
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date voidTime;
private String confirmBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date confirmTime;
/** 0.06 表示 6% */
private BigDecimal taxRate;
private BigDecimal amountNet;
private BigDecimal taxAmount;
private BigDecimal exchangeRate;
private BigDecimal baseAmount;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date rateDate;
private Integer termDays;
private String submitBy;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@@ -58,4 +95,10 @@ public class PayBill
private List<PayBillLine> lines;
private List<PayBillLog> logs;
private List<PayBillDiff> diffs;
private List<PayBillFile> files;
private List<PayBillApproval> approvals;
}
@@ -0,0 +1,24 @@
package com.bms.price.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应付账单审核记录。 */
@Data
public class PayBillApproval
{
private Long approvalId;
private Long billId;
/** SUBMIT APPROVE REJECT */
private String action;
private String opinion;
private String operatorName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
@@ -32,4 +32,34 @@ public class PayBillCommand
/** 要作废的应付账单ID */
private Long billId;
/** 到期日,yyyy-MM-dd */
private String dueDate;
/** 账单费用行。登记对方金额时使用 */
private java.util.List<PayBillLine> lines;
/** 要处理的账单行 */
private Long lineId;
/** 对方金额 */
private java.math.BigDecimal supplierAmount;
/** OURS 接受我方,THEIRS 接受对方 */
private String handleType;
/** 差异原因或红字原因 */
private String reason;
/** 驳回意见 */
private String opinion;
/** 税率,0.06 表示 6% */
private java.math.BigDecimal taxRate;
/** 折人民币汇率 */
private java.math.BigDecimal exchangeRate;
/** 汇率日期,yyyy-MM-dd */
private String rateDate;
}
@@ -0,0 +1,37 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应付账单上的一行对账差异。 */
@Data
public class PayBillDiff
{
private Long diffId;
private Long billId;
private Long lineId;
private String feeId;
private BigDecimal ourAmount;
private BigDecimal supplierAmount;
private BigDecimal diffAmount;
private String reason;
/** OPEN ACCEPT_OURS ACCEPT_THEIRS */
private String handleType;
private String adjustFeeId;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
@@ -0,0 +1,50 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应付凭证。发票、对账单或费用通知挂在应付账单上。 */
@Data
public class PayBillFile
{
private Long fileId;
private Long billId;
private String billNo;
private String supplierName;
/** INVOICE STATEMENT DEBIT OTHER */
private String fileType;
private String invoiceNo;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date invoiceDate;
private BigDecimal amount;
private BigDecimal taxAmount;
private BigDecimal amountNet;
private String sellerName;
private BigDecimal matchedAmount;
private String currency;
private String fileName;
private String fileUrl;
private String deleted;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
@@ -13,6 +13,8 @@ public class PayBillLine
private Long billId;
private String feeId;
private String orderBillId;
private String orderNo;
@@ -23,8 +25,27 @@ public class PayBillLine
private String feeName;
private String feeSource;
private String feeLevel;
private BigDecimal amount;
private BigDecimal supplierAmount;
private BigDecimal diffAmount;
/** NONE OPEN ACCEPT_OURS ACCEPT_THEIRS */
private String diffStatus;
private BigDecimal taxRate;
private BigDecimal amountNet;
private BigDecimal taxAmount;
private BigDecimal baseAmount;
private String currency;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@@ -1,5 +1,6 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@@ -12,6 +13,14 @@ public class PayBillLog
private Long billId;
private String bizType;
private Long bizId;
private Long lineId;
private String feeId;
private String operationType;
private String fromStatus;
@@ -20,6 +29,10 @@ public class PayBillLog
private String operationDesc;
private BigDecimal amountBefore;
private BigDecimal amountAfter;
private String operatorName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@@ -45,4 +45,10 @@ public class PayBillOrder
/** 已挂在未作废账单上时的账单号 */
private String occupiedBillNo;
/** 未删除费用行数 */
private Integer feeCount;
/** 还没进入未作废账单的费用行数 */
private Integer unbilledCount;
}
@@ -0,0 +1,57 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应付付款单。一张付款可以核销多张已确认账单。 */
@Data
public class PayPayment
{
private Long paymentId;
private String paymentNo;
private Long supplierId;
private String supplierName;
private String currency;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date payDate;
/** BANK CASH OTHER */
private String payMethod;
private String bankAccount;
private BigDecimal amount;
private BigDecimal bankFee;
private BigDecimal exchangeRate;
private BigDecimal baseAmount;
private BigDecimal allocAmount;
/** DRAFT PAID VOID REFUNDED */
private String status;
private String remark;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private String confirmBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date confirmTime;
private List<PayPaymentAlloc> allocs;
}
@@ -0,0 +1,31 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import lombok.Data;
/** 应付付款核销到一张账单的金额。 */
@Data
public class PayPaymentAlloc
{
private Long allocId;
private Long paymentId;
private Long billId;
private String billNo;
private BigDecimal allocAmount;
/** ACTIVE REVERSED */
private String allocStatus;
/** 账单未核销金额,只在查询未付账单时带出 */
private BigDecimal openAmount;
private String currency;
private Long supplierId;
private String supplierName;
}
@@ -151,5 +151,8 @@ public class PayProduct extends BaseEntity
private List<PaySurcharge> surcharges;
/** 这一版附加费配置。只在计费时带上,不入库 */
private List<PaySurchargeRule> billingRules;
private List<PayExtraLine> extraLines;
}
@@ -0,0 +1,54 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/**
* 应付附加费计费条件。同一产品版本的全部条件都在这张表,用 calcRule 区分列表。
*/
@Data
public class PaySurchargeRule
{
private Long ruleId;
/** 这一版应付产品 */
private Long productId;
/** 价卡版本周期 */
private Long termId;
private Integer versionNo;
/** ALWAYS、POSTCODE、ZONE、EXTRA_KG、WEIGHT、SIZE、RETURN */
private String calcRule;
/** 1开 0关 */
private String enabled;
/** 条件摘要,由明细生成 */
private String calcParam;
/** 条件明细 JSON。接口不直接收发这一列,用 detail */
@JsonIgnore
private String ruleDetail;
/** 这一条规则的结构化参数。超尺寸含长宽高、围长、体积、加收比例和命中方式 */
private Map<String, Object> detail;
/** 续重行的邮编 */
private String postcodeParam;
private String zoneName;
private BigDecimal weightMin;
private BigDecimal weightMax;
private Integer sortNo;
private String createBy;
private String updateBy;
}
@@ -0,0 +1,28 @@
package com.bms.price.domain;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 把同一套计费条件写到若干产品的当前版本。每个版本只保留这一份。
*/
@Data
public class PaySurchargeRuleSave
{
private List<PaySurchargeRuleBind> products = new ArrayList<>();
private List<PaySurchargeRule> rules = new ArrayList<>();
@Data
public static class PaySurchargeRuleBind
{
private Long productId;
private Long termId;
private Integer versionNo;
private String productName;
}
}
@@ -35,6 +35,14 @@ public class PrcPayBillFee implements Serializable
/** 是否删除:0未删除 1已删除 */
private String isDeleted;
/** 已进入的应付账单行。空表示还没出账 */
private Long statementLineId;
/** 结算供应商。空表示还没标明付款对象 */
private Long settleSupplierId;
private String settleSupplierName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -3,9 +3,13 @@ package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayBill;
import com.bms.price.domain.PayBillApproval;
import com.bms.price.domain.PayBillDiff;
import com.bms.price.domain.PayBillFile;
import com.bms.price.domain.PayBillLine;
import com.bms.price.domain.PayBillLog;
import com.bms.price.domain.PayBillOrder;
import com.bms.price.domain.PayPaymentAlloc;
import com.bms.price.domain.PrcPayBillFee;
/** 供应商应付账单 pay_bill 及其明细、日志。 */
@@ -63,6 +67,22 @@ public interface PayBillMapper
*/
List<PrcPayBillFee> selectOrderFees(@Param("billIds") List<String> billIds);
/**
* 读取还没被未作废账单占用的费用。旧账单没有费用行号时,整票视为已占用。
*
* @param billIds 计费订单ID
* @return 可抄进本次账单的费用
*/
List<PrcPayBillFee> selectFreeFees(@Param("billIds") List<String> billIds);
/**
* 这票订单还有没有未删除的计算费用。
*
* @param billId 计费订单ID
* @return 计算费用条数
*/
int countCalcFees(@Param("billId") String billId);
/**
* 插入一张草稿应付账单。
*
@@ -87,6 +107,103 @@ public interface PayBillMapper
*/
int insertLines(@Param("list") List<PayBillLine> list);
/**
* 写入一条账单费用快照,并回填行号。
*
* @param line 费用行
* @return 影响行数
*/
int insertLine(PayBillLine line);
/**
* 把费用明细占用到刚写入的账单行。已被占用时影响行数为 0。
*
* @param feeId 费用明细ID
* @param lineId 账单行ID
* @return 影响行数
*/
int bindFeeLine(@Param("feeId") String feeId, @Param("lineId") Long lineId);
/**
* 释放一张账单占用的费用明细。
*
* @param billId 应付账单ID
* @return 影响行数
*/
int clearFeeLines(@Param("billId") Long billId);
/**
* 释放某一条账单行占用的费用明细。
*
* @param lineId 账单行ID
* @return 影响行数
*/
int clearFeeLine(@Param("lineId") Long lineId);
/**
* 按费用行重算账单金额、订单数,并写回状态。
*
* @param bill 账单ID、金额、状态和更新人
* @return 影响行数
*/
int updatePayBillMoney(PayBill bill);
/**
* 删除草稿中的一条费用快照。
*
* @param billId 应付账单ID
* @param lineId 账单行ID
* @return 影响行数
*/
int deleteLine(@Param("billId") Long billId, @Param("lineId") Long lineId);
int updateLineAmount(PayBillLine line);
int deleteOpenDiff(@Param("lineId") Long lineId);
/**
* 读取一张账单的差异、凭证和审核。
*
* @param billId 应付账单ID
* @return 对应记录
*/
List<PayBillDiff> selectDiffs(@Param("billId") Long billId);
List<PayBillFile> selectFiles(@Param("billId") Long billId);
List<PayBillApproval> selectApprovals(@Param("billId") Long billId);
PayBillLine selectLine(@Param("lineId") Long lineId);
PayBillDiff selectOpenDiff(@Param("lineId") Long lineId);
int insertDiff(PayBillDiff diff);
int updateDiff(PayBillDiff diff);
int deleteDiffsByLine(@Param("lineId") Long lineId);
int insertFile(PayBillFile file);
PayBillFile selectFile(@Param("fileId") Long fileId);
int markFileDeleted(@Param("fileId") Long fileId);
int insertApproval(PayBillApproval approval);
List<PayBillFile> selectFileList(PayBillFile query);
int countActiveFiles(@Param("billId") Long billId);
/**
* 查询可以核销的账单。已确认或部分付款,且还有未付金额。
*
* @param supplierId 供应商
* @param currency 币种
* @return 未付账单
*/
List<PayPaymentAlloc> selectOpenBills(@Param("supplierId") Long supplierId, @Param("currency") String currency);
/**
* 写入一条账单操作日志。
*
@@ -0,0 +1,28 @@
package com.bms.price.mapper;
import java.math.BigDecimal;
import java.util.Date;
import org.apache.ibatis.annotations.Param;
/** 应付价税、发票勾稽和费用付款对象。 */
public interface PayFinanceMapper
{
int updateFinanceHead(@Param("billId") Long billId, @Param("taxRate") BigDecimal taxRate,
@Param("exchangeRate") BigDecimal exchangeRate, @Param("rateDate") Date rateDate);
int updateSubmitBy(@Param("billId") Long billId, @Param("submitBy") String submitBy);
int updateLineTax(@Param("billId") Long billId, @Param("taxRate") BigDecimal taxRate,
@Param("exchangeRate") BigDecimal exchangeRate);
int countDuplicateInvoice(@Param("supplierId") Long supplierId, @Param("invoiceNo") String invoiceNo,
@Param("fileId") Long fileId);
int markInvoiceMatched(@Param("billId") Long billId);
Long selectLadingSupplier(@Param("ladingNo") String ladingNo);
int stampFee(@Param("feeId") String feeId, @Param("supplierId") Long supplierId, @Param("supplierName") String supplierName);
int stampCalc(@Param("billId") String billId, @Param("supplierId") Long supplierId, @Param("supplierName") String supplierName);
}
@@ -0,0 +1,24 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayPayment;
import com.bms.price.domain.PayPaymentAlloc;
/** 应付付款单和核销。 */
public interface PayPaymentMapper
{
List<PayPayment> selectPayPaymentList(PayPayment query);
PayPayment selectPayPaymentById(@Param("paymentId") Long paymentId);
List<PayPaymentAlloc> selectAllocs(@Param("paymentId") Long paymentId);
int insertPayPayment(PayPayment payment);
int insertAlloc(PayPaymentAlloc alloc);
int updatePayPaymentStatus(PayPayment payment);
int reverseAlloc(@Param("allocId") Long allocId, @Param("reverseBy") String reverseBy);
}
@@ -0,0 +1,18 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PaySurchargeRule;
/** 应付附加费计费条件。一个产品版本一份。 */
public interface PaySurchargeRuleMapper
{
List<PaySurchargeRule> selectByProduct(@Param("productId") Long productId, @Param("termId") Long termId);
int deleteByTerm(@Param("productId") Long productId, @Param("termId") Long termId);
int insertBatch(@Param("list") List<PaySurchargeRule> rules);
/** 同一产品字典下,比当前版本号小的最近一版 */
PaySurchargeRule selectPreviousVersion(@Param("productId") Long productId);
}
@@ -9,12 +9,14 @@ import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import com.bms.common.enums.PaymentMesEnum;
import com.bms.price.domain.PayCostLine;
import com.bms.price.domain.PayOrderCostResult;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PaySurchargeRule;
import com.bms.price.domain.PayWeightBand;
/**
@@ -69,7 +71,7 @@ public final class PayCostCalculator
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus)
{
return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null);
return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null, true, true);
}
/**
@@ -89,6 +91,16 @@ public final class PayCostCalculator
*/
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List<PayRemoteBook.Hit> remoteHits)
{
return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, remoteHits, true, true);
}
/**
* postcodeOn、zoneOn 来自附加费配置的两个开关。关掉时不再按价卡上的邮编或分区条件加附加费。
*/
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List<PayRemoteBook.Hit> remoteHits,
boolean postcodeOn, boolean zoneOn)
{
PayOrderCostResult result = new PayOrderCostResult();
result.setFees(new ArrayList<>());
@@ -146,7 +158,7 @@ public final class PayCostCalculator
result.getFees().add(freightLine);
result.setCurrency(currency);
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
band.getSectionName(), remoteHits, result);
band.getSectionName(), remoteHits, postcodeOn, zoneOn, result);
BigDecimal total = BigDecimal.ZERO;
for (PayCostLine fee : result.getFees())
{
@@ -266,7 +278,7 @@ public final class PayCostCalculator
*/
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
String freightSection, List<PayRemoteBook.Hit> remoteHits, PayOrderCostResult result)
String freightSection, List<PayRemoteBook.Hit> remoteHits, boolean postcodeOn, boolean zoneOn, PayOrderCostResult result)
{
if (product.getSurcharges() == null)
{
@@ -283,6 +295,7 @@ public final class PayCostCalculator
if (remoteHits != null && remoteFee(fee))
{
String name = blank(fee.getFeeName());
boolean added = false;
if (postcode == null || postcode.isBlank())
{
result.getSkipped().add(name + ":订单没有邮编");
@@ -302,7 +315,21 @@ public final class PayCostCalculator
{
addFlat(result.getFees(), fee, currency, remark);
}
added = true;
}
if (added || !PaySurchargeRules.ZONE.equals(rule))
{
continue;
}
}
if (!postcodeOn && PaySurchargeRules.POSTCODE.equals(rule))
{
result.getSkipped().add(blank(fee.getFeeName()) + ":未打开命中邮编");
continue;
}
if (!zoneOn && PaySurchargeRules.ZONE.equals(rule))
{
result.getSkipped().add(blank(fee.getFeeName()) + ":未打开命中分区");
continue;
}
boolean postcodeDriven = PaySurchargeRules.POSTCODE.equals(rule) || !blank(fee.getPostcodeParam()).isEmpty();
@@ -346,9 +373,9 @@ public final class PayCostCalculator
{
result.getSkipped().add(name + ":订单没有长宽高");
}
else if (PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height))
else if (sizeHit(product, fee, length, width, height, weight))
{
PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中 " + fee.getCalcParam());
PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中");
if (matched != null)
{
sized.add(matched);
@@ -384,6 +411,10 @@ public final class PayCostCalculator
}
else if (PaySurchargeRules.ALWAYS.equals(rule))
{
if (!PaySurchargeRules.allowsAlways(ruleDetails(product, PaySurchargeRules.ALWAYS), name, bizDate))
{
continue;
}
PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText()));
if (matched != null)
{
@@ -517,6 +548,42 @@ public final class PayCostCalculator
return top;
}
private static boolean sizeHit(PayProduct product, PaySurcharge fee, BigDecimal length, BigDecimal width, BigDecimal height,
BigDecimal weight)
{
List<Map<String, Object>> details = ruleDetails(product, PaySurchargeRules.SIZE);
if (details.isEmpty())
{
return PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height);
}
for (Map<String, Object> detail : details)
{
if (PaySurchargeRules.matchesConfiguredSize(detail, length, width, height, weight))
{
return true;
}
}
return false;
}
private static List<Map<String, Object>> ruleDetails(PayProduct product, String calcRule)
{
List<Map<String, Object>> details = new ArrayList<>();
if (product == null || product.getBillingRules() == null)
{
return details;
}
for (PaySurchargeRule rule : product.getBillingRules())
{
if (rule == null || !calcRule.equals(rule.getCalcRule()) || "0".equals(rule.getEnabled()) || rule.getDetail() == null)
{
continue;
}
details.add(rule.getDetail());
}
return details;
}
private static boolean remoteFee(PaySurcharge fee)
{
String name = blank(fee.getFeeName()).toLowerCase(Locale.ROOT);
@@ -540,7 +607,7 @@ public final class PayCostCalculator
continue;
}
boolean areaKnown = !blank(hit.getZoneName()).isEmpty() || !blank(hit.getRegionName()).isEmpty();
if (zoned && areaKnown && !sameArea(hit, fee))
if (zoned && (!areaKnown || !sameArea(hit, fee)))
{
continue;
}
@@ -121,7 +121,8 @@ public final class PayRemoteBook
List<Hit> hits = new ArrayList<>();
for (PayPostcodeLine line : lines)
{
if (line == null || !remote.containsKey(line.getGroupId()) || !PayZoneMatcher.matchesLine(line, postcode))
if (line == null || !remote.containsKey(line.getGroupId()) || !sameLineCountry(line, countryCode, countryName)
|| !PayZoneMatcher.matchesLine(line, postcode))
{
continue;
}
@@ -146,6 +147,21 @@ public final class PayRemoteBook
return new BigDecimal(matcher.group(1));
}
/** 明细上写了国家时必须和订单目的国一致。没写则沿用分组上的国家。 */
private static boolean sameLineCountry(PayPostcodeLine line, String countryCode, String countryName)
{
boolean lineBlank = blank(line.getCountryCode()) && blank(line.getCountryName());
if (lineBlank)
{
return true;
}
if (!blank(countryCode) && countryCode.equalsIgnoreCase(line.getCountryCode() == null ? "" : line.getCountryCode().trim()))
{
return true;
}
return !blank(countryName) && countryName.trim().equals(line.getCountryName() == null ? "" : line.getCountryName().trim());
}
private static boolean sameCountry(PayPostcodeGroup group, String countryCode, String countryName)
{
boolean orderBlank = blank(countryCode) && blank(countryName);
@@ -0,0 +1,110 @@
package com.bms.price.pay;
import java.util.List;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.bms.customer.supplierinfo.domain.BmsSupplier;
import com.bms.customer.supplierinfo.mapper.BmsSupplierMapper;
import com.bms.price.mapper.PayFinanceMapper;
/** 把价卡上的归属名称对到供应商主数据。 */
@Component
public class PaySettleSupport
{
@Autowired
private BmsSupplierMapper supplierMapper;
@Autowired
private PayFinanceMapper payFinanceMapper;
/**
* 计算费用落库后,按价卡归属写上结算供应商。对不上供应商时保持为空。
*
* @param billId 计费订单ID
* @param cardName 价卡归属名称
*/
public void stampCalc(String billId, String cardName)
{
BmsSupplier supplier = match(cardName);
if (supplier == null || billId == null || billId.isBlank())
{
return;
}
payFinanceMapper.stampCalc(billId, supplier.getSupplierId(), supplier.getSupplierName());
}
/**
* 用价卡归属名称找到唯一供应商。名称、编码或对外代码一致优先,其次互相包含。
*
* @param cardName 价卡上的供应商名称
* @return 对上的供应商;对不上或对上多家且没有精确项时返回 null
*/
public BmsSupplier match(String cardName)
{
String card = norm(cardName);
if (card.isEmpty())
{
return null;
}
List<BmsSupplier> all = supplierMapper.selectBmsSupplierList(new BmsSupplier());
BmsSupplier exact = null;
BmsSupplier fuzzy = null;
int fuzzyCount = 0;
if (all == null)
{
return null;
}
for (BmsSupplier supplier : all)
{
if (supplier == null || supplier.getSupplierId() == null)
{
continue;
}
String name = norm(supplier.getSupplierName());
String code = norm(supplier.getSupplierCode());
String dw = norm(supplier.getSupplierCodeDw());
if (card.equals(name) || card.equals(code) || (!dw.isEmpty() && card.equals(dw)))
{
exact = supplier;
break;
}
if (card.length() >= 2 && name.length() >= 2 && (name.contains(card) || card.contains(name)))
{
fuzzy = supplier;
fuzzyCount++;
}
}
if (exact != null)
{
return exact;
}
return fuzzyCount == 1 ? fuzzy : null;
}
/**
* 所选供应商和价卡归属是不是同一家。
*
* @param supplier 账单上选择的供应商
* @param cardName 价卡归属名称
* @return 对得上时为 true
*/
public boolean same(BmsSupplier supplier, String cardName)
{
if (supplier == null || supplier.getSupplierId() == null)
{
return false;
}
BmsSupplier matched = match(cardName);
return matched != null && supplier.getSupplierId().equals(matched.getSupplierId());
}
private static String norm(String value)
{
if (value == null)
{
return "";
}
return value.replaceAll("\\s+", "").toLowerCase(Locale.ROOT);
}
}
@@ -125,13 +125,23 @@ public final class PaySurchargeRules
{
continue;
}
String compact = value.replaceAll("[^A-Z0-9]", "");
String whole = postcode.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
if (!compact.isEmpty() && compact.equals(whole))
{
return true;
}
if (parsed != null && tokenMatches(value, parsed))
{
return true;
}
if (parsed == null && value.matches("\\d{1,5}") && !digits.isEmpty() && digits.startsWith(value))
if (parsed == null && value.matches("\\d{1,5}") && !digits.isEmpty())
{
return true;
boolean full = value.length() >= 4 ? digits.equals(value) : digits.startsWith(value);
if (full)
{
return true;
}
}
}
return false;
@@ -159,6 +169,218 @@ public final class PaySurchargeRules
return String.join(",", tokens);
}
/**
* 按附加费配置里的超尺寸明细判断。上限表示超过这个值才命中。箱规按从大到小比,任一边超过对应边即命中。
*/
public static boolean matchesConfiguredSize(java.util.Map<String, Object> detail, BigDecimal length, BigDecimal width,
BigDecimal height, BigDecimal weight)
{
if (detail == null || detail.isEmpty() || length == null || width == null || height == null)
{
return false;
}
BigDecimal weightMin = number(detail.get("weightMin"));
BigDecimal weightMax = number(detail.get("weightMax"));
if (weightMin != null || weightMax != null)
{
if (weight == null)
{
return false;
}
if (weightMin != null && weight.compareTo(weightMin) <= 0)
{
return false;
}
if (weightMax != null && weight.compareTo(weightMax) > 0)
{
return false;
}
}
BigDecimal[] sides = desc(length, width, height);
List<Boolean> checks = new ArrayList<>();
addBound(checks, sides[0], detail.get("longestMin"), detail.get("longestMax"));
addBound(checks, sides[1], detail.get("secondMin"), detail.get("secondMax"));
addBound(checks, length.add(width).add(height), detail.get("sumMin"), detail.get("sumMax"));
addBound(checks, sides[0].add(sides[1].add(sides[2]).multiply(new BigDecimal("2"))), detail.get("girthMin"), detail.get("girthMax"));
BigDecimal volumeMin = number(detail.get("volumeMin"));
BigDecimal volumeMax = number(detail.get("volumeMax"));
if (volumeMin != null || volumeMax != null)
{
BigDecimal cubic = length.multiply(width).multiply(height).divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
addBound(checks, cubic, volumeMin, volumeMax);
}
BigDecimal cubicMax = number(detail.get("cubicMax"));
if (cubicMax != null)
{
checks.add(length.multiply(width).multiply(height).compareTo(cubicMax) > 0);
}
BigDecimal boxLength = number(detail.get("boxLength"));
BigDecimal boxWidth = number(detail.get("boxWidth"));
BigDecimal boxHeight = number(detail.get("boxHeight"));
if (boxLength != null && boxWidth != null && boxHeight != null)
{
boolean under = "UNDER".equals(String.valueOf(detail.get("boxMode")));
checks.add(under ? withinBox(sides, boxLength, boxWidth, boxHeight) : exceedsBox(sides, boxLength, boxWidth, boxHeight));
}
if (checks.isEmpty())
{
return false;
}
if ("ALL".equals(String.valueOf(detail.get("matchMode"))))
{
return !checks.contains(Boolean.FALSE);
}
return checks.contains(Boolean.TRUE);
}
/** 每票都收。写了费用名的规则只约束这一项;带生效月日的规则只在这段日期内收取。 */
public static boolean allowsAlways(List<java.util.Map<String, Object>> details, String feeName, java.util.Date bizDate)
{
if (details == null || details.isEmpty())
{
return true;
}
String name = feeName == null ? "" : feeName.trim();
List<java.util.Map<String, Object>> named = new ArrayList<>();
List<java.util.Map<String, Object>> shared = new ArrayList<>();
for (java.util.Map<String, Object> detail : details)
{
if (detail == null)
{
continue;
}
String ruleName = detail.get("feeName") == null ? "" : String.valueOf(detail.get("feeName")).trim();
if (ruleName.isEmpty())
{
shared.add(detail);
}
else if (ruleName.equals(name))
{
named.add(detail);
}
}
List<java.util.Map<String, Object>> mine = named.isEmpty() ? shared : named;
if (mine.isEmpty())
{
return shared.isEmpty();
}
java.time.LocalDate day = bizDate == null ? null : bizDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
for (java.util.Map<String, Object> detail : mine)
{
if (!hasPeriod(detail))
{
return true;
}
if (day != null && inPeriod(day, detail))
{
return true;
}
}
return false;
}
private static void addBound(List<Boolean> checks, BigDecimal actual, Object min, Object max)
{
BigDecimal lower = min instanceof BigDecimal ? (BigDecimal) min : number(min);
BigDecimal upper = max instanceof BigDecimal ? (BigDecimal) max : number(max);
if (lower == null && upper == null)
{
return;
}
if (lower != null && upper != null)
{
checks.add(actual.compareTo(lower) > 0 && actual.compareTo(upper) <= 0);
}
else if (upper != null)
{
checks.add(actual.compareTo(upper) > 0);
}
else
{
checks.add(actual.compareTo(lower) > 0);
}
}
private static boolean withinBox(BigDecimal[] parcel, BigDecimal a, BigDecimal b, BigDecimal c)
{
BigDecimal[] limit = desc(a, b, c);
return parcel[0].compareTo(limit[0]) < 0 && parcel[1].compareTo(limit[1]) < 0 && parcel[2].compareTo(limit[2]) < 0;
}
private static boolean hasPeriod(java.util.Map<String, Object> detail)
{
return text(detail.get("periodStart")) != null || text(detail.get("periodEnd")) != null;
}
private static boolean inPeriod(java.time.LocalDate day, java.util.Map<String, Object> detail)
{
int[] start = monthDay(text(detail.get("periodStart")));
int[] end = monthDay(text(detail.get("periodEnd")));
int current = day.getMonthValue() * 100 + day.getDayOfMonth();
int from = start == null ? 101 : start[0] * 100 + start[1];
int to = end == null ? 1231 : end[0] * 100 + end[1];
if (from <= to)
{
return current >= from && current <= to;
}
return current >= from || current <= to;
}
private static int[] monthDay(String text)
{
if (text == null)
{
return null;
}
String[] parts = text.split("-");
try
{
if (parts.length == 2)
{
return new int[] { Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) };
}
if (parts.length == 3)
{
return new int[] { Integer.parseInt(parts[1]), Integer.parseInt(parts[2]) };
}
}
catch (NumberFormatException ex)
{
return null;
}
return null;
}
private static String text(Object value)
{
if (value == null)
{
return null;
}
String text = String.valueOf(value).trim();
return text.isEmpty() ? null : text;
}
private static BigDecimal number(Object value)
{
if (value == null || (value instanceof String text && text.isBlank()))
{
return null;
}
if (value instanceof BigDecimal decimal)
{
return decimal;
}
try
{
return new BigDecimal(String.valueOf(value).trim());
}
catch (NumberFormatException ex)
{
return null;
}
}
public static boolean matchesWeight(String param, BigDecimal weight)
{
if (param == null || param.isBlank() || weight == null)
@@ -247,11 +247,56 @@ public final class PayZoneMatcher
private static int matchedLength(PayPostcodeLine line, String postcode)
{
int best = -1;
best = longer(best, line.getPostcode(), postcode);
best = longer(best, line.getPostcodeEnd(), postcode);
String start = line.getPostcode();
String end = line.getPostcodeEnd();
if (start != null && end != null && !end.isBlank() && !compact(start).equals(compact(end)))
{
best = Math.max(best, rangeLength(start, end, postcode));
}
best = longer(best, start, postcode);
return best;
}
/** 完整邮编按整段相等,短码按前缀。区间用起止邮编,含两端。 */
private static int rangeLength(String start, String end, String postcode)
{
String from = compact(start);
String to = compact(end);
String value = compact(postcode);
if (from.isEmpty() || to.isEmpty() || value.isEmpty())
{
return -1;
}
if (from.compareTo(to) > 0)
{
String swap = from;
from = to;
to = swap;
}
if (digits(from) && digits(to) && digits(value) && value.length() == from.length())
{
long low = Long.parseLong(from);
long high = Long.parseLong(to);
long current = Long.parseLong(value);
return current >= low && current <= high ? from.length() : -1;
}
if (from.length() == to.length() && value.length() == from.length() && value.compareTo(from) >= 0 && value.compareTo(to) <= 0)
{
return from.length();
}
return -1;
}
private static String compact(String value)
{
return value == null ? "" : value.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
}
private static boolean digits(String value)
{
return !value.isEmpty() && value.chars().allMatch(Character::isDigit);
}
private static int longer(int best, String raw, String postcode)
{
if (raw == null || raw.isBlank())
@@ -1,8 +1,11 @@
package com.bms.price.service;
import java.io.OutputStream;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.bms.price.domain.PayBill;
import com.bms.price.domain.PayBillCommand;
import com.bms.price.domain.PayBillFile;
import com.bms.price.domain.PayBillPreview;
/**
@@ -66,4 +69,43 @@ public interface IPayBillService
* @return 成功时为 1
*/
int voidBill(PayBillCommand command);
/** 草稿提交审核。 */
int submit(Long billId);
/** 审核通过。蓝字需要已有凭证。红字会冲回原账单并释放费用。 */
int approve(Long billId);
/** 审核驳回,回到草稿。 */
int reject(PayBillCommand command);
/** 草稿登记对方金额。 */
int saveSupplierAmounts(PayBillCommand command);
/** 接受我方或接受对方。接受对方会另写调整费用。 */
int acceptDiff(PayBillCommand command);
/** 从草稿剔除一条费用。 */
int removeLine(PayBillCommand command);
/** 上传应付凭证。 */
int saveFile(PayBillFile file);
/** 删除草稿或待审核账单上的凭证。 */
int deleteFile(Long fileId);
/** 查询应付凭证。 */
List<PayBillFile> listFiles(PayBillFile query);
/** 对未付款的已确认账单开红字草稿。 */
PayBill redBill(PayBillCommand command);
/** 草稿保存税率和汇率,并重算不含税、税额、本位币。 */
int saveFinance(PayBillCommand command);
/** 导出对账单,供供应商回填对方金额。 */
void exportStatement(Long billId, OutputStream out);
/** 按行号导入对方金额。 */
int importSupplierAmounts(Long billId, MultipartFile file);
}
@@ -14,4 +14,20 @@ public interface IPayOrderCostService
* @return 本次算出的产品、版本、币种和费用明细
*/
PayOrderCostResult generate(String billId);
/**
* 单独提交一票尾程。失败时只回滚这一票,不影响外面的对账单事务。
*
* @param billId 计费订单ID
* @return 本次算出的产品、版本、币种和费用明细
*/
PayOrderCostResult generateIsolated(String billId);
/**
* 这票订单当前尾程价卡上的归属供应商名称。没有价卡时返回空字符串。
*
* @param billId 计费订单ID
* @return 价卡归属名称
*/
String cardSupplierName(String billId);
}
@@ -0,0 +1,27 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PayPayment;
import com.bms.price.domain.PayPaymentAlloc;
/** 应付付款。草稿保存核销计划,确认后才回写账单已付金额。 */
public interface IPayPaymentService
{
List<PayPayment> selectPayPaymentList(PayPayment query);
PayPayment selectPayPaymentDetail(Long paymentId);
List<PayPaymentAlloc> selectOpenBills(Long supplierId, String currency);
PayPayment create(PayPayment payment);
int confirm(Long paymentId);
int voidPayment(Long paymentId);
/** 反核销已确认付款。账单未付金额回来。 */
int reverse(Long paymentId);
/** 供应商退回这笔付款。核销冲回。 */
int refund(Long paymentId);
}
@@ -0,0 +1,17 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PaySurchargeRule;
import com.bms.price.domain.PaySurchargeRuleSave;
/** 应付附加费计费条件。一个产品版本只保留一份。 */
public interface IPaySurchargeRuleService
{
List<PaySurchargeRule> list(Long productId, Long termId);
/** 上一版的条件,只读出来供当前页编辑,不直接落库 */
List<PaySurchargeRule> previous(Long productId);
/** 覆盖所选产品当前版本的配置。同一版本不会留下两份。 */
void save(PaySurchargeRuleSave command);
}
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ import java.util.*;
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.bms.common.enums.PaymentMesEnum;
import com.bms.common.exception.ServiceException;
@@ -17,6 +18,7 @@ import com.bms.price.domain.PayPostcodeGroup;
import com.bms.price.domain.PayPostcodeLine;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PaySurchargeRule;
import com.bms.price.domain.PayWeightBand;
import com.bms.price.domain.PrcOrderBill;
import com.bms.price.domain.PrcOrderBillExt;
@@ -28,11 +30,13 @@ import com.bms.price.mapper.PrcOrderBillMapper;
import com.bms.price.mapper.PrcPayBillFeeMapper;
import com.bms.price.mapper.PrcPayBillMapper;
import com.bms.price.pay.PayCostCalculator;
import com.bms.price.pay.PaySettleSupport;
import com.bms.price.pay.PayRemoteBook;
import com.bms.price.pay.PaySurchargeRules;
import com.bms.price.pay.PayZoneMatcher;
import com.bms.price.service.IPayOrderCostService;
import com.bms.price.service.IPayProductService;
import com.bms.price.service.IPaySurchargeRuleService;
import com.bms.price.service.IPrcPayBillFeeService;
/**
@@ -62,9 +66,15 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
@Autowired
private IPrcPayBillFeeService payBillFeeService;
@Autowired
private PaySettleSupport paySettleSupport;
@Autowired
private PayPostcodeMapper payPostcodeMapper;
@Autowired
private IPaySurchargeRuleService paySurchargeRuleService;
/**
* 重算一票订单的尾程应付并落库。
* 先找渠道和计费日对应的价卡版本,再用重量、尺寸、邮编算出费用,写回订单应付。
@@ -72,6 +82,13 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
* @param billId 计费订单ID
* @return 本次使用的渠道、价卡版本和费用明细
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PayOrderCostResult generateIsolated(String billId)
{
return generate(billId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayOrderCostResult generate(String billId)
@@ -93,10 +110,21 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus());
List<PayPostcodeGroup> linked = linkedGroups(product);
String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName(), linked);
// 根据邮编判断是否是偏远地区
List<PayRemoteBook.Hit> remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName());
List<PaySurchargeRule> billingRules = billingRules(product);
product.setBillingRules(billingRules);
boolean postcodeOn = ruleOn(billingRules, PaySurchargeRules.POSTCODE);
boolean zoneOn = ruleOn(billingRules, PaySurchargeRules.ZONE);
List<PayRemoteBook.Hit> remoteHits = null;
if (postcodeOn)
{
remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName());
if (remoteHits == null)
{
remoteHits = new ArrayList<>();
}
}
PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(),
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits);
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits, postcodeOn, zoneOn);
result.setBillId(bill.getBillId());
result.setChannelId(channel.getId());
result.setChannelName(channel.getChannelName());
@@ -112,15 +140,64 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
String end = product.getTermEnd() == null ? "不限" : day(product.getTermEnd());
result.setMessage("渠道「" + channelName + "」产品编号 " + product.getOriginId() + " 第 " + product.getVersionNo()
+ " 版(" + day(product.getTermStart()) + " 至 " + end + ")," + result.getMessage());
paySettleSupport.stampCalc(bill.getBillId(), product.getSupplierName());
return result;
}
@Override
public String cardSupplierName(String billId)
{
if (billId == null || billId.isBlank())
{
return "";
}
try
{
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim());
if (bill == null)
{
return "";
}
PrcOrderBillExt ext = orderBillExtMapper.selectByBillId(bill.getBillId());
ServiceChannel channel = resolveChannel(ext);
Date bizDate = bill.getBizDate() != null ? bill.getBizDate() : bill.getOrderTime();
PayProduct product = payProductService.selectPriceForDate(channel.getId(), bizDate);
return product.getSupplierName() == null ? "" : product.getSupplierName().trim();
}
catch (ServiceException ex)
{
return "";
}
}
/**
* 用产品名称、报价工作表、字典值和关联分区表,对上这份产品的邮编配置。
*
* @param product 当前价卡版本
* @return 名称能对上的邮编组,没有时为空列表
*/
private List<PaySurchargeRule> billingRules(PayProduct product)
{
if (product == null || product.getProductId() == null || product.getTermId() == null)
{
return new ArrayList<>();
}
List<PaySurchargeRule> rules = paySurchargeRuleService.list(product.getProductId(), product.getTermId());
return rules == null ? new ArrayList<>() : rules;
}
private static boolean ruleOn(List<PaySurchargeRule> rules, String calcRule)
{
for (PaySurchargeRule rule : rules)
{
if (rule != null && calcRule.equals(rule.getCalcRule()) && !"0".equals(rule.getEnabled()))
{
return true;
}
}
return false;
}
private List<PayPostcodeGroup> linkedGroups(PayProduct product)
{
List<String> names = new ArrayList<>();
@@ -0,0 +1,413 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.common.enums.PaymentMesEnum;
import com.bms.common.utils.SecurityUtils;
import com.bms.common.utils.uuid.IdUtils;
import com.bms.customer.supplierinfo.domain.BmsSupplier;
import com.bms.customer.supplierinfo.mapper.BmsSupplierMapper;
import com.bms.price.domain.PayBill;
import com.bms.price.domain.PayBillLog;
import com.bms.price.domain.PayPayment;
import com.bms.price.domain.PayPaymentAlloc;
import com.bms.price.mapper.PayBillMapper;
import com.bms.price.mapper.PayPaymentMapper;
import com.bms.price.service.IPayPaymentService;
/** 应付付款。确认时按核销金额回写账单,一张付款可以核多张账单。 */
@Service
public class PayPaymentServiceImpl implements IPayPaymentService
{
@Autowired
private PayPaymentMapper payPaymentMapper;
@Autowired
private PayBillMapper payBillMapper;
@Autowired
private BmsSupplierMapper supplierMapper;
@Override
public List<PayPayment> selectPayPaymentList(PayPayment query)
{
return payPaymentMapper.selectPayPaymentList(query);
}
@Override
public PayPayment selectPayPaymentDetail(Long paymentId)
{
PayPayment payment = required(paymentId);
payment.setAllocs(payPaymentMapper.selectAllocs(paymentId));
return payment;
}
@Override
public List<PayPaymentAlloc> selectOpenBills(Long supplierId, String currency)
{
if (supplierId == null)
{
throw PaymentMesEnum.BMS_PAY_PICK_SUP.exception();
}
if (currency == null || currency.isBlank())
{
throw PaymentMesEnum.BMS_PAY_CURRENCY.exception();
}
return payBillMapper.selectOpenBills(supplierId, currency.trim());
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayPayment create(PayPayment payment)
{
if (payment == null || payment.getSupplierId() == null)
{
throw PaymentMesEnum.BMS_PAY_PICK_SUP.exception();
}
if (payment.getAmount() == null || payment.getAmount().compareTo(BigDecimal.ZERO) <= 0)
{
throw PaymentMesEnum.BMS_PAY_PAY_AMT.exception();
}
if (payment.getPayDate() == null)
{
throw PaymentMesEnum.BMS_PAY_PAY_DATE.exception();
}
if (payment.getCurrency() == null || payment.getCurrency().isBlank())
{
throw PaymentMesEnum.BMS_PAY_CURRENCY.exception();
}
BmsSupplier supplier = supplierMapper.selectBmsSupplierBySupplierId(payment.getSupplierId());
if (supplier == null)
{
throw PaymentMesEnum.BMS_PAY_SUP_MISS.exception();
}
String operator = SecurityUtils.getUsername();
payment.setPaymentNo(nextNo());
payment.setSupplierName(supplier.getSupplierName());
payment.setCurrency(payment.getCurrency().trim());
payment.setPayMethod(payment.getPayMethod() == null || payment.getPayMethod().isBlank() ? "BANK" : payment.getPayMethod());
payment.setBankFee(payment.getBankFee() == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : payment.getBankFee());
payment.setAmount(payment.getAmount().setScale(2, RoundingMode.HALF_UP));
if (payment.getExchangeRate() == null || payment.getExchangeRate().compareTo(BigDecimal.ZERO) <= 0)
{
payment.setExchangeRate(BigDecimal.ONE);
}
payment.setCreateBy(operator);
checkAllocs(payment);
payPaymentMapper.insertPayPayment(payment);
if (payment.getAllocs() != null)
{
for (PayPaymentAlloc alloc : payment.getAllocs())
{
if (alloc.getBillId() == null || alloc.getAllocAmount() == null || alloc.getAllocAmount().compareTo(BigDecimal.ZERO) <= 0)
{
continue;
}
alloc.setPaymentId(payment.getPaymentId());
alloc.setAllocAmount(alloc.getAllocAmount().setScale(2, RoundingMode.HALF_UP));
payPaymentMapper.insertAlloc(alloc);
}
}
return selectPayPaymentDetail(payment.getPaymentId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int confirm(Long paymentId)
{
PayPayment payment = required(paymentId);
if (!"DRAFT".equals(payment.getStatus()))
{
throw PaymentMesEnum.BMS_PAY_PAY_DRAFT.exception();
}
payment.setAllocs(payPaymentMapper.selectAllocs(paymentId));
checkAllocs(payment);
String operator = SecurityUtils.getUsername();
if (operator.equals(payment.getCreateBy()))
{
throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception();
}
BigDecimal rate = payment.getExchangeRate() == null ? BigDecimal.ONE : payment.getExchangeRate();
BigDecimal allocSum = BigDecimal.ZERO;
if (payment.getAllocs() != null)
{
for (PayPaymentAlloc alloc : payment.getAllocs())
{
if (alloc.getAllocAmount() == null || alloc.getAllocAmount().compareTo(BigDecimal.ZERO) <= 0)
{
continue;
}
PayBill bill = payBillMapper.selectPayBillById(alloc.getBillId());
applyAlloc(payment, bill, alloc.getAllocAmount(), operator);
allocSum = allocSum.add(alloc.getAllocAmount());
}
}
payment.setExchangeRate(rate);
payment.setAllocAmount(allocSum.setScale(2, RoundingMode.HALF_UP));
payment.setBaseAmount(payment.getAmount().multiply(rate).setScale(2, RoundingMode.HALF_UP));
payment.setStatus("PAID");
payment.setConfirmBy(operator);
payment.setConfirmTime(new Date());
payPaymentMapper.updatePayPaymentStatus(payment);
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int reverse(Long paymentId)
{
PayPayment payment = required(paymentId);
if (!"PAID".equals(payment.getStatus()))
{
throw PaymentMesEnum.BMS_PAY_NO_REVERSE.exception();
}
String operator = SecurityUtils.getUsername();
if (operator.equals(payment.getConfirmBy()))
{
throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception();
}
BigDecimal back = releaseAllocs(payment, operator);
if (back.compareTo(BigDecimal.ZERO) == 0)
{
throw PaymentMesEnum.BMS_PAY_NO_BACK.exception();
}
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int refund(Long paymentId)
{
PayPayment payment = required(paymentId);
if (!"PAID".equals(payment.getStatus()))
{
throw PaymentMesEnum.BMS_PAY_NO_REVERSE.exception();
}
String operator = SecurityUtils.getUsername();
if (operator.equals(payment.getConfirmBy()))
{
throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception();
}
BigDecimal back = releaseAllocs(payment, operator);
if (back.compareTo(BigDecimal.ZERO) == 0)
{
throw PaymentMesEnum.BMS_PAY_NO_BACK.exception();
}
payment.setAllocAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
payment.setStatus("REFUNDED");
payPaymentMapper.updatePayPaymentStatus(payment);
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int voidPayment(Long paymentId)
{
PayPayment payment = required(paymentId);
if (!"DRAFT".equals(payment.getStatus()))
{
throw PaymentMesEnum.BMS_PAY_PAY_DRAFT.exception();
}
payment.setStatus("VOID");
payPaymentMapper.updatePayPaymentStatus(payment);
return 1;
}
private void checkAllocs(PayPayment payment)
{
BigDecimal sum = BigDecimal.ZERO;
Set<Long> seen = new HashSet<>();
if (payment.getAllocs() == null)
{
return;
}
for (PayPaymentAlloc alloc : payment.getAllocs())
{
if (alloc.getBillId() == null || alloc.getAllocAmount() == null || alloc.getAllocAmount().compareTo(BigDecimal.ZERO) <= 0)
{
continue;
}
if (!seen.add(alloc.getBillId()))
{
throw PaymentMesEnum.BMS_PAY_ALLOC_DUP.exception();
}
PayBill bill = payBillMapper.selectPayBillById(alloc.getBillId());
if (bill == null)
{
throw PaymentMesEnum.BMS_PAY_BILL_MISS.exception();
}
if (!payment.getSupplierId().equals(bill.getSupplierId()) || !payment.getCurrency().equals(bill.getCurrency()))
{
throw PaymentMesEnum.BMS_PAY_PAY_SAME.exception();
}
if (!"CONFIRMED".equals(bill.getStatus()) && !"PARTIAL".equals(bill.getStatus()))
{
throw PaymentMesEnum.BMS_PAY_PAY_BILL.exception();
}
BigDecimal open = openAmount(bill);
if (alloc.getAllocAmount().compareTo(open) > 0)
{
throw PaymentMesEnum.BMS_PAY_ALLOC_AMT.exception();
}
sum = sum.add(alloc.getAllocAmount());
}
if (sum.compareTo(payment.getAmount()) > 0)
{
throw PaymentMesEnum.BMS_PAY_ALLOC_OVER.exception();
}
}
/**
* 冲回仍有效的核销,并减少付款单上的已核销金额。
*
* @param payment 已付款单
* @param operator 操作人
* @return 冲回的金额
*/
private BigDecimal releaseAllocs(PayPayment payment, String operator)
{
List<PayPaymentAlloc> allocs = payPaymentMapper.selectAllocs(payment.getPaymentId());
BigDecimal back = BigDecimal.ZERO;
for (PayPaymentAlloc alloc : allocs)
{
if (alloc.getAllocAmount() == null || !"ACTIVE".equals(alloc.getAllocStatus()))
{
continue;
}
PayBill bill = payBillMapper.selectPayBillById(alloc.getBillId());
if (bill != null)
{
undoAlloc(payment, bill, alloc.getAllocAmount(), operator);
}
payPaymentMapper.reverseAlloc(alloc.getAllocId(), operator);
back = back.add(alloc.getAllocAmount());
}
back = back.setScale(2, RoundingMode.HALF_UP);
if (back.compareTo(BigDecimal.ZERO) > 0)
{
BigDecimal allocAmount = payment.getAllocAmount() == null ? BigDecimal.ZERO : payment.getAllocAmount();
BigDecimal nextAlloc = allocAmount.subtract(back);
payment.setAllocAmount(nextAlloc.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : nextAlloc.setScale(2, RoundingMode.HALF_UP));
payPaymentMapper.updatePayPaymentStatus(payment);
}
return back;
}
private void undoAlloc(PayPayment payment, PayBill bill, BigDecimal allocAmount, String operator)
{
BigDecimal paid = bill.getPaidAmount() == null ? BigDecimal.ZERO : bill.getPaidAmount();
BigDecimal next = paid.subtract(allocAmount);
if (next.compareTo(BigDecimal.ZERO) < 0)
{
next = BigDecimal.ZERO;
}
next = next.setScale(2, RoundingMode.HALF_UP);
BigDecimal total = bill.getTotalAmount() == null ? BigDecimal.ZERO : bill.getTotalAmount();
String from = bill.getStatus();
bill.setPaidAmount(next);
if (next.compareTo(BigDecimal.ZERO) == 0)
{
bill.setStatus("CONFIRMED");
}
else if (next.compareTo(total) >= 0)
{
bill.setStatus("PAID");
}
else
{
bill.setStatus("PARTIAL");
}
bill.setUpdateBy(operator);
if (bill.getSupplierAmount() == null)
{
bill.setSupplierAmount(total);
}
if (bill.getDiffAmount() == null)
{
bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
}
if (bill.getOrderCount() == null)
{
bill.setOrderCount(0);
}
payBillMapper.updatePayBillMoney(bill);
PayBillLog log = new PayBillLog();
log.setBillId(bill.getBillId());
log.setBizType("PAYMENT");
log.setBizId(payment.getPaymentId());
log.setOperationType("REVERSE");
log.setFromStatus(from);
log.setToStatus(bill.getStatus());
log.setOperationDesc("付款 " + payment.getPaymentNo() + " 反核销 " + allocAmount);
log.setAmountBefore(paid);
log.setAmountAfter(next);
log.setOperatorName(operator);
payBillMapper.insertLog(log);
}
private void applyAlloc(PayPayment payment, PayBill bill, BigDecimal allocAmount, String operator)
{
BigDecimal paid = bill.getPaidAmount() == null ? BigDecimal.ZERO : bill.getPaidAmount();
BigDecimal next = paid.add(allocAmount).setScale(2, RoundingMode.HALF_UP);
BigDecimal total = bill.getTotalAmount() == null ? BigDecimal.ZERO : bill.getTotalAmount();
String from = bill.getStatus();
bill.setPaidAmount(next);
bill.setStatus(next.compareTo(total) >= 0 ? "PAID" : "PARTIAL");
bill.setUpdateBy(operator);
if (bill.getSupplierAmount() == null)
{
bill.setSupplierAmount(total);
}
if (bill.getDiffAmount() == null)
{
bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
}
if (bill.getOrderCount() == null)
{
bill.setOrderCount(0);
}
payBillMapper.updatePayBillMoney(bill);
PayBillLog log = new PayBillLog();
log.setBillId(bill.getBillId());
log.setBizType("PAYMENT");
log.setBizId(payment.getPaymentId());
log.setOperationType("PAY");
log.setFromStatus(from);
log.setToStatus(bill.getStatus());
log.setOperationDesc("付款 " + payment.getPaymentNo() + " 核销 " + allocAmount);
log.setAmountBefore(paid);
log.setAmountAfter(next);
log.setOperatorName(operator);
payBillMapper.insertLog(log);
}
private BigDecimal openAmount(PayBill bill)
{
BigDecimal total = bill.getTotalAmount() == null ? BigDecimal.ZERO : bill.getTotalAmount();
BigDecimal paid = bill.getPaidAmount() == null ? BigDecimal.ZERO : bill.getPaidAmount();
return total.subtract(paid);
}
private PayPayment required(Long paymentId)
{
PayPayment payment = paymentId == null ? null : payPaymentMapper.selectPayPaymentById(paymentId);
if (payment == null)
{
throw PaymentMesEnum.BMS_PAY_PAY_MISS.exception();
}
return payment;
}
private String nextNo()
{
return "FK" + new SimpleDateFormat("yyyyMM").format(new Date()) + IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase();
}
}
@@ -0,0 +1,563 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.SecurityUtils;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PayProductTerm;
import com.bms.price.domain.PaySurchargeRule;
import com.bms.price.domain.PaySurchargeRuleSave;
import com.bms.price.mapper.PayProductMapper;
import com.bms.price.mapper.PayProductTermMapper;
import com.bms.price.mapper.PaySurchargeRuleMapper;
import com.bms.price.service.IPaySurchargeRuleService;
@Service
public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
{
private static final Set<String> RULES = Set.of("ALWAYS", "POSTCODE", "ZONE", "EXTRA_KG", "WEIGHT", "SIZE", "RETURN");
private static final Set<String> NUMBERS = Set.of(
"longestMin", "longestMax", "secondMin", "secondMax", "sumMin", "sumMax", "girthMin", "girthMax",
"boxLength", "boxWidth", "boxHeight", "volumeMin", "volumeMax", "cubicMax", "weightMin", "weightMax",
"exceedStepCm", "exceedRatio", "thresholdKg", "stepKg", "minCharge", "ratio");
@Autowired
private PaySurchargeRuleMapper ruleMapper;
@Autowired
private PayProductMapper productMapper;
@Autowired
private PayProductTermMapper termMapper;
@Override
public List<PaySurchargeRule> list(Long productId, Long termId)
{
if (productId == null || termId == null)
{
return List.of();
}
List<PaySurchargeRule> rows = ruleMapper.selectByProduct(productId, termId);
rows.forEach(PaySurchargeRuleServiceImpl::hydrate);
return rows;
}
@Override
public List<PaySurchargeRule> previous(Long productId)
{
if (productId == null)
{
throw new ServiceException("请选择产品版本");
}
PaySurchargeRule previous = ruleMapper.selectPreviousVersion(productId);
if (previous == null || previous.getProductId() == null || previous.getTermId() == null)
{
return List.of();
}
List<PaySurchargeRule> rows = ruleMapper.selectByProduct(previous.getProductId(), previous.getTermId());
rows.forEach(PaySurchargeRuleServiceImpl::hydrate);
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(PaySurchargeRuleSave command)
{
if (command == null || command.getProducts() == null || command.getProducts().isEmpty())
{
throw new ServiceException("请选择要绑定的产品");
}
List<PaySurchargeRule> rules = normalize(command.getRules());
String operator = SecurityUtils.getUsername();
Map<String, PaySurchargeRuleSave.PaySurchargeRuleBind> versions = new LinkedHashMap<>();
for (PaySurchargeRuleSave.PaySurchargeRuleBind bind : command.getProducts())
{
if (bind == null || bind.getProductId() == null || bind.getTermId() == null)
{
throw new ServiceException("产品还没有可绑定的生效版本");
}
versions.put(bind.getProductId() + ":" + bind.getTermId(), bind);
}
for (PaySurchargeRuleSave.PaySurchargeRuleBind bind : versions.values())
{
PayProduct product = productMapper.selectPayProductById(bind.getProductId());
if (product == null)
{
throw new ServiceException("应付产品不存在");
}
PayProductTerm term = termMapper.selectById(bind.getTermId());
if (term == null || term.getProductId() == null || !term.getProductId().equals(bind.getProductId()))
{
throw new ServiceException("计费条件必须跟这一版有效期走");
}
ruleMapper.deleteByTerm(bind.getProductId(), bind.getTermId());
if (rules.isEmpty())
{
continue;
}
List<PaySurchargeRule> rows = new ArrayList<>();
int sort = 0;
String lastRule = "";
for (PaySurchargeRule rule : rules)
{
if (!rule.getCalcRule().equals(lastRule))
{
sort = 0;
lastRule = rule.getCalcRule();
}
PaySurchargeRule row = copy(rule);
row.setProductId(bind.getProductId());
row.setTermId(bind.getTermId());
row.setVersionNo(term.getVersionNo() == null ? bind.getVersionNo() : term.getVersionNo());
row.setSortNo(sort++);
row.setCreateBy(operator);
row.setUpdateBy(operator);
rows.add(row);
}
ruleMapper.insertBatch(rows);
}
}
private static List<PaySurchargeRule> normalize(List<PaySurchargeRule> raw)
{
List<PaySurchargeRule> rules = new ArrayList<>();
if (raw == null)
{
return rules;
}
for (PaySurchargeRule rule : raw)
{
if (rule == null)
{
continue;
}
String calcRule = rule.getCalcRule() == null ? "" : rule.getCalcRule().trim().toUpperCase(Locale.ROOT);
if (!RULES.contains(calcRule))
{
throw new ServiceException("计费条件不在可选范围内");
}
PaySurchargeRule row = new PaySurchargeRule();
row.setCalcRule(calcRule);
row.setEnabled("0".equals(rule.getEnabled()) ? "0" : "1");
Map<String, Object> detail = cleanDetail(calcRule, rule.getDetail());
row.setDetail(detail);
row.setRuleDetail(detail.isEmpty() ? null : JSON.toJSONString(detail));
row.setCalcParam(trim(summary(calcRule, detail), 2000));
row.setPostcodeParam(trim(text(detail.get("postcodes")), 2000));
row.setZoneName(trim(text(detail.get("zoneName")), 200));
row.setWeightMin(weightMin(calcRule, detail));
row.setWeightMax(decimal(detail.get("weightMax"), "重量上限"));
rules.add(row);
}
return rules;
}
private static void hydrate(PaySurchargeRule row)
{
Map<String, Object> detail = parse(row.getRuleDetail());
if (detail.isEmpty())
{
detail = legacy(row);
}
row.setDetail(detail.isEmpty() ? null : detail);
}
private static Map<String, Object> parse(String json)
{
if (json == null || json.isBlank())
{
return new LinkedHashMap<>();
}
Map<String, Object> parsed = JSON.parseObject(json, new TypeReference<LinkedHashMap<String, Object>>() { });
return parsed == null ? new LinkedHashMap<>() : parsed;
}
/** 还没写明细的旧行,用原来的摘要列带回弹框。 */
private static Map<String, Object> legacy(PaySurchargeRule row)
{
Map<String, Object> detail = new LinkedHashMap<>();
String calc = row.getCalcParam();
String postcode = row.getPostcodeParam();
if ("POSTCODE".equals(row.getCalcRule()))
{
put(detail, "postcodeMode", "PREFIX");
put(detail, "postcodes", postcode == null || postcode.isBlank() ? calc : postcode);
}
else if ("ZONE".equals(row.getCalcRule()))
{
put(detail, "zoneName", row.getZoneName());
}
else if ("EXTRA_KG".equals(row.getCalcRule()))
{
put(detail, "thresholdKg", row.getWeightMin() == null ? calc : row.getWeightMin());
put(detail, "postcodes", postcode);
put(detail, "weightBasis", "ACTUAL");
}
else if ("WEIGHT".equals(row.getCalcRule()))
{
put(detail, "weightMin", row.getWeightMin() == null ? calc : row.getWeightMin());
put(detail, "weightBasis", "ACTUAL");
}
else if ("SIZE".equals(row.getCalcRule()))
{
put(detail, "matchMode", "ANY");
put(detail, "remark", calc);
}
else
{
put(detail, "remark", calc);
}
if (row.getWeightMin() != null)
{
detail.putIfAbsent("weightMin", row.getWeightMin());
}
if (row.getWeightMax() != null)
{
detail.put("weightMax", row.getWeightMax());
}
return detail;
}
private static Map<String, Object> cleanDetail(String calcRule, Map<String, Object> raw)
{
Map<String, Object> detail = new LinkedHashMap<>();
if (raw != null)
{
raw.forEach((key, value) -> put(detail, key, value));
}
checkChoice(detail, "matchMode", Set.of("ANY", "ALL"), "命中方式");
checkChoice(detail, "postcodeMode", Set.of("PREFIX", "EXACT"), "邮编匹配");
checkChoice(detail, "weightBasis", Set.of("ACTUAL", "CHARGE"), "重量口径");
checkChoice(detail, "rateMode", Set.of("FIXED", "RATIO"), "计费方式");
checkChoice(detail, "chargeBase", Set.of("HANDLE", "DELIVERY", "FREIGHT_FEE"), "计费基数");
checkChoice(detail, "chargeMode", Set.of("SAME", "AIR", "ROAD", "FIXED"), "退回方式");
checkChoice(detail, "boxMode", Set.of("OVER", "UNDER"), "箱规比较");
for (String key : NUMBERS)
{
if (!detail.containsKey(key))
{
continue;
}
BigDecimal number = decimal(detail.get(key), numberLabel(key));
if (number == null)
{
detail.remove(key);
}
else
{
detail.put(key, number);
}
}
if ("SIZE".equals(calcRule))
{
require(detail.get("matchMode") != null, "超尺寸要选择任一命中或全部命中");
require(hasSize(detail), "超尺寸至少要填最长边、次长边、三边之和、围长、箱规、体积或超出步长");
require(boxComplete(detail), "箱规的长、宽、高要一起填");
}
else if ("WEIGHT".equals(calcRule))
{
require(detail.get("weightMin") != null || detail.get("weightMax") != null, "超重要填写重量下限或上限");
}
else if ("EXTRA_KG".equals(calcRule))
{
require(detail.get("thresholdKg") != null, "超重续收要填写超过多少公斤后开始续收");
}
else if ("POSTCODE".equals(calcRule) || "ZONE".equals(calcRule))
{
detail.clear();
}
else if ("RETURN".equals(calcRule))
{
require(detail.get("chargeMode") != null, "退件要选择退回方式");
require(boxComplete(detail), "箱规的长、宽、高要一起填");
}
return detail;
}
private static boolean hasSize(Map<String, Object> detail)
{
return present(detail, "longestMin", "longestMax", "secondMin", "secondMax", "sumMin", "sumMax",
"girthMin", "girthMax", "boxLength", "volumeMin", "volumeMax", "cubicMax", "exceedStepCm");
}
private static boolean boxComplete(Map<String, Object> detail)
{
boolean any = present(detail, "boxLength", "boxWidth", "boxHeight");
boolean all = detail.get("boxLength") != null && detail.get("boxWidth") != null && detail.get("boxHeight") != null;
return !any || all;
}
private static boolean present(Map<String, Object> detail, String... keys)
{
for (String key : keys)
{
if (detail.get(key) != null)
{
return true;
}
}
return false;
}
private static BigDecimal weightMin(String calcRule, Map<String, Object> detail)
{
if ("EXTRA_KG".equals(calcRule) && detail.get("thresholdKg") != null)
{
return (BigDecimal) detail.get("thresholdKg");
}
return (BigDecimal) detail.get("weightMin");
}
private static String summary(String calcRule, Map<String, Object> detail)
{
if ("POSTCODE".equals(calcRule))
{
return "按应付邮编配置判断,邮编前缀和完整邮编都认";
}
if ("ZONE".equals(calcRule))
{
return "按应付邮编配置的分区判断";
}
List<String> parts = new ArrayList<>();
add(parts, choice(detail.get("matchMode"), Map.of("ANY", "任一命中", "ALL", "全部命中")));
add(parts, choice(detail.get("postcodeMode"), Map.of("PREFIX", "邮编开头", "EXACT", "完整邮编")));
add(parts, span("最长边", detail.get("longestMin"), detail.get("longestMax"), "cm"));
add(parts, span("次长边", detail.get("secondMin"), detail.get("secondMax"), "cm"));
add(parts, span("三边之和", detail.get("sumMin"), detail.get("sumMax"), "cm"));
add(parts, span("围长", detail.get("girthMin"), detail.get("girthMax"), "cm"));
if (detail.get("boxLength") != null)
{
String mode = "UNDER".equals(detail.get("boxMode")) ? "小于" : "超过";
parts.add(mode + "箱规" + plain(detail.get("boxLength")) + "×" + plain(detail.get("boxWidth")) + "×" + plain(detail.get("boxHeight")) + "cm");
}
add(parts, span("体积", detail.get("volumeMin"), detail.get("volumeMax"), "m³"));
if (detail.get("cubicMax") != null)
{
parts.add("长×宽×高≤" + plain(detail.get("cubicMax")) + "cm³");
}
if (detail.get("exceedStepCm") != null)
{
parts.add("每超出" + plain(detail.get("exceedStepCm")) + "cm");
}
if (detail.get("exceedRatio") != null)
{
parts.add("运费加收" + plain(detail.get("exceedRatio")) + "%");
}
add(parts, span("重量", detail.get("weightMin"), detail.get("weightMax"), "kg"));
add(parts, choice(detail.get("weightBasis"), Map.of("ACTUAL", "实重", "CHARGE", "计费重")));
if (detail.get("thresholdKg") != null)
{
parts.add("超过" + plain(detail.get("thresholdKg")) + "kg后续收");
}
if (detail.get("stepKg") != null)
{
parts.add("每" + plain(detail.get("stepKg")) + "kg");
}
if (detail.get("minCharge") != null)
{
parts.add("最低" + plain(detail.get("minCharge")));
}
add(parts, text(detail.get("zoneName")));
add(parts, text(detail.get("postcodes")));
add(parts, text(detail.get("countries")));
add(parts, choice(detail.get("chargeMode"), Map.of("SAME", "同正向运费", "AIR", "空运退回", "ROAD", "陆运退回", "FIXED", "固定金额")));
add(parts, choice(detail.get("rateMode"), Map.of("FIXED", "固定金额", "RATIO", "按比例")));
if (detail.get("ratio") != null)
{
parts.add(plain(detail.get("ratio")) + "%");
}
String period = joinPeriod(text(detail.get("periodStart")), text(detail.get("periodEnd")));
add(parts, period);
if ("ALWAYS".equals(calcRule) || "RETURN".equals(calcRule) || "SIZE".equals(calcRule))
{
add(parts, text(detail.get("remark")));
}
return parts.isEmpty() ? null : String.join(";", parts);
}
private static String span(String name, Object min, Object max, String unit)
{
if (min == null && max == null)
{
return null;
}
if (min != null && max != null)
{
return name + " " + plain(min) + "<x≤" + plain(max) + unit;
}
if (min != null)
{
return name + ">" + plain(min) + unit;
}
return name + "≤" + plain(max) + unit;
}
private static String joinPeriod(String start, String end)
{
if (start == null && end == null)
{
return null;
}
return (start == null ? "不限" : start) + "至" + (end == null ? "不限" : end);
}
private static String choice(Object value, Map<String, String> labels)
{
String text = text(value);
return text == null ? null : labels.get(text);
}
private static void checkChoice(Map<String, Object> detail, String key, Set<String> allowed, String label)
{
String value = text(detail.get(key));
if (value == null)
{
detail.remove(key);
return;
}
if (!allowed.contains(value))
{
throw new ServiceException(label + "不在可选范围内");
}
detail.put(key, value);
}
private static void put(Map<String, Object> detail, String key, Object value)
{
if (value == null)
{
return;
}
if (value instanceof String text)
{
if (text.isBlank())
{
return;
}
if (text.length() > 2000)
{
throw new ServiceException("条件内容不能超过2000字");
}
detail.put(key, text.trim());
return;
}
detail.put(key, value);
}
private static BigDecimal decimal(Object value, String label)
{
if (value == null || (value instanceof String text && text.isBlank()))
{
return null;
}
try
{
return new BigDecimal(String.valueOf(value).trim());
}
catch (NumberFormatException ex)
{
throw new ServiceException(label + "要填写数字");
}
}
private static String numberLabel(String key)
{
return switch (key)
{
case "longestMin", "longestMax" -> "最长边";
case "secondMin", "secondMax" -> "次长边";
case "sumMin", "sumMax" -> "三边之和";
case "girthMin", "girthMax" -> "围长";
case "boxLength" -> "箱规长";
case "boxWidth" -> "箱规宽";
case "boxHeight" -> "箱规高";
case "volumeMin", "volumeMax" -> "体积";
case "cubicMax" -> "长宽高乘积";
case "exceedStepCm" -> "超出步长";
case "exceedRatio", "ratio" -> "比例";
case "thresholdKg", "weightMin", "weightMax", "stepKg" -> "重量";
case "minCharge" -> "最低消费";
default -> "数值";
};
}
private static String text(Object value)
{
if (value == null)
{
return null;
}
String text = String.valueOf(value).trim();
return text.isEmpty() ? null : text;
}
private static String plain(Object value)
{
if (value instanceof BigDecimal number)
{
return number.stripTrailingZeros().toPlainString();
}
return text(value);
}
private static void add(List<String> parts, String value)
{
if (value != null && !value.isBlank())
{
parts.add(value);
}
}
private static void require(boolean ok, String message)
{
if (!ok)
{
throw new ServiceException(message);
}
}
private static PaySurchargeRule copy(PaySurchargeRule rule)
{
PaySurchargeRule row = new PaySurchargeRule();
row.setCalcRule(rule.getCalcRule());
row.setEnabled(rule.getEnabled());
row.setCalcParam(rule.getCalcParam());
row.setRuleDetail(rule.getRuleDetail());
row.setDetail(rule.getDetail());
row.setPostcodeParam(rule.getPostcodeParam());
row.setZoneName(rule.getZoneName());
row.setWeightMin(rule.getWeightMin());
row.setWeightMax(rule.getWeightMax());
return row;
}
private static String trim(String value, int max)
{
if (value == null)
{
return null;
}
String text = value.trim();
if (text.isEmpty())
{
return null;
}
if (text.length() > max)
{
throw new ServiceException("条件内容不能超过" + max + "字");
}
return text;
}
}
@@ -5,8 +5,13 @@
<select id="selectPayBillList" resultType="com.bms.price.domain.PayBill">
select bill_id as billId, bill_no as billNo, supplier_id as supplierId, supplier_name as supplierName,
currency, source_type as sourceType, period_start as periodStart, period_end as periodEnd,
order_count as orderCount, total_amount as totalAmount, status, void_reason as voidReason,
void_by as voidBy, void_time as voidTime, create_by as createBy, create_time as createTime, remark
order_count as orderCount, total_amount as totalAmount, supplier_amount as supplierAmount,
diff_amount as diffAmount, paid_amount as paidAmount, due_date as dueDate, status,
bill_kind as billKind, original_bill_id as originalBillId, void_reason as voidReason,
void_by as voidBy, void_time as voidTime, confirm_by as confirmBy, confirm_time as confirmTime,
tax_rate as taxRate, amount_net as amountNet, tax_amount as taxAmount, exchange_rate as exchangeRate,
base_amount as baseAmount, rate_date as rateDate, term_days as termDays, submit_by as submitBy,
create_by as createBy, create_time as createTime, remark
from pay_bill
<where>
<if test="billNo != null and billNo != ''">and bill_no like concat('%', #{billNo}, '%')</if>
@@ -20,22 +25,30 @@
<select id="selectPayBillById" resultType="com.bms.price.domain.PayBill">
select bill_id as billId, bill_no as billNo, supplier_id as supplierId, supplier_name as supplierName,
currency, source_type as sourceType, period_start as periodStart, period_end as periodEnd,
order_count as orderCount, total_amount as totalAmount, status, void_reason as voidReason,
void_by as voidBy, void_time as voidTime, create_by as createBy, create_time as createTime, remark
order_count as orderCount, total_amount as totalAmount, supplier_amount as supplierAmount,
diff_amount as diffAmount, paid_amount as paidAmount, due_date as dueDate, status,
bill_kind as billKind, original_bill_id as originalBillId, void_reason as voidReason,
void_by as voidBy, void_time as voidTime, confirm_by as confirmBy, confirm_time as confirmTime,
tax_rate as taxRate, amount_net as amountNet, tax_amount as taxAmount, exchange_rate as exchangeRate,
base_amount as baseAmount, rate_date as rateDate, term_days as termDays, submit_by as submitBy,
create_by as createBy, create_time as createTime, remark
from pay_bill where bill_id = #{billId}
</select>
<select id="selectLines" resultType="com.bms.price.domain.PayBillLine">
select line_id as lineId, bill_id as billId, order_bill_id as orderBillId, order_no as orderNo,
lading_no as ladingNo, lading_fee_id as ladingFeeId, fee_name as feeName, amount, currency,
price_date as priceDate, version_no as versionNo, remote_flag as remoteFlag,
density_level as densityLevel, line_source as lineSource
select line_id as lineId, bill_id as billId, fee_id as feeId, order_bill_id as orderBillId, order_no as orderNo,
lading_no as ladingNo, lading_fee_id as ladingFeeId, fee_name as feeName, fee_source as feeSource,
fee_level as feeLevel, amount, supplier_amount as supplierAmount, diff_amount as diffAmount,
diff_status as diffStatus, currency, price_date as priceDate, version_no as versionNo,
remote_flag as remoteFlag, density_level as densityLevel, line_source as lineSource
from pay_bill_line where bill_id = #{billId} order by line_id
</select>
<select id="selectLogs" resultType="com.bms.price.domain.PayBillLog">
select log_id as logId, bill_id as billId, operation_type as operationType, from_status as fromStatus,
to_status as toStatus, operation_desc as operationDesc, operator_name as operatorName, create_time as createTime
select log_id as logId, bill_id as billId, biz_type as bizType, biz_id as bizId, line_id as lineId,
fee_id as feeId, operation_type as operationType, from_status as fromStatus, to_status as toStatus,
operation_desc as operationDesc, amount_before as amountBefore, amount_after as amountAfter,
operator_name as operatorName, create_time as createTime
from pay_bill_log where bill_id = #{billId} order by log_id
</select>
@@ -48,9 +61,21 @@
(select group_concat(concat(ifnull(nullif(trim(f.fee_name), ''), ifnull(f.fee_type, '费用')), ' ', format(f.amount, 2))
order by field(f.fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), f.fee_id separator ';')
from prc_pay_bill_fee f where f.bill_id = o.bill_id and f.is_deleted = '0') as feeText,
(select count(*) from prc_pay_bill_fee f
where f.bill_id = o.bill_id and ifnull(f.is_deleted, '0') = '0') as feeCount,
(select count(*) from prc_pay_bill_fee f
where f.bill_id = o.bill_id and ifnull(f.is_deleted, '0') = '0'
and f.statement_line_id is null
and not exists (
select 1 from pay_bill_line l
inner join pay_bill b on b.bill_id = l.bill_id
where l.order_bill_id = o.bill_id
and (l.fee_id is null or l.fee_id = '')
and b.status not in ('VOID', 'RED')
)) as unbilledCount,
(select b.bill_no from pay_bill_line l
inner join pay_bill b on b.bill_id = l.bill_id
where l.order_bill_id = o.bill_id and b.status &lt;&gt; 'VOID'
where l.order_bill_id = o.bill_id and b.status not in ('VOID', 'RED')
limit 1) as occupiedBillNo
from prc_order_bill o
left join prc_order_bill_ext e on e.bill_id = o.bill_id
@@ -81,8 +106,9 @@
</select>
<select id="selectOrderFees" resultType="com.bms.price.domain.PrcPayBillFee">
select fee_id as feeId, bill_id as billId, fee_name as feeName, amount, currency, fee_source as feeSource,
source_id as sourceId, is_deleted as isDeleted
select fee_id as feeId, bill_id as billId, pay_bill_id as payBillId, fee_type as feeType, fee_name as feeName,
sc_id as scId, fee_level as feeLevel, amount, currency, fee_source as feeSource, source_id as sourceId,
is_deleted as isDeleted
from prc_pay_bill_fee
where is_deleted = '0'
and bill_id in
@@ -93,10 +119,15 @@
<insert id="insertPayBill" useGeneratedKeys="true" keyProperty="billId">
insert into pay_bill (
bill_no, supplier_id, supplier_name, currency, source_type, period_start, period_end,
order_count, total_amount, status, create_by, create_time, remark
order_count, total_amount, supplier_amount, diff_amount, paid_amount, due_date, status, bill_kind,
original_bill_id, tax_rate, amount_net, tax_amount, exchange_rate, base_amount, rate_date, term_days,
create_by, create_time, remark
) values (
#{billNo}, #{supplierId}, #{supplierName}, #{currency}, #{sourceType}, #{periodStart}, #{periodEnd},
#{orderCount}, #{totalAmount}, 'DRAFT', #{createBy}, sysdate(), #{remark}
#{orderCount}, #{totalAmount}, #{supplierAmount}, #{diffAmount}, #{paidAmount}, #{dueDate}, 'DRAFT',
ifnull(#{billKind}, 'BLUE'), #{originalBillId}, ifnull(#{taxRate}, 0), ifnull(#{amountNet}, #{totalAmount}),
ifnull(#{taxAmount}, 0), ifnull(#{exchangeRate}, 1), ifnull(#{baseAmount}, #{totalAmount}), #{rateDate},
#{termDays}, #{createBy}, sysdate(), #{remark}
)
</insert>
@@ -113,18 +144,245 @@
<insert id="insertLines">
insert into pay_bill_line (
bill_id, order_bill_id, order_no, lading_no, lading_fee_id, fee_name, amount, currency,
price_date, version_no, remote_flag, density_level, line_source
bill_id, fee_id, order_bill_id, order_no, lading_no, lading_fee_id, fee_name, fee_source, fee_level,
amount, supplier_amount, diff_amount, diff_status, currency, price_date, version_no, remote_flag,
density_level, line_source
) values
<foreach collection="list" item="item" separator=",">
(#{item.billId}, #{item.orderBillId}, #{item.orderNo}, #{item.ladingNo}, #{item.ladingFeeId},
#{item.feeName}, #{item.amount}, #{item.currency}, #{item.priceDate}, #{item.versionNo},
#{item.remoteFlag}, #{item.densityLevel}, #{item.lineSource})
(#{item.billId}, #{item.feeId}, #{item.orderBillId}, #{item.orderNo}, #{item.ladingNo}, #{item.ladingFeeId},
#{item.feeName}, #{item.feeSource}, #{item.feeLevel}, #{item.amount}, #{item.supplierAmount},
#{item.diffAmount}, ifnull(#{item.diffStatus}, 'NONE'), #{item.currency}, #{item.priceDate},
#{item.versionNo}, #{item.remoteFlag}, #{item.densityLevel}, #{item.lineSource})
</foreach>
</insert>
<insert id="insertLine" useGeneratedKeys="true" keyProperty="lineId">
insert into pay_bill_line (
bill_id, fee_id, order_bill_id, order_no, lading_no, lading_fee_id, fee_name, fee_source, fee_level,
amount, supplier_amount, diff_amount, diff_status, currency, price_date, version_no, remote_flag,
density_level, line_source
) values (
#{billId}, #{feeId}, #{orderBillId}, #{orderNo}, #{ladingNo}, #{ladingFeeId}, #{feeName}, #{feeSource},
#{feeLevel}, #{amount}, #{supplierAmount}, #{diffAmount}, ifnull(#{diffStatus}, 'NONE'), #{currency},
#{priceDate}, #{versionNo}, #{remoteFlag}, #{densityLevel}, #{lineSource}
)
</insert>
<update id="bindFeeLine">
update prc_pay_bill_fee
set statement_line_id = #{lineId}
where fee_id = #{feeId}
and statement_line_id is null
and ifnull(is_deleted, '0') = '0'
</update>
<update id="clearFeeLine">
update prc_pay_bill_fee set statement_line_id = null where statement_line_id = #{lineId}
</update>
<update id="clearFeeLines">
update prc_pay_bill_fee f
inner join pay_bill_line l on l.line_id = f.statement_line_id
set f.statement_line_id = null
where l.bill_id = #{billId}
</update>
<update id="updatePayBillMoney">
update pay_bill
set total_amount = #{totalAmount},
supplier_amount = #{supplierAmount},
diff_amount = #{diffAmount},
paid_amount = #{paidAmount},
order_count = #{orderCount},
status = #{status},
confirm_by = #{confirmBy},
confirm_time = #{confirmTime},
tax_rate = ifnull(#{taxRate}, 0),
amount_net = ifnull(#{amountNet}, #{totalAmount}),
tax_amount = ifnull(#{taxAmount}, 0),
exchange_rate = ifnull(#{exchangeRate}, 1),
base_amount = ifnull(#{baseAmount}, #{totalAmount}),
rate_date = #{rateDate},
term_days = #{termDays},
update_by = #{updateBy},
update_time = sysdate()
where bill_id = #{billId}
</update>
<update id="updateLineAmount">
update pay_bill_line
set supplier_amount = #{supplierAmount},
diff_amount = #{diffAmount},
diff_status = #{diffStatus}
where line_id = #{lineId}
and bill_id = #{billId}
</update>
<delete id="deleteOpenDiff">
delete from pay_bill_diff where line_id = #{lineId} and handle_type = 'OPEN'
</delete>
<delete id="deleteLine">
delete from pay_bill_line where bill_id = #{billId} and line_id = #{lineId}
</delete>
<select id="selectFreeFees" resultType="com.bms.price.domain.PrcPayBillFee">
select f.fee_id as feeId, f.bill_id as billId, f.pay_bill_id as payBillId, f.fee_type as feeType,
f.fee_name as feeName, f.sc_id as scId, f.fee_level as feeLevel, f.amount, f.currency,
f.fee_source as feeSource, f.source_id as sourceId, f.settle_supplier_id as settleSupplierId,
f.settle_supplier_name as settleSupplierName
from prc_pay_bill_fee f
where ifnull(f.is_deleted, '0') = '0'
and f.statement_line_id is null
and f.bill_id in
<foreach collection="billIds" item="id" open="(" separator="," close=")">#{id}</foreach>
and not exists (
select 1 from pay_bill_line l
inner join pay_bill b on b.bill_id = l.bill_id
where l.order_bill_id = f.bill_id
and (l.fee_id is null or l.fee_id = '')
and b.status not in ('VOID', 'RED')
)
order by f.bill_id, f.fee_id
</select>
<select id="countCalcFees" resultType="int">
select count(*)
from prc_pay_bill_fee
where bill_id = #{billId}
and ifnull(is_deleted, '0') = '0'
and fee_source = 'CALC'
</select>
<select id="selectDiffs" resultType="com.bms.price.domain.PayBillDiff">
select diff_id as diffId, bill_id as billId, line_id as lineId, fee_id as feeId, our_amount as ourAmount,
supplier_amount as supplierAmount, diff_amount as diffAmount, reason, handle_type as handleType,
adjust_fee_id as adjustFeeId, create_by as createBy, create_time as createTime
from pay_bill_diff where bill_id = #{billId} order by diff_id
</select>
<select id="selectFiles" resultType="com.bms.price.domain.PayBillFile">
select file_id as fileId, bill_id as billId, file_type as fileType, invoice_no as invoiceNo,
invoice_date as invoiceDate, amount, tax_amount as taxAmount, amount_net as amountNet,
seller_name as sellerName, matched_amount as matchedAmount, currency, file_name as fileName,
file_url as fileUrl, deleted, create_by as createBy, create_time as createTime
from pay_bill_file
where bill_id = #{billId} and deleted = '0'
order by file_id
</select>
<select id="selectApprovals" resultType="com.bms.price.domain.PayBillApproval">
select approval_id as approvalId, bill_id as billId, action, opinion, operator_name as operatorName,
create_time as createTime
from pay_bill_approval where bill_id = #{billId} order by approval_id
</select>
<select id="selectLine" resultType="com.bms.price.domain.PayBillLine">
select line_id as lineId, bill_id as billId, fee_id as feeId, order_bill_id as orderBillId, order_no as orderNo,
lading_no as ladingNo, fee_name as feeName, fee_source as feeSource, fee_level as feeLevel, amount,
supplier_amount as supplierAmount, diff_amount as diffAmount, diff_status as diffStatus, currency,
line_source as lineSource
from pay_bill_line where line_id = #{lineId}
</select>
<select id="selectOpenDiff" resultType="com.bms.price.domain.PayBillDiff">
select diff_id as diffId, bill_id as billId, line_id as lineId, fee_id as feeId, our_amount as ourAmount,
supplier_amount as supplierAmount, diff_amount as diffAmount, reason, handle_type as handleType,
adjust_fee_id as adjustFeeId
from pay_bill_diff
where line_id = #{lineId} and handle_type = 'OPEN'
order by diff_id desc
limit 1
</select>
<insert id="insertDiff" useGeneratedKeys="true" keyProperty="diffId">
insert into pay_bill_diff (
bill_id, line_id, fee_id, our_amount, supplier_amount, diff_amount, reason, handle_type, adjust_fee_id,
create_by, create_time
) values (
#{billId}, #{lineId}, #{feeId}, #{ourAmount}, #{supplierAmount}, #{diffAmount}, #{reason}, #{handleType},
#{adjustFeeId}, #{createBy}, sysdate()
)
</insert>
<update id="updateDiff">
update pay_bill_diff
set our_amount = #{ourAmount},
supplier_amount = #{supplierAmount},
diff_amount = #{diffAmount},
reason = #{reason},
handle_type = #{handleType},
adjust_fee_id = #{adjustFeeId}
where diff_id = #{diffId}
</update>
<delete id="deleteDiffsByLine">
delete from pay_bill_diff where line_id = #{lineId}
</delete>
<insert id="insertFile" useGeneratedKeys="true" keyProperty="fileId">
insert into pay_bill_file (
bill_id, file_type, invoice_no, invoice_date, amount, tax_amount, amount_net, seller_name, matched_amount,
currency, file_name, file_url, deleted, create_by, create_time
) values (
#{billId}, #{fileType}, #{invoiceNo}, #{invoiceDate}, #{amount}, #{taxAmount}, #{amountNet}, #{sellerName},
ifnull(#{matchedAmount}, 0), #{currency}, #{fileName}, #{fileUrl}, '0', #{createBy}, sysdate()
)
</insert>
<select id="selectFile" resultType="com.bms.price.domain.PayBillFile">
select file_id as fileId, bill_id as billId, file_type as fileType, invoice_no as invoiceNo,
file_name as fileName, file_url as fileUrl, deleted
from pay_bill_file where file_id = #{fileId}
</select>
<update id="markFileDeleted">
update pay_bill_file set deleted = '1' where file_id = #{fileId}
</update>
<insert id="insertApproval">
insert into pay_bill_approval (bill_id, action, opinion, operator_name, create_time)
values (#{billId}, #{action}, #{opinion}, #{operatorName}, sysdate())
</insert>
<select id="selectFileList" resultType="com.bms.price.domain.PayBillFile">
select f.file_id as fileId, f.bill_id as billId, b.bill_no as billNo, b.supplier_name as supplierName,
f.file_type as fileType, f.invoice_no as invoiceNo, f.invoice_date as invoiceDate, f.amount,
f.tax_amount as taxAmount, f.file_name as fileName, f.file_url as fileUrl, f.create_by as createBy,
f.create_time as createTime
from pay_bill_file f
inner join pay_bill b on b.bill_id = f.bill_id
where f.deleted = '0'
<if test="billNo != null and billNo != ''">and b.bill_no like concat('%', #{billNo}, '%')</if>
<if test="supplierName != null and supplierName != ''">and b.supplier_name like concat('%', #{supplierName}, '%')</if>
<if test="invoiceNo != null and invoiceNo != ''">and f.invoice_no like concat('%', #{invoiceNo}, '%')</if>
<if test="fileType != null and fileType != ''">and f.file_type = #{fileType}</if>
order by f.file_id desc
</select>
<select id="countActiveFiles" resultType="int">
select count(*) from pay_bill_file where bill_id = #{billId} and deleted = '0'
</select>
<select id="selectOpenBills" resultType="com.bms.price.domain.PayPaymentAlloc">
select bill_id as billId, bill_no as billNo, currency, supplier_id as supplierId, supplier_name as supplierName,
(total_amount - ifnull(paid_amount, 0)) as openAmount
from pay_bill
where supplier_id = #{supplierId}
and currency = #{currency}
and status in ('CONFIRMED', 'PARTIAL')
and ifnull(bill_kind, 'BLUE') &lt;&gt; 'RED'
and total_amount - ifnull(paid_amount, 0) &gt; 0
order by bill_id
</select>
<insert id="insertLog">
insert into pay_bill_log (bill_id, operation_type, from_status, to_status, operation_desc, operator_name, create_time)
values (#{billId}, #{operationType}, #{fromStatus}, #{toStatus}, #{operationDesc}, #{operatorName}, sysdate())
insert into pay_bill_log (
bill_id, biz_type, biz_id, line_id, fee_id, operation_type, from_status, to_status, operation_desc,
amount_before, amount_after, operator_name, create_time
) values (
#{billId}, #{bizType}, #{bizId}, #{lineId}, #{feeId}, #{operationType}, #{fromStatus}, #{toStatus},
#{operationDesc}, #{amountBefore}, #{amountAfter}, #{operatorName}, sysdate()
)
</insert>
</mapper>
@@ -0,0 +1,71 @@
<?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.price.mapper.PayFinanceMapper">
<update id="updateFinanceHead">
update pay_bill
set tax_rate = #{taxRate},
exchange_rate = #{exchangeRate},
rate_date = #{rateDate},
update_time = sysdate()
where bill_id = #{billId}
</update>
<update id="updateSubmitBy">
update pay_bill set submit_by = #{submitBy} where bill_id = #{billId}
</update>
<update id="updateLineTax">
update pay_bill_line
set tax_rate = #{taxRate},
amount_net = round(amount / (1 + #{taxRate}), 4),
tax_amount = round(amount - amount / (1 + #{taxRate}), 4),
base_amount = round(amount * #{exchangeRate}, 4)
where bill_id = #{billId}
</update>
<select id="countDuplicateInvoice" resultType="int">
select count(*)
from pay_bill_file f
inner join pay_bill b on b.bill_id = f.bill_id
where f.deleted = '0'
and f.file_type = 'INVOICE'
and f.invoice_no = #{invoiceNo}
and b.supplier_id = #{supplierId}
and b.status &lt;&gt; 'VOID'
<if test="fileId != null">and f.file_id &lt;&gt; #{fileId}</if>
</select>
<update id="markInvoiceMatched">
update pay_bill_file
set matched_amount = ifnull(amount, 0)
where bill_id = #{billId}
and deleted = '0'
and file_type = 'INVOICE'
</update>
<select id="selectLadingSupplier" resultType="long">
select supplier_id
from pay_lading
where lading_no = #{ladingNo}
and supplier_id is not null
limit 1
</select>
<update id="stampFee">
update prc_pay_bill_fee
set settle_supplier_id = #{supplierId},
settle_supplier_name = #{supplierName}
where fee_id = #{feeId}
</update>
<update id="stampCalc">
update prc_pay_bill_fee
set settle_supplier_id = #{supplierId},
settle_supplier_name = #{supplierName}
where bill_id = #{billId}
and ifnull(is_deleted, '0') = '0'
and statement_line_id is null
and (fee_source is null or fee_source = '' or fee_source = 'CALC')
</update>
</mapper>
@@ -0,0 +1,71 @@
<?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.price.mapper.PayPaymentMapper">
<select id="selectPayPaymentList" resultType="com.bms.price.domain.PayPayment">
select payment_id as paymentId, payment_no as paymentNo, supplier_id as supplierId, supplier_name as supplierName,
currency, pay_date as payDate, pay_method as payMethod, bank_account as bankAccount, amount, bank_fee as bankFee,
exchange_rate as exchangeRate, base_amount as baseAmount, alloc_amount as allocAmount,
status, remark, create_by as createBy, create_time as createTime, confirm_by as confirmBy, confirm_time as confirmTime
from pay_payment
<where>
<if test="paymentNo != null and paymentNo != ''">and payment_no like concat('%', #{paymentNo}, '%')</if>
<if test="supplierName != null and supplierName != ''">and supplier_name like concat('%', #{supplierName}, '%')</if>
<if test="status != null and status != ''">and status = #{status}</if>
<if test="currency != null and currency != ''">and currency = #{currency}</if>
</where>
order by payment_id desc
</select>
<select id="selectPayPaymentById" resultType="com.bms.price.domain.PayPayment">
select payment_id as paymentId, payment_no as paymentNo, supplier_id as supplierId, supplier_name as supplierName,
currency, pay_date as payDate, pay_method as payMethod, bank_account as bankAccount, amount, bank_fee as bankFee,
exchange_rate as exchangeRate, base_amount as baseAmount, alloc_amount as allocAmount,
status, remark, create_by as createBy, create_time as createTime, confirm_by as confirmBy, confirm_time as confirmTime
from pay_payment where payment_id = #{paymentId}
</select>
<select id="selectAllocs" resultType="com.bms.price.domain.PayPaymentAlloc">
select a.alloc_id as allocId, a.payment_id as paymentId, a.bill_id as billId, b.bill_no as billNo,
a.alloc_amount as allocAmount, a.alloc_status as allocStatus, b.currency
from pay_payment_alloc a
left join pay_bill b on b.bill_id = a.bill_id
where a.payment_id = #{paymentId}
order by a.alloc_id
</select>
<insert id="insertPayPayment" useGeneratedKeys="true" keyProperty="paymentId">
insert into pay_payment (
payment_no, supplier_id, supplier_name, currency, pay_date, pay_method, bank_account, amount, bank_fee,
exchange_rate, status, remark, create_by, create_time
) values (
#{paymentNo}, #{supplierId}, #{supplierName}, #{currency}, #{payDate}, #{payMethod}, #{bankAccount},
#{amount}, #{bankFee}, ifnull(#{exchangeRate}, 1), 'DRAFT', #{remark}, #{createBy}, sysdate()
)
</insert>
<insert id="insertAlloc">
insert into pay_payment_alloc (payment_id, bill_id, alloc_amount, alloc_status)
values (#{paymentId}, #{billId}, #{allocAmount}, 'ACTIVE')
</insert>
<update id="updatePayPaymentStatus">
update pay_payment
set status = #{status},
confirm_by = #{confirmBy},
confirm_time = #{confirmTime},
exchange_rate = ifnull(#{exchangeRate}, exchange_rate),
base_amount = ifnull(#{baseAmount}, base_amount),
alloc_amount = ifnull(#{allocAmount}, alloc_amount)
where payment_id = #{paymentId}
</update>
<update id="reverseAlloc">
update pay_payment_alloc
set alloc_status = 'REVERSED',
reverse_by = #{reverseBy},
reverse_time = sysdate()
where alloc_id = #{allocId}
and alloc_status = 'ACTIVE'
</update>
</mapper>
@@ -155,8 +155,8 @@
</select>
<select id="selectLinesByGroups" resultType="com.bms.price.domain.PayPostcodeLine">
select line_id as lineId, group_id as groupId, region_name as regionName, zone_name as zoneName,
postcode, postcode_end as postcodeEnd
select line_id as lineId, group_id as groupId, country_name as countryName, country_code as countryCode,
region_name as regionName, zone_name as zoneName, postcode, postcode_end as postcodeEnd
from pay_postcode_line
where group_id in
<foreach collection="groupIds" item="id" open="(" separator="," close=")">#{id}</foreach>
@@ -0,0 +1,46 @@
<?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.price.mapper.PaySurchargeRuleMapper">
<select id="selectByProduct" resultType="com.bms.price.domain.PaySurchargeRule">
select rule_id as ruleId, product_id as productId, term_id as termId, version_no as versionNo,
calc_rule as calcRule, enabled, calc_param as calcParam, rule_detail as ruleDetail, postcode_param as postcodeParam,
zone_name as zoneName, weight_min as weightMin, weight_max as weightMax, sort_no as sortNo
from pay_surcharge_rule
where product_id = #{productId}
and term_id = #{termId}
order by calc_rule, sort_no, rule_id
</select>
<delete id="deleteByTerm">
delete from pay_surcharge_rule
where product_id = #{productId}
and term_id = #{termId}
</delete>
<insert id="insertBatch">
insert into pay_surcharge_rule (
product_id, term_id, version_no, calc_rule, enabled, calc_param, rule_detail, postcode_param,
zone_name, weight_min, weight_max, sort_no, create_by, create_time, update_by, update_time
) values
<foreach collection="list" item="row" separator=",">
(#{row.productId}, #{row.termId}, ifnull(#{row.versionNo}, 1), #{row.calcRule}, ifnull(#{row.enabled}, '1'),
#{row.calcParam}, #{row.ruleDetail}, #{row.postcodeParam}, #{row.zoneName}, #{row.weightMin}, #{row.weightMax}, ifnull(#{row.sortNo}, 0),
#{row.createBy}, sysdate(), #{row.updateBy}, sysdate())
</foreach>
</insert>
<select id="selectPreviousVersion" resultType="com.bms.price.domain.PaySurchargeRule">
select v.product_id as productId, t.term_id as termId, ifnull(v.version_no, 1) as versionNo
from pay_product cur
join pay_product v on v.dict_value = cur.dict_value
and v.dict_value is not null and v.dict_value &lt;&gt; ''
and v.product_id &lt;&gt; cur.product_id
and (v.channel_id is null or v.channel_id = '')
and ifnull(v.version_no, 1) &lt; ifnull(cur.version_no, 1)
join pay_product_term t on t.product_id = v.product_id and t.price_set_id is null
where cur.product_id = #{productId}
order by ifnull(v.version_no, 1) desc, t.term_id desc
limit 1
</select>
</mapper>