From 226db7799b68c28f066d7bc44256818a081b0d8e Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Tue, 22 Sep 2026 15:30:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=B9=E7=94=A8=E6=98=8E=E7=BB=86=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=A2=9E=E5=8A=A0=E8=B4=B9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/price/BillingController.java | 30 +++ .../controller/price/SurchargeController.java | 3 +- .../main/resources/i18n/messages.properties | 6 + .../ar/service/impl/ArBillServiceImpl.java | 14 + .../ruoyi/price/domain/PrcIncomeBillFee.java | 4 + .../domain/vo/AddIncomeBillFeeRequest.java | 32 +++ .../price/domain/vo/TmsPriceQuoteFeeLine.java | 5 + .../domain/vo/UpdateIncomeBillFeeRequest.java | 28 ++ .../price/mapper/PrcIncomeBillFeeMapper.java | 13 + .../price/oracle/OracleOrderBillMapper.java | 7 +- .../price/service/IPrcOrderBillService.java | 11 + .../service/impl/PrcOrderBillServiceImpl.java | 255 +++++++++++++++++- .../mapper/price/PrcIncomeBillFeeMapper.xml | 61 ++++- bms-back/sql/price/billing_fee_menu.sql | 26 ++ ...prc_income_bill_fee_add_ar_bill_status.sql | 6 + .../prc_income_bill_fee_add_manual_flag.sql | 5 + bms-front/src/api/price/billing.js | 26 ++ bms-front/src/views/price/billing/bill.vue | 7 +- .../billing/components/AddIncomeFeeDialog.vue | 207 ++++++++++++++ .../price/billing/components/BillDetail.vue | 65 ++++- 20 files changed, 797 insertions(+), 14 deletions(-) create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/AddIncomeBillFeeRequest.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/UpdateIncomeBillFeeRequest.java create mode 100644 bms-back/sql/price/billing_fee_menu.sql create mode 100644 bms-back/sql/price/prc_income_bill_fee_add_ar_bill_status.sql create mode 100644 bms-back/sql/price/prc_income_bill_fee_add_manual_flag.sql create mode 100644 bms-front/src/views/price/billing/components/AddIncomeFeeDialog.vue diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java index 89922c24..ee45b899 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java @@ -4,18 +4,24 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; import com.ruoyi.price.domain.PrcOrderBill; import com.ruoyi.price.domain.PrcQuote; +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.TmsPriceQuoteRequest; import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; @@ -110,6 +116,30 @@ public class BillingController extends BaseController return success(prcOrderBillService.getBill(billId)); } + @PreAuthorize("@ss.hasPermi('price:billing:feeAdd')") + @Log(title = "Billing income fee", businessType = BusinessType.INSERT) + @PostMapping("/bill/fee") + public AjaxResult addIncomeFee(@Validated @RequestBody AddIncomeBillFeeRequest request) + { + return success(prcOrderBillService.addIncomeFee(request)); + } + + @PreAuthorize("@ss.hasPermi('price:billing:feeEdit')") + @Log(title = "Billing income fee", businessType = BusinessType.UPDATE) + @PutMapping("/bill/fee") + public AjaxResult updateIncomeFee(@Validated @RequestBody UpdateIncomeBillFeeRequest request) + { + return success(prcOrderBillService.updateIncomeFee(request)); + } + + @PreAuthorize("@ss.hasPermi('price:billing:feeRemove')") + @Log(title = "Billing income fee", businessType = BusinessType.DELETE) + @DeleteMapping("/bill/fee/{feeId}") + public AjaxResult deleteIncomeFee(@PathVariable String feeId) + { + return success(prcOrderBillService.deleteIncomeFee(feeId)); + } + @PreAuthorize("@ss.hasPermi('price:billing:bill')") @PostMapping("/bill") public AjaxResult bill(@Validated @RequestBody TmsPriceBillRequest request) diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/SurchargeController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/SurchargeController.java index 1822255d..fe5cff47 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/SurchargeController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/SurchargeController.java @@ -69,7 +69,8 @@ public class SurchargeController extends BaseController * 不要求 price:surcharge:list 权限,套餐维护角色(price:set:*)也能读,避免套餐页拿到 403 空列表。 */ @PreAuthorize("@ss.hasPermi('price:surcharge:list') or @ss.hasPermi('price:surcharge:query')" - + " or @ss.hasPermi('price:set:query') or @ss.hasPermi('price:set:add') or @ss.hasPermi('price:set:edit')") + + " or @ss.hasPermi('price:set:query') or @ss.hasPermi('price:set:add') or @ss.hasPermi('price:set:edit')" + + " or @ss.hasPermi('price:billing:bill')") @GetMapping("/options") public AjaxResult listOptions(PrcSurcharge surcharge) { diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index f233348a..7e4b34e3 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -153,6 +153,12 @@ price.bill.notFound=计费记录不存在 price.bill.customerRequired=订单同步缺少有效客户 price.bill.alreadyInArBill=订单{0}已生成应收账单,作废后方可重新计费 price.bill.bizDateRequired=业务日期为空 +price.bill.fee.scIdRequired=附加费不能为空 +price.bill.fee.surchargeNotFound=附加费配置不存在或已停用 +price.bill.fee.notFound=费用明细不存在 +price.bill.fee.notManual=仅手动增加的费用可编辑或删除 +price.bill.fee.alreadyBilled=费用已生成账单,不可编辑或删除 +price.bill.fee.idRequired=费用ID不能为空 price.set.version.effectiveTime.empty=版本生效时间不能为空 price.set.version.notExists=套餐版本不存在 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java index 3636a669..ae8bb5ef 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java @@ -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 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 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); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcIncomeBillFee.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcIncomeBillFee.java index acc3744d..fb6af3b4 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcIncomeBillFee.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcIncomeBillFee.java @@ -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; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/AddIncomeBillFeeRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/AddIncomeBillFeeRequest.java new file mode 100644 index 00000000..5ba76606 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/AddIncomeBillFeeRequest.java @@ -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; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java index 8f79e7a3..3c08b432 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java @@ -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; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/UpdateIncomeBillFeeRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/UpdateIncomeBillFeeRequest.java new file mode 100644 index 00000000..73a51b8d --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/UpdateIncomeBillFeeRequest.java @@ -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; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java index 4b479997..8ecb4344 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java @@ -11,9 +11,22 @@ public interface PrcIncomeBillFeeMapper public List selectByBillIds(@Param("billIds") List 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 fees); + public int updatePrcIncomeBillFee(PrcIncomeBillFee fee); + + public int updateArBillStatusByBillIds(@Param("billIds") List billIds, + @Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus); + + public int deleteByFeeId(String feeId); + public int deleteByBillId(String billId); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java index 84ee22fa..6ce46550 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java @@ -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")); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java index 9a03e35c..ea0f94e4 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcOrderBillService.java @@ -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); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java index bf8cff81..f1f47297 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java @@ -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 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; diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml index 71471c24..eb32a75b 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml @@ -15,18 +15,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + insert into prc_income_bill_fee ( - fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time + fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, + ar_bill_status, manual_flag, create_time ) values ( - #{feeId}, #{billId}, #{incomeBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark}, sysdate() + #{feeId}, #{billId}, #{incomeBillId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark}, + #{arBillStatus}, #{manualFlag}, sysdate() ) insert into prc_income_bill_fee ( - fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time + fee_id, bill_id, income_bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, + ar_bill_status, manual_flag, create_time ) values (#{item.feeId}, #{item.billId}, #{item.incomeBillId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount}, - #{item.currency}, #{item.roundMode}, #{item.remark}, sysdate()) + #{item.currency}, #{item.roundMode}, #{item.remark}, + #{item.arBillStatus}, #{item.manualFlag}, sysdate()) + + update prc_income_bill_fee + set fee_type = #{feeType}, + fee_name = #{feeName}, + sc_id = #{scId}, + amount = #{amount}, + currency = #{currency}, + round_mode = #{roundMode}, + remark = #{remark} + where fee_id = #{feeId} + + + + update prc_income_bill_fee + set ar_bill_status = #{toStatus} + where ar_bill_status = #{fromStatus} + and bill_id in + + #{billId} + + + + + delete from prc_income_bill_fee where fee_id = #{feeId} + + delete from prc_income_bill_fee where bill_id = #{billId} diff --git a/bms-back/sql/price/billing_fee_menu.sql b/bms-back/sql/price/billing_fee_menu.sql new file mode 100644 index 00000000..aa439b4a --- /dev/null +++ b/bms-back/sql/price/billing_fee_menu.sql @@ -0,0 +1,26 @@ +-- Billing list: manual fee button permissions (parent = price:billing:bill) +SET NAMES utf8mb4; + +SET @parent_id := (SELECT menu_id FROM sys_menu WHERE perms = 'price:billing:bill' AND menu_type = 'C' LIMIT 1); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT N'增加费用', @parent_id, 10, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:feeAdd', '#', 'admin', sysdate(), N'计费详情手动增加费用' +FROM DUAL +WHERE @parent_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:billing:feeAdd'); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT N'编辑费用', @parent_id, 11, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:feeEdit', '#', 'admin', sysdate(), N'编辑手动增加且未生成账单的费用' +FROM DUAL +WHERE @parent_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:billing:feeEdit'); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT N'删除费用', @parent_id, 12, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:feeRemove', '#', 'admin', sysdate(), N'删除手动增加且未生成账单的费用' +FROM DUAL +WHERE @parent_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'price:billing:feeRemove'); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, menu_id FROM sys_menu WHERE perms IN ('price:billing:feeAdd', 'price:billing:feeEdit', 'price:billing:feeRemove') + AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = sys_menu.menu_id); diff --git a/bms-back/sql/price/prc_income_bill_fee_add_ar_bill_status.sql b/bms-back/sql/price/prc_income_bill_fee_add_ar_bill_status.sql new file mode 100644 index 00000000..80f7b99d --- /dev/null +++ b/bms-back/sql/price/prc_income_bill_fee_add_ar_bill_status.sql @@ -0,0 +1,6 @@ +-- prc_income_bill_fee: ar_bill_status (UNBILLED/BILLED) +SET NAMES utf8mb4; + +ALTER TABLE prc_income_bill_fee + ADD COLUMN ar_bill_status VARCHAR(20) NOT NULL DEFAULT 'UNBILLED' COMMENT '是否生成账单状态:UNBILLED未生成/BILLED已生成' AFTER remark, + ADD KEY idx_income_fee_ar_status (ar_bill_status); diff --git a/bms-back/sql/price/prc_income_bill_fee_add_manual_flag.sql b/bms-back/sql/price/prc_income_bill_fee_add_manual_flag.sql new file mode 100644 index 00000000..30d22b21 --- /dev/null +++ b/bms-back/sql/price/prc_income_bill_fee_add_manual_flag.sql @@ -0,0 +1,5 @@ +-- prc_income_bill_fee: manual_flag +SET NAMES utf8mb4; + +ALTER TABLE prc_income_bill_fee + ADD COLUMN manual_flag CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否手动增加:0否 1是' AFTER ar_bill_status; diff --git a/bms-front/src/api/price/billing.js b/bms-front/src/api/price/billing.js index 88c10f8d..522e3e67 100644 --- a/bms-front/src/api/price/billing.js +++ b/bms-front/src/api/price/billing.js @@ -78,3 +78,29 @@ export function listTransferNoOptions(keyword) { params: { keyword } }) } + +/** 计费详情:增加应收费用明细 */ +export function addIncomeBillFee(data) { + return request({ + url: '/price/billing/bill/fee', + method: 'post', + data + }) +} + +/** 计费详情:编辑手动应收费用 */ +export function updateIncomeBillFee(data) { + return request({ + url: '/price/billing/bill/fee', + method: 'put', + data + }) +} + +/** 计费详情:删除手动应收费用 */ +export function deleteIncomeBillFee(feeId) { + return request({ + url: '/price/billing/bill/fee/' + feeId, + method: 'delete' + }) +} diff --git a/bms-front/src/views/price/billing/bill.vue b/bms-front/src/views/price/billing/bill.vue index 6a65856b..33c61356 100644 --- a/bms-front/src/views/price/billing/bill.vue +++ b/bms-front/src/views/price/billing/bill.vue @@ -71,7 +71,7 @@ - + @@ -156,6 +156,11 @@ function handleView(row) { }) } +function onDetailUpdated(data) { + detail.value = data + tableRef.value?.refresh?.() +} + function formatMoney(val) { if (val === null || val === undefined || val === '') return '-' const num = Number(val) diff --git a/bms-front/src/views/price/billing/components/AddIncomeFeeDialog.vue b/bms-front/src/views/price/billing/components/AddIncomeFeeDialog.vue new file mode 100644 index 00000000..62f5def3 --- /dev/null +++ b/bms-front/src/views/price/billing/components/AddIncomeFeeDialog.vue @@ -0,0 +1,207 @@ + + + diff --git a/bms-front/src/views/price/billing/components/BillDetail.vue b/bms-front/src/views/price/billing/components/BillDetail.vue index 77983d0c..c69f2f7a 100644 --- a/bms-front/src/views/price/billing/components/BillDetail.vue +++ b/bms-front/src/views/price/billing/components/BillDetail.vue @@ -90,6 +90,9 @@
费用明细
+
+ 增加费用 +
@@ -114,6 +117,15 @@ + + +
@@ -149,10 +161,23 @@ +