费用明细可以增加费用

This commit is contained in:
wuweihua
2026-09-22 15:30:25 +08:00
parent 77b95f9450
commit 226db7799b
20 changed files with 797 additions and 14 deletions
@@ -568,6 +568,8 @@ public class ArBillServiceImpl implements IArBillService {
if (claimedCount != currentSourceBillIds.size()) {
throw new ServiceException("部分计费订单已被其他账单占用,请重新查询后生成");
}
sourceFeeMapper.updateArBillStatusByBillIds(currentSourceBillIds,
ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.BILLED.getCode());
int detailNo = (task.getProcessedCount() == null ? 0 : task.getProcessedCount()) + 1;
List<ArBillDetail> details = new ArrayList<>(sources.size());
@@ -623,8 +625,18 @@ public class ArBillServiceImpl implements IArBillService {
*/
private void cleanupGenerateTask(ArBillGenerateTask task, String message, boolean canceled,
String operator, String requestId) {
List<String> sourceBillIds = new ArrayList<>();
for (ArBillDetail detail : detailMapper.selectByBillId(task.getArBillId())) {
if (!StringUtils.isEmpty(detail.getPrcOrderBillId())) {
sourceBillIds.add(detail.getPrcOrderBillId());
}
}
sourceBillMapper.releaseArBillOrdersByArBillId(task.getArBillId(), operator,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
if (!sourceBillIds.isEmpty()) {
sourceFeeMapper.updateArBillStatusByBillIds(sourceBillIds,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
}
detailFeeMapper.deleteByBillId(task.getArBillId());
detailMapper.deleteByBillId(task.getArBillId());
billMapper.updateArBillGenerationStatus(task.getArBillId(), ArBillGenerationStatus.FAILED, operator, message);
@@ -788,6 +800,8 @@ public class ArBillServiceImpl implements IArBillService {
bill.getStatus(),
"部分来源订单状态异常,账单未作废,请刷新后重试", operator, requestId);
}
sourceFeeMapper.updateArBillStatusByBillIds(sourceBillIds,
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
}
recordSuccess(bill, ArLogObjectType.BILL, ArLogOperationType.VOID,
"作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, operator);
@@ -23,6 +23,10 @@ public class PrcIncomeBillFee implements Serializable
private String currency;
private String roundMode;
private String remark;
/** 是否生成账单状态:UNBILLED/BILLED */
private String arBillStatus;
/** 是否手动增加:0否 1是 */
private String manualFlag;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,32 @@
package com.ruoyi.price.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/** Manual add receivable fee line on billing detail. */
@Data
public class AddIncomeBillFeeRequest implements Serializable
{
private static final long serialVersionUID = 1L;
@NotBlank(message = "billId required")
private String billId;
/** Surcharge config id (prc_surcharge.sc_id). */
@NotNull(message = "scId required")
private Long scId;
/** Dict bms_fee_type; empty = use surcharge.feeType. */
private String feeType;
/** Empty = calculate from surcharge chargeType. */
private BigDecimal amount;
/** Default CNY. */
private String currency;
private String remark;
}
@@ -10,6 +10,7 @@ public class TmsPriceQuoteFeeLine implements Serializable
{
private static final long serialVersionUID = 1L;
private String feeId;
private String feeType;
private String feeName;
/** surcharge config id, 0 for freight/registration */
@@ -18,4 +19,8 @@ public class TmsPriceQuoteFeeLine implements Serializable
private String currency;
private String roundMode;
private String remark;
/** UNBILLED / BILLED */
private String arBillStatus;
/** 0=system 1=manual */
private String manualFlag;
}
@@ -0,0 +1,28 @@
package com.ruoyi.price.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/** Update manual receivable fee line on billing detail. */
@Data
public class UpdateIncomeBillFeeRequest implements Serializable
{
private static final long serialVersionUID = 1L;
@NotBlank(message = "feeId required")
private String feeId;
/** Optional: change surcharge config. */
private Long scId;
/** Dict bms_fee_type. */
private String feeType;
private BigDecimal amount;
private String currency;
private String remark;
}
@@ -11,9 +11,22 @@ public interface PrcIncomeBillFeeMapper
public List<PrcIncomeBillFee> selectByBillIds(@Param("billIds") List<String> billIds);
public PrcIncomeBillFee selectByFeeId(String feeId);
/** Count fees of a bill under given ar_bill_status and fee_type (e.g. BILLED + BASIC_FREIGHT). */
public int countByBillIdAndArBillStatusAndFeeType(@Param("billId") String billId,
@Param("arBillStatus") String arBillStatus, @Param("feeType") String feeType);
public int insertPrcIncomeBillFee(PrcIncomeBillFee fee);
public int insertBatch(@Param("list") List<PrcIncomeBillFee> fees);
public int updatePrcIncomeBillFee(PrcIncomeBillFee fee);
public int updateArBillStatusByBillIds(@Param("billIds") List<String> billIds,
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
public int deleteByFeeId(String feeId);
public int deleteByBillId(String billId);
}
@@ -44,8 +44,11 @@ public class OracleOrderBillMapper
String countryName = firstStr(row, "COUNTRY_NAME", "COUNTRY_EINGTH_NAME", "COUNTRY");
req.setCountryName(countryName);
req.setDestPostcode(firstStr(row, "CONSIGNEE_POSTCODE"));
// Prefer positive weight: Oracle WEIGHT is often 0 while ACTUAL_WEIGHT/PUSH_WEIGHT holds real value.
BigDecimal weight = firstPositiveDecimal(row, "ACTUAL_WEIGHT", "PUSH_WEIGHT", "WEIGHT", "VOLUME_WEIGHT");
// TAB_ORDER.DIMENSIONAL_WEIGHT_FACTOR = billing weight (计费重) -> quote weight / prc_order_bill.charge_weight
BigDecimal chargeWeight = firstPositiveDecimal(row, "DIMENSIONAL_WEIGHT_FACTOR");
BigDecimal weight = chargeWeight != null
? chargeWeight
: firstPositiveDecimal(row, "ACTUAL_WEIGHT", "PUSH_WEIGHT", "WEIGHT", "VOLUME_WEIGHT");
req.setWeight(weight);
req.setActualWeight(firstPositiveDecimal(row, "ACTUAL_WEIGHT", "PUSH_WEIGHT", "WEIGHT"));
req.setPieces(firstInt(row, "ORDER_PIECE", "PACKAGE_NUM", "PRODUCT_SUM"));
@@ -2,6 +2,8 @@ package com.ruoyi.price.service;
import java.util.List;
import com.ruoyi.price.domain.PrcOrderBill;
import com.ruoyi.price.domain.vo.AddIncomeBillFeeRequest;
import com.ruoyi.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
import com.ruoyi.price.domain.vo.TmsOrderBillStatusResult;
@@ -26,6 +28,15 @@ public interface IPrcOrderBillService
TmsPriceQuoteResult getBillByOrderNo(String orderNo);
/** Manual add income fee from surcharge config; refresh receivable totals. */
TmsPriceQuoteResult addIncomeFee(AddIncomeBillFeeRequest request);
/** Update manual UNBILLED income fee; refresh receivable totals. */
TmsPriceQuoteResult updateIncomeFee(UpdateIncomeBillFeeRequest request);
/** Delete manual UNBILLED income fee; refresh receivable totals. */
TmsPriceQuoteResult deleteIncomeFee(String feeId);
/** 提供给 TMS 判断订单是否可以继续处理的客户状态查询。 */
TmsOrderBillStatusResult queryTmsOrderBillStatus(String identifier);
}
@@ -1,6 +1,7 @@
package com.ruoyi.price.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -26,6 +27,9 @@ import com.ruoyi.price.domain.PrcOrderBill;
import com.ruoyi.price.domain.PrcOrderBillExt;
import com.ruoyi.price.domain.PrcPayBill;
import com.ruoyi.price.domain.PrcPayBillFee;
import com.ruoyi.price.domain.PrcSurcharge;
import com.ruoyi.price.domain.vo.AddIncomeBillFeeRequest;
import com.ruoyi.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsOrderBillStatusResult;
import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine;
@@ -48,6 +52,7 @@ import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
import com.ruoyi.customer.customermoney.vo.CustomerAccountStatusVO;
import com.ruoyi.price.service.IPrcOrderBillService;
import com.ruoyi.price.service.IPrcSurchargeService;
import com.ruoyi.price.service.ITmsPriceQuoteService;
@Service
@@ -80,6 +85,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Autowired
private ICustomerAccountApi customerAccountApi;
@Autowired
private IPrcSurchargeService surchargeService;
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult bill(TmsPriceBillRequest request)
@@ -201,6 +209,237 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
payBillFeeMapper.selectByBillId(bill.getBillId()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult addIncomeFee(AddIncomeBillFeeRequest request)
{
if (request == null || StringUtils.isEmpty(request.getBillId()))
{
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
}
if (request.getScId() == null)
{
throw new ServiceException(MessageUtils.message("price.bill.fee.scIdRequired"));
}
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(request.getBillId().trim());
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
PrcSurcharge surcharge = requireActiveSurcharge(request.getScId());
PrcIncomeBill incomeBill = incomeBillMapper.selectByBillId(bill.getBillId());
String feeType = StringUtils.isNotEmpty(request.getFeeType()) ? request.getFeeType().trim()
: (StringUtils.isEmpty(surcharge.getFeeType()) ? "SURCHARGE" : surcharge.getFeeType());
String currency = normalizeFeeCurrency(request.getCurrency());
BigDecimal amount = request.getAmount();
if (amount == null)
{
amount = calcManualSurchargeAmount(surcharge, bill.getChargeWeight(), bill.getFreightAmount());
}
if (amount == null)
{
amount = BigDecimal.ZERO;
}
PrcIncomeBillFee fee = new PrcIncomeBillFee();
fee.setFeeId(UUID.randomUUID().toString());
fee.setBillId(bill.getBillId());
fee.setIncomeBillId(incomeBill == null ? null : incomeBill.getIncomeBillId());
fee.setFeeType(feeType);
fee.setFeeName(surcharge.getScName());
fee.setScId(surcharge.getScId());
fee.setAmount(amount);
fee.setCurrency(currency);
fee.setRoundMode(StringUtils.isEmpty(surcharge.getRoundMode()) ? bill.getRoundMode() : surcharge.getRoundMode());
fee.setRemark(request.getRemark());
fee.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
fee.setManualFlag("1");
incomeBillFeeMapper.insertPrcIncomeBillFee(fee);
return refreshBillIncomeTotals(bill);
}
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult updateIncomeFee(UpdateIncomeBillFeeRequest request)
{
if (request == null || StringUtils.isEmpty(request.getFeeId()))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.idRequired"));
}
PrcIncomeBillFee fee = requireEditableManualFee(request.getFeeId().trim());
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(fee.getBillId());
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
if (request.getScId() != null)
{
PrcSurcharge surcharge = requireActiveSurcharge(request.getScId());
fee.setScId(surcharge.getScId());
fee.setFeeName(surcharge.getScName());
if (StringUtils.isEmpty(request.getFeeType()))
{
fee.setFeeType(StringUtils.isEmpty(surcharge.getFeeType()) ? "SURCHARGE" : surcharge.getFeeType());
}
if (StringUtils.isEmpty(fee.getRoundMode()))
{
fee.setRoundMode(StringUtils.isEmpty(surcharge.getRoundMode()) ? bill.getRoundMode() : surcharge.getRoundMode());
}
}
if (StringUtils.isNotEmpty(request.getFeeType()))
{
fee.setFeeType(request.getFeeType().trim());
}
if (request.getAmount() != null)
{
fee.setAmount(request.getAmount());
}
if (StringUtils.isNotEmpty(request.getCurrency()))
{
fee.setCurrency(normalizeFeeCurrency(request.getCurrency()));
}
if (request.getRemark() != null)
{
fee.setRemark(request.getRemark());
}
incomeBillFeeMapper.updatePrcIncomeBillFee(fee);
return refreshBillIncomeTotals(bill);
}
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPriceQuoteResult deleteIncomeFee(String feeId)
{
if (StringUtils.isEmpty(feeId))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.idRequired"));
}
PrcIncomeBillFee fee = requireEditableManualFee(feeId.trim());
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(fee.getBillId());
if (bill == null)
{
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
}
incomeBillFeeMapper.deleteByFeeId(fee.getFeeId());
return refreshBillIncomeTotals(bill);
}
private PrcSurcharge requireActiveSurcharge(Long scId)
{
PrcSurcharge surcharge = surchargeService.selectPrcSurchargeById(scId);
String ssCode = StringUtils.nvl(surcharge == null ? null : surcharge.getSsCode(), "on");
if (surcharge == null || "of".equalsIgnoreCase(ssCode) || "vd".equalsIgnoreCase(ssCode))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.surchargeNotFound"));
}
return surcharge;
}
private PrcIncomeBillFee requireEditableManualFee(String feeId)
{
PrcIncomeBillFee fee = incomeBillFeeMapper.selectByFeeId(feeId);
if (fee == null)
{
throw new ServiceException(MessageUtils.message("price.bill.fee.notFound"));
}
if (!"1".equals(fee.getManualFlag()))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.notManual"));
}
if (ArOrderBillStatus.BILLED.getCode().equals(fee.getArBillStatus()))
{
throw new ServiceException(MessageUtils.message("price.bill.fee.alreadyBilled"));
}
return fee;
}
private String normalizeFeeCurrency(String currency)
{
String ccy = StringUtils.isNotEmpty(currency) ? currency.trim() : "CNY";
if ("RMB".equalsIgnoreCase(ccy))
{
return "CNY";
}
return ccy;
}
/** Recalc surcharge/income totals on order + income bill, sync credit, return detail. */
private TmsPriceQuoteResult refreshBillIncomeTotals(PrcOrderBill bill)
{
List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId());
BigDecimal previousIncome = nz(bill.getIncomeTotal());
BigDecimal surchargeTotal = BigDecimal.ZERO;
BigDecimal incomeTotal = BigDecimal.ZERO;
for (PrcIncomeBillFee row : fees)
{
BigDecimal amt = nz(row.getAmount());
incomeTotal = incomeTotal.add(amt);
if (!"BASIC_FREIGHT".equalsIgnoreCase(row.getFeeType())
&& !"REGISTRATION_FEE".equalsIgnoreCase(row.getFeeType()))
{
surchargeTotal = surchargeTotal.add(amt);
}
}
bill.setSurchargeAmount(surchargeTotal);
bill.setIncomeTotal(incomeTotal);
PrcIncomeBill incomeBill = incomeBillMapper.selectByBillId(bill.getBillId());
if (incomeBill != null)
{
incomeBill.setSalesAmount(incomeTotal);
incomeBill.setIncomeTotal(incomeTotal);
incomeBill.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
incomeBillMapper.updatePrcIncomeBill(incomeBill);
}
String creditArrearsStatus = syncOrderCredit(bill, previousIncome,
StringUtils.isEmpty(bill.getCustomerActiveStatus())
? BillCustomerActiveStatus.ACTIVE.getCode() : bill.getCustomerActiveStatus());
bill.setCreditArrearsStatus(creditArrearsStatus);
orderBillMapper.updatePrcOrderBill(bill);
return toResult(bill, fees, payBillFeeMapper.selectByBillId(bill.getBillId()));
}
private BigDecimal calcManualSurchargeAmount(PrcSurcharge surcharge, BigDecimal weight, BigDecimal freightAmount)
{
if (surcharge == null || surcharge.getChargeValue() == null)
{
return BigDecimal.ZERO;
}
RoundingMode rm = resolveFeeRoundingMode(surcharge.getRoundMode());
String chargeType = surcharge.getChargeType() == null ? "FIXED" : surcharge.getChargeType().toUpperCase();
BigDecimal amount;
if ("WEIGHT".equals(chargeType))
{
amount = surcharge.getChargeValue().multiply(nz(weight));
}
else if ("RATE".equals(chargeType))
{
amount = nz(freightAmount).multiply(surcharge.getChargeValue());
}
else
{
amount = surcharge.getChargeValue();
}
return amount.setScale(4, rm);
}
private RoundingMode resolveFeeRoundingMode(String roundMode)
{
if ("UP".equalsIgnoreCase(roundMode))
{
return RoundingMode.UP;
}
if ("DOWN".equalsIgnoreCase(roundMode))
{
return RoundingMode.DOWN;
}
return RoundingMode.HALF_UP;
}
/** 查询 TMS 后续流程所需的客户状态,只返回订单数据和状态事实,不代替上游做业务决策。 */
@Override
@Transactional(readOnly = true)
@@ -551,7 +790,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
billSearchStore.save(EsLogType.CREDIT, bill.getOrderNo(), req.toJSONString(), resp.toJSONString(), status, err);
}
/** Block re-bill when order already claimed by an AR bill (BILLED). Void restores UNBILLED. */
/**
* Block re-bill when BASIC_FREIGHT fee is already in an AR bill (BILLED).
* Source of truth is prc_income_bill_fee.ar_bill_status (not prc_order_bill.ar_bill_status).
*/
private void assertOrderBillEditable(String orderNo)
{
if (StringUtils.isEmpty(orderNo))
@@ -563,11 +805,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
private void assertOrderBillEditable(PrcOrderBill existed)
{
if (existed == null)
if (existed == null || StringUtils.isEmpty(existed.getBillId()))
{
return;
}
if (ArOrderBillStatus.BILLED.getCode().equals(existed.getArBillStatus()))
int billedFreightCount = incomeBillFeeMapper.countByBillIdAndArBillStatusAndFeeType(
existed.getBillId(), ArOrderBillStatus.BILLED.getCode(), "BASIC_FREIGHT");
if (billedFreightCount > 0)
{
throw new ServiceException(MessageUtils.message("price.bill.alreadyInArBill", existed.getOrderNo()));
}
@@ -598,6 +842,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
fee.setCurrency(line.getCurrency());
fee.setRoundMode(line.getRoundMode());
fee.setRemark(line.getRemark());
fee.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
fee.setManualFlag("0");
fees.add(fee);
}
return fees;
@@ -777,6 +1023,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
for (PrcIncomeBillFee fee : fees)
{
TmsPriceQuoteFeeLine line = new TmsPriceQuoteFeeLine();
line.setFeeId(fee.getFeeId());
line.setFeeType(fee.getFeeType());
line.setFeeName(fee.getFeeName());
line.setScId(fee.getScId());
@@ -784,6 +1031,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
line.setCurrency(fee.getCurrency());
line.setRoundMode(fee.getRoundMode());
line.setRemark(fee.getRemark());
line.setArBillStatus(fee.getArBillStatus());
line.setManualFlag(fee.getManualFlag());
lines.add(line);
}
return lines;