From fbc138f041e7029ee3f6f9b6cdab8caccd487daa Mon Sep 17 00:00:00 2001 From: liuminghui <468701464@qq.com> Date: Tue, 10 Nov 2026 17:24:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E5=8D=95=E8=B4=B9=E7=94=A8=E5=9D=87?= =?UTF-8?q?=E6=91=8A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../price/SrvBillAirController.java | 58 +++ .../domain/SrvBillAirAllocCommand.java | 11 + .../airbill/domain/SrvBillAirAllocFee.java | 45 ++ .../airbill/domain/SrvBillAirAllocOrder.java | 19 + .../airbill/domain/SrvBillAirFeeLog.java | 6 + .../com/bms/price/domain/PrcPayBillFee.java | 3 + .../bms/price/mapper/PrcPayBillFeeMapper.java | 17 + .../price/mapper/SrvBillAirAllocMapper.java | 56 +++ .../bms/price/mapper/SrvBillAirFeeMapper.java | 8 + .../service/ISrvBillAirAllocService.java | 26 + .../price/service/ISrvBillAirFeeService.java | 18 + .../impl/SrvBillAirAllocServiceImpl.java | 472 ++++++++++++++++++ .../impl/SrvBillAirFeeServiceImpl.java | 130 ++++- .../mapper/price/PrcPayBillFeeMapper.xml | 43 +- .../mapper/price/SrvBillAirAllocMapper.xml | 61 +++ .../mapper/price/SrvBillAirFeeMapper.xml | 27 + bms-back/sql/price/air_bill_fee_dict.sql | 45 ++ bms-back/sql/price/air_bill_menu.sql | 23 + bms-front/src/api/price/airBill.js | 37 ++ bms-front/src/components/BasicTable/index.vue | 31 +- bms-front/src/views/price/billing/airBill.vue | 20 + .../billing/components/AirBillAllocDialog.vue | 154 ++++++ .../billing/components/AirBillFeeDialog.vue | 77 ++- 23 files changed, 1355 insertions(+), 32 deletions(-) create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocCommand.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocOrder.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirAllocMapper.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirAllocService.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java create mode 100644 bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirAllocMapper.xml create mode 100644 bms-front/src/views/price/billing/components/AirBillAllocDialog.vue diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java index 9b3d2977..c67daeef 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java @@ -16,10 +16,12 @@ import com.bms.common.core.domain.AjaxResult; import com.bms.common.core.page.TableDataInfo; import com.bms.common.enums.BusinessType; import com.bms.common.utils.StringUtils; +import com.bms.price.airbill.domain.SrvBillAirAllocCommand; import com.bms.price.airbill.domain.SrvBillAirFeeLog; import com.bms.price.airbill.domain.SrvBillAirFeeSave; import com.bms.price.domain.vo.SrvBillAirQuery; import com.bms.price.domain.vo.SrvBillAirVo; +import com.bms.price.service.ISrvBillAirAllocService; import com.bms.price.service.ISrvBillAirFeeService; import com.bms.price.service.ISrvBillAirService; @@ -36,6 +38,9 @@ public class SrvBillAirController extends BaseController @Autowired private ISrvBillAirFeeService srvBillAirFeeService; + @Autowired + private ISrvBillAirAllocService srvBillAirAllocService; + /** * 提单列表 */ @@ -94,4 +99,57 @@ public class SrvBillAirController extends BaseController { return success(srvBillAirFeeService.save(command)); } + + /** 已分摊的费用改为已确定 */ + @PreAuthorize("@ss.hasPermi('price:airBill:feeConfirm')") + @Log(title = "提单费用确定", businessType = BusinessType.UPDATE) + @PostMapping("/fee/confirm") + public AjaxResult confirmFee(@RequestParam("billCode") String billCode, @RequestParam("feeId") Long feeId) + { + return success(srvBillAirFeeService.confirm(billCode, feeId)); + } + + /** 作废已分摊或已确定的费用,并逻辑删除已分摊到订单的明细 */ + @PreAuthorize("@ss.hasPermi('price:airBill:feeVoid')") + @Log(title = "提单费用作废", businessType = BusinessType.DELETE) + @PostMapping("/fee/void") + public AjaxResult voidFee(@RequestParam("billCode") String billCode, @RequestParam("feeId") Long feeId) + { + return success(srvBillAirFeeService.voidFee(billCode, feeId)); + } + + /** 勾选提单后,查出需要分摊到订单的费用 */ + @PreAuthorize("@ss.hasPermi('price:airBill:fee')") + @GetMapping("/fee/alloc/preview") + public AjaxResult allocPreview(@RequestParam("billCodes") String billCodes) + { + return success(srvBillAirAllocService.preview(splitCodes(billCodes))); + } + + /** 按重量或订单数把费用分摊到订单,并把费用状态改为已分摊 */ + @PreAuthorize("@ss.hasPermi('price:airBill:fee')") + @Log(title = "提单费用分摊", businessType = BusinessType.UPDATE) + @PostMapping("/fee/alloc") + public AjaxResult allocate(@RequestBody SrvBillAirAllocCommand command) + { + List billCodes = command == null ? null : command.getBillCodes(); + return success(srvBillAirAllocService.allocate(billCodes)); + } + + private static List splitCodes(String billCodes) + { + List codes = new ArrayList<>(); + if (StringUtils.isEmpty(billCodes)) + { + return codes; + } + for (String part : billCodes.split("[,,\\s]+")) + { + if (StringUtils.isNotEmpty(part)) + { + codes.add(part.trim()); + } + } + return codes; + } } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocCommand.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocCommand.java new file mode 100644 index 00000000..5b71c0d6 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocCommand.java @@ -0,0 +1,11 @@ +package com.bms.price.airbill.domain; + +import java.util.List; +import lombok.Data; + +/** 提单费用分摊请求。一次最多 5 个提单号。 */ +@Data +public class SrvBillAirAllocCommand +{ + private List billCodes; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java new file mode 100644 index 00000000..f367124d --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocFee.java @@ -0,0 +1,45 @@ +package com.bms.price.airbill.domain; + +import java.math.BigDecimal; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +/** 待分摊的一条提单费用,供费用处理弹框展示。 */ +@Data +public class SrvBillAirAllocFee +{ + private Long id; + + private String billCode; + + /** 提单主键 srv_bill_air.guid,写入订单费用的 source_id */ + private String ladingId; + + /** 费用阶段,字典 bms_fee_level */ + private String feeLevel; + + /** 费用状态,字典 bill_fee_status */ + private String billFeeStatus; + + private String feeName; + + private BigDecimal price; + + private String currency; + + /** 分摊维度:1订单 2重量 */ + private String allocDim; + + /** 提单实际重量。只在按重量分摊时按提单号填入,不返回页面 */ + @JsonIgnore + private BigDecimal actualWeight; + + /** 干线打板重量。只在按重量分摊时按提单号填入,不返回页面 */ + @JsonIgnore + private BigDecimal bandBoxWeight; + + private Integer orderCount; + + /** 不能分摊时的原因。空表示可以分摊 */ + private String blockReason; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocOrder.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocOrder.java new file mode 100644 index 00000000..ef789233 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirAllocOrder.java @@ -0,0 +1,19 @@ +package com.bms.price.airbill.domain; + +import java.math.BigDecimal; +import lombok.Data; + +/** 提单下参与分摊的一票订单。 */ +@Data +public class SrvBillAirAllocOrder +{ + private String billCode; + + private String orderNo; + + /** 计费订单 ID,对应 prc_order_bill.bill_id */ + private String billId; + + /** 计费重,没有计费重时依次取实际重量、收货重量 */ + private BigDecimal chargeWeight; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java index 9525a3ba..3fa0e8f3 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java @@ -17,6 +17,12 @@ public class SrvBillAirFeeLog public static final String DELETE = "3"; + /** 已分摊费用确定。日志不记录字段变化。 */ + public static final String CONFIRM = "4"; + + /** 已分摊或已确定费用作废。 */ + public static final String VOID = "5"; + private Long logId; private String billCode; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java index 4a40a783..c71cc16a 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java @@ -19,6 +19,9 @@ public class PrcPayBillFee implements Serializable private String feeType; private String feeName; private Long scId; + + /** 费用所属阶段:1 a段,2 b段,3 c段,4 d段 */ + private String feeLevel; private BigDecimal amount; private String currency; private String roundMode; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java index a18e57f3..87c2b135 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPayBillFeeMapper.java @@ -38,4 +38,21 @@ public interface PrcPayBillFeeMapper * @return 影响行数 */ public int deleteByBillId(String billId); + + /** + * 逻辑删除某一条提单费用分摊到订单上的明细。 + * + * @param feeId 提单费用 ID,保存在 sc_id + * @return 影响行数 + */ + public int markLadingFeeDeleted(@Param("feeId") Long feeId); + + /** + * 这条提单费用分摊到的订单里,取一个已经生成应付账单的订单号。 + * 是否出账看 prc_order_bill_ext.is_meet:1 已生成。 + * + * @param feeId 提单费用 ID,保存在 sc_id + * @return 订单号,没有已出账订单时为空 + */ + public String selectBilledOrderNo(@Param("feeId") Long feeId); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirAllocMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirAllocMapper.java new file mode 100644 index 00000000..611049fc --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirAllocMapper.java @@ -0,0 +1,56 @@ +package com.bms.price.mapper; + +import java.util.Date; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.price.airbill.domain.SrvBillAirAllocFee; +import com.bms.price.airbill.domain.SrvBillAirAllocOrder; +import com.bms.price.domain.PrcPayBill; + +/** + * 提单费用分摊到订单。 + */ +public interface SrvBillAirAllocMapper +{ + /** + * 查询这些提单上尚未分摊、且标记为需要分摊的费用。 + * + * @param billCodes 提单号 + * @return 待分摊费用 + */ + List selectPendingFees(@Param("billCodes") List billCodes); + + /** + * 按提单号取出实际重量和打板重量,只供按重量分摊计算。 + * + * @param billCodes 提单号 + * @return 提单号、实际重量、打板重量 + */ + List selectLadingWeight(@Param("billCodes") List billCodes); + + /** + * 按提单号查出关联订单。订单号在扩展表,计费单号和重量在订单表。 + * + * @param billCodes 提单号,对应 prc_order_bill_ext.air_no_old + * @return 订单 + */ + List selectOrders(@Param("billCodes") List billCodes); + + /** + * 按计费订单号取出已有的应付主表。 + * + * @param billIds 计费订单 ID + * @return 应付主表编号 + */ + List selectPayBills(@Param("billIds") List billIds); + + /** + * 把仍是未分摊的费用改为已分摊。已经不是未分摊的行不会被改到。 + * + * @param ids 提单费用 ID + * @param updateBy 操作人 + * @param updateTime 操作时间 + * @return 实际改到的行数 + */ + int markAllocated(@Param("ids") List ids, @Param("updateBy") String updateBy, @Param("updateTime") Date updateTime); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java index 68325483..b0df69fa 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java @@ -22,6 +22,14 @@ public interface SrvBillAirFeeMapper int deleteFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy, @Param("updateTime") java.util.Date updateTime); + /** 已分摊改为已确定。 */ + int confirmFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy, + @Param("updateTime") java.util.Date updateTime); + + /** 已分摊或已确定的费用作废:状态改为已作废,并逻辑删除。 */ + int voidFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy, + @Param("updateTime") java.util.Date updateTime); + int insertLog(SrvBillAirFeeLog log); List selectLogs(String billCode); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirAllocService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirAllocService.java new file mode 100644 index 00000000..0b66520f --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirAllocService.java @@ -0,0 +1,26 @@ +package com.bms.price.service; + +import java.util.List; +import com.bms.price.airbill.domain.SrvBillAirAllocFee; + +/** + * 把提单上待分摊的费用均摊到关联订单。 + */ +public interface ISrvBillAirAllocService +{ + /** + * 查出勾选提单上需要分摊、且尚未分摊的费用。 + * + * @param billCodes 提单号,最多 5 个 + * @return 费用和不能分摊的原因 + */ + List preview(List billCodes); + + /** + * 按费用上的分摊维度把金额摊到每个订单,并改费用状态为已分摊。整次处理在一个事务里,写入按 1000 条一批。 + * + * @param billCodes 提单号,最多 5 个 + * @return 给页面的处理结果 + */ + String allocate(List billCodes); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java index 4c2128f7..e4445133 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java @@ -16,4 +16,22 @@ public interface ISrvBillAirFeeService /** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */ String save(SrvBillAirFeeSave command); + + /** + * 已分摊的费用确认为已确定。 + * + * @param billCode 提单号 + * @param feeId 费用 ID + * @return 给页面的提示 + */ + String confirm(String billCode, Long feeId); + + /** + * 作废已分摊或已确定的费用。订单上的分摊明细逻辑删除,这条提单费用也逻辑删除。 + * + * @param billCode 提单号 + * @param feeId 费用 ID + * @return 给页面的提示 + */ + String voidFee(String billCode, Long feeId); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java new file mode 100644 index 00000000..3e142acc --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirAllocServiceImpl.java @@ -0,0 +1,472 @@ +package com.bms.price.service.impl; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; +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.bms.common.core.domain.entity.SysUser; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.DateUtils; +import com.bms.common.utils.SecurityUtils; +import com.bms.price.airbill.domain.SrvBillAirAllocFee; +import com.bms.price.airbill.domain.SrvBillAirAllocOrder; +import com.bms.price.airbill.domain.SrvBillAirFeeLog; +import com.bms.price.domain.PrcPayBill; +import com.bms.price.domain.PrcPayBillFee; +import com.bms.price.mapper.SrvBillAirAllocMapper; +import com.bms.price.mapper.SrvBillAirFeeMapper; +import com.bms.price.mapper.SrvBillAirMapper; +import com.bms.price.mapper.PrcPayBillFeeMapper; +import com.bms.price.service.ISrvBillAirAllocService; + +/** + * 提单费用分摊。只处理标记为分摊、状态仍是未分摊的费用。 + */ +@Service +public class SrvBillAirAllocServiceImpl implements ISrvBillAirAllocService +{ + private static final int MAX_BILLS = 5; + + private static final int BATCH_SIZE = 1000; + + private static final String DIM_ORDER = "1"; + + private static final String DIM_WEIGHT = "2"; + + private static final String FEE_TYPE = "LADING"; + + private static final String FEE_SOURCE = "LADING"; + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + @Autowired + private SrvBillAirAllocMapper allocMapper; + + @Autowired + private SrvBillAirMapper billMapper; + + @Autowired + private SrvBillAirFeeMapper feeMapper; + + @Autowired + private PrcPayBillFeeMapper payBillFeeMapper; + + @Override + public List preview(List billCodes) + { + List codes = normalize(billCodes); + List fees = dedupe(allocMapper.selectPendingFees(codes)); + fillWeight(fees); + Map> orders = ordersOf(codes); + for (SrvBillAirAllocFee fee : fees) + { + List rows = orders.getOrDefault(fee.getBillCode(), List.of()); + fee.setOrderCount(rows.size()); + fee.setBlockReason(blockReason(fee, rows)); + } + return fees; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String allocate(List billCodes) + { + List codes = normalize(billCodes); + List fees = dedupe(allocMapper.selectPendingFees(codes)); + fillWeight(fees); + if (fees.isEmpty()) + { + throw new ServiceException("所选提单没有需要分摊的费用"); + } + Map> orders = ordersOf(codes); + for (SrvBillAirAllocFee fee : fees) + { + List rows = orders.getOrDefault(fee.getBillCode(), List.of()); + fee.setOrderCount(rows.size()); + String reason = blockReason(fee, rows); + if (!reason.isEmpty()) + { + throw new ServiceException(fee.getBillCode() + " " + blankName(fee) + ":" + reason); + } + } + Map payBillIds = payBillIds(orders); + List batch = new ArrayList<>(BATCH_SIZE); + int lineCount = 0; + for (SrvBillAirAllocFee fee : fees) + { + List rows = orders.get(fee.getBillCode()); + fee.setOrderCount(rows.size()); + boolean byWeight = DIM_WEIGHT.equals(fee.getAllocDim()); + List shares = split(byWeight ? weightAmount(fee) : fee.getPrice(), rows, byWeight); + for (int i = 0; i < rows.size(); i++) + { + batch.add(toOrderFee(fee, rows.get(i), shares.get(i), payBillIds)); + if (batch.size() == BATCH_SIZE) + { + lineCount += flush(batch); + } + } + } + lineCount += flush(batch); + Date now = DateUtils.getNowDate(); + String operator = SecurityUtils.getUsername(); + int updated = allocMapper.markAllocated(feeIds(fees), operator, now); + if (updated != fees.size()) + { + throw new ServiceException("费用状态已变化,请重新打开后再分摊"); + } + writeLogs(fees, operator, now); + return "已将 " + fees.size() + " 条费用分摊为 " + lineCount + " 笔订单费用"; + } + + /** + * 按重量分摊的费用,按提单号补上实际重量和打板重量。按订单数量分摊的不查、不填。 + * 这两个值只参与计算,不返回给页面。 + */ + private void fillWeight(List fees) + { + List codes = new ArrayList<>(); + for (SrvBillAirAllocFee fee : fees) + { + if (fee != null && DIM_WEIGHT.equals(fee.getAllocDim()) && fee.getBillCode() != null && !codes.contains(fee.getBillCode())) + { + codes.add(fee.getBillCode()); + } + } + if (codes.isEmpty()) + { + return; + } + Map weights = new LinkedHashMap<>(); + for (SrvBillAirAllocFee row : allocMapper.selectLadingWeight(codes)) + { + if (row != null && row.getBillCode() != null) + { + weights.put(row.getBillCode(), row); + } + } + for (SrvBillAirAllocFee fee : fees) + { + if (fee == null || !DIM_WEIGHT.equals(fee.getAllocDim())) + { + continue; + } + SrvBillAirAllocFee weight = weights.get(fee.getBillCode()); + if (weight == null) + { + continue; + } + fee.setActualWeight(weight.getActualWeight()); + fee.setBandBoxWeight(weight.getBandBoxWeight()); + } + } + + /** 提单号去重、去空白,并限制单次 5 条。 */ + private List normalize(List billCodes) + { + LinkedHashSet codes = new LinkedHashSet<>(); + if (billCodes != null) + { + for (String raw : billCodes) + { + if (raw == null) + { + continue; + } + for (String part : raw.split("[,,\\s]+")) + { + if (!part.isBlank()) + { + codes.add(part.trim()); + } + } + } + } + if (codes.isEmpty()) + { + throw new ServiceException("请勾选提单"); + } + if (codes.size() > MAX_BILLS) + { + throw new ServiceException("单次最多处理5条提单"); + } + for (String code : codes) + { + if (billMapper.selectSrvBillAirByBillCode(code) == null) + { + throw new ServiceException("提单不存在:" + code); + } + } + return new ArrayList<>(codes); + } + + private Map> ordersOf(List codes) + { + Map> grouped = new LinkedHashMap<>(); + for (SrvBillAirAllocOrder order : allocMapper.selectOrders(codes)) + { + if (order.getBillId() == null || order.getBillId().isBlank()) + { + continue; + } + grouped.computeIfAbsent(order.getBillCode(), key -> new ArrayList<>()).add(order); + } + return grouped; + } + + private Map payBillIds(Map> orders) + { + List billIds = new ArrayList<>(); + for (List rows : orders.values()) + { + for (SrvBillAirAllocOrder order : rows) + { + billIds.add(order.getBillId()); + } + } + Map payIds = new LinkedHashMap<>(); + for (int from = 0; from < billIds.size(); from += BATCH_SIZE) + { + List chunk = billIds.subList(from, Math.min(from + BATCH_SIZE, billIds.size())); + for (PrcPayBill payBill : allocMapper.selectPayBills(chunk)) + { + if (payBill.getBillId() != null && payBill.getPayBillId() != null) + { + payIds.put(payBill.getBillId(), payBill.getPayBillId()); + } + } + } + return payIds; + } + + private String blockReason(SrvBillAirAllocFee fee, List orders) + { + if (fee.getPrice() == null) + { + return "费用没有价格"; + } + if (fee.getPrice().signum() < 0) + { + return "费用价格不能小于0"; + } + if (fee.getCurrency() == null || fee.getCurrency().isBlank()) + { + return "费用没有币种"; + } + if (fee.getLadingId() == null || fee.getLadingId().isBlank()) + { + return "提单缺少主键"; + } + if (!DIM_ORDER.equals(fee.getAllocDim()) && !DIM_WEIGHT.equals(fee.getAllocDim())) + { + return "未选择分摊维度"; + } + if (orders == null || orders.isEmpty()) + { + return "提单下没有关联订单"; + } + if (DIM_WEIGHT.equals(fee.getAllocDim())) + { + if (fee.getActualWeight() == null || fee.getActualWeight().signum() <= 0) + { + return "提单没有实际重量,无法按重量分摊"; + } + if (fee.getBandBoxWeight() == null || fee.getBandBoxWeight().signum() <= 0) + { + return "干线没有打板重量,无法按重量分摊"; + } + if (totalWeight(orders).signum() <= 0) + { + return "订单没有可用重量,无法按重量分摊"; + } + } + return ""; + } + + private PrcPayBillFee toOrderFee(SrvBillAirAllocFee fee, SrvBillAirAllocOrder order, BigDecimal amount, Map payBillIds) + { + PrcPayBillFee row = new PrcPayBillFee(); + row.setFeeId(UUID.randomUUID().toString()); + row.setBillId(order.getBillId()); + row.setPayBillId(payBillIds.get(order.getBillId())); + row.setFeeType(FEE_TYPE); + row.setFeeName(blankName(fee)); + row.setScId(fee.getId() == null ? 0L : fee.getId()); + row.setFeeLevel(fee.getFeeLevel()); + row.setAmount(amount); + row.setCurrency(fee.getCurrency().trim()); + row.setRoundMode("HALF_UP"); + row.setRemark(remark(fee)); + row.setFeeSource(FEE_SOURCE); + row.setSourceId(fee.getLadingId().trim()); + row.setIsDeleted("0"); + return row; + } + + private int flush(List batch) + { + if (batch.isEmpty()) + { + return 0; + } + int rows = batch.size(); + payBillFeeMapper.insertBatch(batch); + batch.clear(); + return rows; + } + + private void writeLogs(List fees, String operator, Date now) + { + SysUser user = SecurityUtils.getLoginUser().getUser(); + String name = user.getNickName() == null || user.getNickName().isBlank() ? operator : user.getNickName(); + for (SrvBillAirAllocFee fee : fees) + { + SrvBillAirFeeLog log = new SrvBillAirFeeLog(); + log.setBillCode(fee.getBillCode()); + log.setFeeId(fee.getId()); + log.setOperationType(SrvBillAirFeeLog.UPDATE); + log.setSummary("分摊到 " + fee.getOrderCount() + " 票订单"); + log.setChangeContent(JSON.toJSONString(List.of( + new SrvBillAirFeeLog.FieldChange("billFeeStatus", "费用状态", "未分摊", "已分摊")))); + log.setCreateBy(operator); + log.setCreateByName(name); + log.setCreateTime(now); + feeMapper.insertLog(log); + } + } + + /** + * 按重量分摊时,先算出这一票提单实际可分摊的金额。 + * 百分比 = 提单实际重量 srv_bill_air.actual_weight / 干线打板重量 srv_bill_air_flight.band_box_weight。 + * 可分摊金额 = 费用价格 × 百分比,保留两位,四舍五入。 + * 调用前须已确认实际重量和打板重量都大于 0。 + * + * @param fee 待分摊的提单费用,带有实际重量和打板重量 + * @return 按百分比算出的可分摊金额 + */ + private static BigDecimal weightAmount(SrvBillAirAllocFee fee) + { + BigDecimal price = fee.getPrice().setScale(2, RoundingMode.HALF_UP); + return price.multiply(fee.getActualWeight()).divide(fee.getBandBoxWeight(), 2, RoundingMode.HALF_UP); + } + + /** + * 把金额拆到每一票订单,结果保留两位。 + * 按订单数量:每一票先向下取整,差额补在最后一票,合计等于费用价格。 + * 按重量:传入的金额已是「费用价格 × 提单实际重量 / 干线打板重量」。 + * 再按订单重量占这些订单总重量的比例分摊;前面的订单向下取整,差额补在最后一票有重量的订单上,合计等于这笔金额。 + * 订单重量优先取计费重,没有计费重时取实际重量,再没有取收货重量。 + * + * @param price 本次要拆开的金额。按重量时是百分比计算后的可分摊金额 + * @param orders 该提单下的订单 + * @param byWeight true 按订单重量占比,false 按订单数量均分 + * @return 与订单顺序一致的分摊金额 + */ + private static List split(BigDecimal price, List orders, boolean byWeight) + { + BigDecimal total = price.setScale(2, RoundingMode.HALF_UP); + int count = orders.size(); + List shares = new ArrayList<>(count); + if (!byWeight) + { + BigDecimal each = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.DOWN); + BigDecimal used = ZERO; + for (int i = 0; i < count - 1; i++) + { + shares.add(each); + used = used.add(each); + } + shares.add(total.subtract(used)); + return shares; + } + BigDecimal sum = totalWeight(orders); + int last = count - 1; + for (int i = count - 1; i >= 0; i--) + { + if (weight(orders.get(i)).signum() > 0) + { + last = i; + break; + } + } + BigDecimal used = ZERO; + for (int i = 0; i < count; i++) + { + if (i == last) + { + shares.add(null); + continue; + } + BigDecimal share = total.multiply(weight(orders.get(i))).divide(sum, 2, RoundingMode.DOWN); + shares.add(share); + used = used.add(share); + } + shares.set(last, total.subtract(used)); + return shares; + } + + private static BigDecimal totalWeight(List orders) + { + BigDecimal sum = ZERO; + for (SrvBillAirAllocOrder order : orders) + { + sum = sum.add(weight(order)); + } + return sum; + } + + private static BigDecimal weight(SrvBillAirAllocOrder order) + { + return order.getChargeWeight() == null ? ZERO : order.getChargeWeight(); + } + + private static List dedupe(List fees) + { + Map unique = new LinkedHashMap<>(); + if (fees != null) + { + for (SrvBillAirAllocFee fee : fees) + { + if (fee != null && fee.getId() != null) + { + unique.putIfAbsent(fee.getId(), fee); + } + } + } + return new ArrayList<>(unique.values()); + } + + private static List feeIds(List fees) + { + List ids = new ArrayList<>(); + for (SrvBillAirAllocFee fee : fees) + { + ids.add(fee.getId()); + } + return ids; + } + + private static String blankName(SrvBillAirAllocFee fee) + { + if (fee.getFeeName() != null && !fee.getFeeName().isBlank()) + { + return fee.getFeeName().trim(); + } + return "提单费用"; + } + + private static String remark(SrvBillAirAllocFee fee) + { + String dim = DIM_WEIGHT.equals(fee.getAllocDim()) ? "重量" : "订单"; + return "提单" + fee.getBillCode() + "按" + dim + "分摊"; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java index eed2ef7f..fa957dda 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java @@ -22,6 +22,7 @@ import com.bms.price.airbill.domain.SrvBillAirFee; import com.bms.price.airbill.domain.SrvBillAirFeeLog; import com.bms.price.airbill.domain.SrvBillAirFeeSave; import com.bms.price.domain.PrcSurcharge; +import com.bms.price.mapper.PrcPayBillFeeMapper; import com.bms.price.mapper.SrvBillAirFeeMapper; import com.bms.price.mapper.SrvBillAirMapper; import com.bms.price.service.IPrcSurchargeService; @@ -42,11 +43,18 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService private static final String STATUS_DICT = "bill_fee_status"; + private static final String STATUS_ALLOCATED = "1"; + + private static final String STATUS_CONFIRMED = "3"; + private static final String CURRENCY_DICT = "bms_currency"; @Autowired private SrvBillAirFeeMapper feeMapper; + @Autowired + private PrcPayBillFeeMapper payBillFeeMapper; + @Autowired private IPrcSurchargeService surchargeService; @@ -121,6 +129,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService { throw new ServiceException("费用已删除或不属于该提单"); } + if (locked(current)) + { + throw new ServiceException("已分摊或已确定的费用不能删除,请使用作废"); + } int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now); if (rows > 0) { @@ -143,6 +155,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService { throw new ServiceException("费用已删除或不属于该提单"); } + if (locked(current)) + { + throw new ServiceException("已分摊或已确定的费用不能修改"); + } SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels); next.setBillFeeStatus(current.getBillFeeStatus() == null || current.getBillFeeStatus().isBlank() ? "0" : current.getBillFeeStatus()); Map feeNames = feeNames(current, next); @@ -187,11 +203,90 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条"; } + @Override + @Transactional(rollbackFor = Exception.class) + public String confirm(String billCode, Long feeId) + { + String code = requireBill(billCode); + if (feeId == null) + { + throw new ServiceException("请选择要确定的费用"); + } + SrvBillAirFee current = liveFee(feeId, code); + if (current == null) + { + throw new ServiceException("费用已删除或不属于该提单"); + } + if (!STATUS_ALLOCATED.equals(current.getBillFeeStatus())) + { + throw new ServiceException("只有已分摊的费用可以确定"); + } + Operator operator = operator(); + Date now = DateUtils.getNowDate(); + if (feeMapper.confirmFee(feeId, code, operator.account, now) != 1) + { + throw new ServiceException("费用状态已变化,请刷新后再确定"); + } + writePlainLog(code, feeId, SrvBillAirFeeLog.CONFIRM, "确定费用", operator, now); + return "费用已确定"; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String voidFee(String billCode, Long feeId) + { + String code = requireBill(billCode); + if (feeId == null) + { + throw new ServiceException("请选择要作废的费用"); + } + SrvBillAirFee current = liveFee(feeId, code); + if (current == null) + { + throw new ServiceException("费用已删除或不属于该提单"); + } + if (!locked(current)) + { + throw new ServiceException("只有已分摊或已确定的费用可以作废"); + } + String billedOrderNo = payBillFeeMapper.selectBilledOrderNo(feeId); + if (billedOrderNo != null) + { + String orderNo = billedOrderNo.trim(); + if (orderNo.isEmpty()) + { + orderNo = "未知订单"; + } + throw new ServiceException("该费用存在单笔或多笔订单已出账单-如:" + orderNo + ",不可作废"); + } + Operator operator = operator(); + Date now = DateUtils.getNowDate(); + payBillFeeMapper.markLadingFeeDeleted(feeId); + if (feeMapper.voidFee(feeId, code, operator.account, now) != 1) + { + throw new ServiceException("费用状态已变化,请刷新后再作废"); + } + Map levelLabels = dictLabels(LEVEL_DICT, "费用阶段"); + Map allocLabels = dictLabels(ALLOC_DICT, "是否分摊"); + Map dimLabels = dictLabels(ALLOC_DIM_DICT, "分摊维度"); + Map statusLabels = dictLabels(STATUS_DICT, "费用状态"); + Map currencyLabels = dictLabels(CURRENCY_DICT, "币种"); + writeLog(code, feeId, SrvBillAirFeeLog.VOID, current, null, feeNames(current, null), + levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now); + return "费用已作废"; + } + private SrvBillAirFee liveFee(Long id, String billCode) { return feeMapper.selectLive(id, billCode); } + private static boolean locked(SrvBillAirFee fee) + { + String status = fee == null ? "" : text(fee.getBillFeeStatus()); + return STATUS_ALLOCATED.equals(status) || STATUS_CONFIRMED.equals(status); + } + private SrvBillAirFee normalize(SrvBillAirFee raw, Map levelLabels, Map allocLabels, Map dimLabels, Map currencyLabels) { @@ -270,6 +365,22 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService return fee; } + /** + * 费用日志组装 + * @param billCode + * @param feeId + * @param operation + * @param before + * @param after + * @param feeLabels + * @param levelLabels + * @param allocLabels + * @param dimLabels + * @param statusLabels + * @param currencyLabels + * @param operator + * @param now + */ private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after, Map feeLabels, Map levelLabels, Map allocLabels, Map dimLabels, Map statusLabels, Map currencyLabels, Operator operator, Date now) @@ -287,6 +398,21 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService feeMapper.insertLog(log); } + /** 确定费用不比较字段,更改前和更改后都留空。 */ + private void writePlainLog(String billCode, Long feeId, String operation, String summary, Operator operator, Date now) + { + SrvBillAirFeeLog log = new SrvBillAirFeeLog(); + log.setBillCode(billCode); + log.setFeeId(feeId); + log.setOperationType(operation); + log.setSummary(summary); + log.setChangeContent("[]"); + log.setCreateBy(operator.account); + log.setCreateByName(operator.name); + log.setCreateTime(now); + feeMapper.insertLog(log); + } + private static List diff(SrvBillAirFee before, SrvBillAirFee after, Map feeLabels, Map levelLabels, Map allocLabels, Map dimLabels, Map statusLabels, Map currencyLabels) @@ -323,7 +449,9 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService private static String summary(String operation, List changes) { String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用" - : SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用"; + : SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" + : SrvBillAirFeeLog.CONFIRM.equals(operation) ? "确定费用" + : SrvBillAirFeeLog.VOID.equals(operation) ? "作废费用" : "修改费用"; StringBuilder text = new StringBuilder(action); for (SrvBillAirFeeLog.FieldChange change : changes) { diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PrcPayBillFeeMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PrcPayBillFeeMapper.xml index 6d1126f7..adf50145 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PrcPayBillFeeMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PrcPayBillFeeMapper.xml @@ -11,7 +11,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -21,8 +23,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + select e.order_no + from prc_pay_bill_fee f + inner join prc_order_bill_ext e on e.bill_id = f.bill_id + where f.fee_source = 'LADING' + and f.sc_id = #{feeId} + and ifnull(f.is_deleted, '0') = '0' + and e.is_meet = 1 + order by e.order_no + limit 1 + + + + update prc_pay_bill_fee + set is_deleted = '1' + where fee_source = 'LADING' + and sc_id = #{feeId} + and ifnull(is_deleted, '0') = '0' + + delete from prc_pay_bill_fee where bill_id = #{billId} diff --git a/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirAllocMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirAllocMapper.xml new file mode 100644 index 00000000..6541cb12 --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirAllocMapper.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + update srv_bill_air_fee + set bill_fee_status = '1', + update_by = #{updateBy}, + update_time = #{updateTime} + where del_flag = '0' + and alloc_flag = '1' + and bill_fee_status = '0' + and id in + #{id} + + diff --git a/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml index 4c98e841..e867200f 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml @@ -12,6 +12,7 @@ left join prc_surcharge sc on sc.sc_id = f.fee_type and ifnull(sc.is_del, '0') = '0' where f.bill_code = #{billCode} and f.del_flag = '0' + and ifnull(f.bill_fee_status, '0') <> '2' order by f.id @@ -26,6 +27,7 @@ where f.id = #{id} and f.bill_code = #{billCode} and f.del_flag = '0' + and ifnull(f.bill_fee_status, '0') <> '2' @@ -52,6 +54,30 @@ where id = #{id} and bill_code = #{billCode} and del_flag = '0' + and ifnull(bill_fee_status, '0') not in ('1', '3') + + + + update srv_bill_air_fee + set bill_fee_status = '3', + update_by = #{updateBy}, + update_time = #{updateTime} + where id = #{id} + and bill_code = #{billCode} + and del_flag = '0' + and bill_fee_status = '1' + + + + update srv_bill_air_fee + set del_flag = '1', + bill_fee_status = '2', + update_by = #{updateBy}, + update_time = #{updateTime} + where id = #{id} + and bill_code = #{billCode} + and del_flag = '0' + and bill_fee_status in ('1', '3') @@ -62,6 +88,7 @@ where id = #{id} and bill_code = #{billCode} and del_flag = '0' + and ifnull(bill_fee_status, '0') not in ('1', '3') diff --git a/bms-back/sql/price/air_bill_fee_dict.sql b/bms-back/sql/price/air_bill_fee_dict.sql index 27933ed0..deab84fd 100644 --- a/bms-back/sql/price/air_bill_fee_dict.sql +++ b/bms-back/sql/price/air_bill_fee_dict.sql @@ -32,6 +32,51 @@ WHERE NOT EXISTS ( SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_status' AND d.dict_value = v.dict_value ); +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, N'已确定', '3', 'bill_fee_status', '', 'primary', 'N', '0', 'admin', sysdate(), N'分摊结果确认后' +FROM DUAL WHERE NOT EXISTS ( + SELECT 1 FROM sys_dict_data d WHERE d.dict_type = 'bill_fee_status' AND d.dict_value = '3' +); + +SET @sql = ( + SELECT IF( + COUNT(*) > 0, + 'ALTER TABLE srv_bill_air_fee MODIFY COLUMN bill_fee_status char(1) NOT NULL DEFAULT ''0'' COMMENT ''提单费用状态,字典 bill_fee_status:0未分摊 1已分摊 2已作废 3已确定''', + 'SELECT 1' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'bill_fee_status' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + COUNT(*) = 0, + 'ALTER TABLE srv_bill_air_fee ADD COLUMN del_flag char(1) NOT NULL DEFAULT ''0'' COMMENT ''逻辑删除:0未删除 1已删除''', + 'SELECT 1' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_bill_air_fee' AND COLUMN_NAME = 'del_flag' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + COUNT(*) = 0, + 'ALTER TABLE prc_pay_bill_fee ADD COLUMN is_deleted char(1) NOT NULL DEFAULT ''0'' COMMENT ''是否删除:0未删除 1已删除''', + 'SELECT 1' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'prc_pay_bill_fee' AND COLUMN_NAME = 'is_deleted' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + SET @sql = ( SELECT IF( COUNT(*) = 0, diff --git a/bms-back/sql/price/air_bill_menu.sql b/bms-back/sql/price/air_bill_menu.sql index 8ff30fa4..8d494de0 100644 --- a/bms-back/sql/price/air_bill_menu.sql +++ b/bms-back/sql/price/air_bill_menu.sql @@ -38,3 +38,26 @@ WHERE rm.menu_id IN (2520, 2522) AND NOT EXISTS ( SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = fee.menu_id ); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2630, N'确定费用', 2520, 4, '', NULL, 1, 0, 'F', '0', '0', 'price:airBill:feeConfirm', '#', 'admin', sysdate(), N'已分摊的提单费用确定' +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2630 OR perms = 'price:airBill:feeConfirm'); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2631, N'作废费用', 2520, 5, '', NULL, 1, 0, 'F', '0', '0', 'price:airBill:feeVoid', '#', 'admin', sysdate(), N'作废已分摊或已确定的提单费用' +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2631 OR perms = 'price:airBill:feeVoid'); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id +FROM sys_menu m +WHERE m.perms IN ('price:airBill:feeConfirm', 'price:airBill:feeVoid') + AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, btn.menu_id +FROM sys_role_menu rm +JOIN sys_menu fee ON fee.menu_id = rm.menu_id AND fee.perms = 'price:airBill:fee' +JOIN sys_menu btn ON btn.perms IN ('price:airBill:feeConfirm', 'price:airBill:feeVoid') +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = btn.menu_id +); diff --git a/bms-front/src/api/price/airBill.js b/bms-front/src/api/price/airBill.js index dd2dc4dd..346e731e 100644 --- a/bms-front/src/api/price/airBill.js +++ b/bms-front/src/api/price/airBill.js @@ -36,6 +36,43 @@ export function listAirBillFeeLog(query) { }) } +/** 勾选提单后,查询需要分摊到订单的费用 */ +export function previewAirBillAlloc(billCodes) { + return request({ + url: '/price/airBill/fee/alloc/preview', + method: 'get', + params: { billCodes: (billCodes || []).join(',') } + }) +} + +/** 按重量或订单数把费用分摊到订单 */ +export function allocateAirBillFee(billCodes) { + return request({ + url: '/price/airBill/fee/alloc', + method: 'post', + data: { billCodes }, + timeout: 120000 + }) +} + +/** 已分摊的费用改为已确定 */ +export function confirmAirBillFee(billCode, feeId) { + return request({ + url: '/price/airBill/fee/confirm', + method: 'post', + params: { billCode, feeId } + }) +} + +/** 作废已分摊或已确定的费用,并逻辑删除订单上的分摊明细 */ +export function voidAirBillFee(billCode, feeId) { + return request({ + url: '/price/airBill/fee/void', + method: 'post', + params: { billCode, feeId } + }) +} + /** 只提交新增、修改、删除的费用 */ export function saveAirBillFee(data) { return request({ diff --git a/bms-front/src/components/BasicTable/index.vue b/bms-front/src/components/BasicTable/index.vue index 77aaa722..10a2fb95 100644 --- a/bms-front/src/components/BasicTable/index.vue +++ b/bms-front/src/components/BasicTable/index.vue @@ -238,6 +238,11 @@ const props = defineProps({ type: Boolean, default: true }, + // 最多勾选条数,0 表示不限制 + selectionMax: { + type: Number, + default: 0 + }, // 是否显示序号列 showIndex: { type: Boolean, @@ -436,10 +441,30 @@ function resetQuery() { getList() } -// 选中变化 +// 选中变化。超过上限时只保留先勾到的行,并同步虚拟表和普通表的勾选状态。 +let cappingSelection = false function handleSelectionChange(val) { - selectionList.value = val - emit('selection-change', val) + if (cappingSelection) return + const rows = Array.isArray(val) ? val.slice() : [] + const max = Number(props.selectionMax) || 0 + if (max > 0 && rows.length > max) { + const kept = rows.slice(0, max) + cappingSelection = true + selectionList.value = kept + emit('selection-change', kept) + proxy.$modal.msgWarning(`单次最多勾选 ${max} 条`) + nextTick(() => { + const table = tableRef.value + if (table?.toggleRowSelection) { + const keys = new Set(kept.map((row) => virtualRowId(row))) + tableData.value.forEach((row) => table.toggleRowSelection(row, keys.has(virtualRowId(row)))) + } + cappingSelection = false + }) + return + } + selectionList.value = rows + emit('selection-change', rows) } const selectionIds = computed(() => selectionList.value.map((r) => (props.rowKey ? r[props.rowKey] : r.id)).filter((v) => v !== undefined) diff --git a/bms-front/src/views/price/billing/airBill.vue b/bms-front/src/views/price/billing/airBill.vue index fead2bb6..614c7bd5 100644 --- a/bms-front/src/views/price/billing/airBill.vue +++ b/bms-front/src/views/price/billing/airBill.vue @@ -11,7 +11,20 @@ :page-size="100" :height="tableHeight" virtual + selection + :selection-max="5" > + @@ -62,9 +76,11 @@ import { listAirBill, getAirBill } from '@/api/price/airBill' import AirBillDetail from './components/AirBillDetail.vue' import AirBillFeeDialog from './components/AirBillFeeDialog.vue' +import AirBillAllocDialog from './components/AirBillAllocDialog.vue' const tableRef = ref() const feeRef = ref() +const allocRef = ref() const tableHeight = ref(480) let tableResizeObserver = null @@ -171,6 +187,10 @@ function openFee(row) { feeRef.value?.open(row) } +function openAlloc(rows) { + allocRef.value?.open((rows || []).map(row => row.billCode)) +} + function openDetail(row) { if (!row?.billCode) return open.value = true diff --git a/bms-front/src/views/price/billing/components/AirBillAllocDialog.vue b/bms-front/src/views/price/billing/components/AirBillAllocDialog.vue new file mode 100644 index 00000000..660ec030 --- /dev/null +++ b/bms-front/src/views/price/billing/components/AirBillAllocDialog.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/bms-front/src/views/price/billing/components/AirBillFeeDialog.vue b/bms-front/src/views/price/billing/components/AirBillFeeDialog.vue index 25507100..98062f3f 100644 --- a/bms-front/src/views/price/billing/components/AirBillFeeDialog.vue +++ b/bms-front/src/views/price/billing/components/AirBillFeeDialog.vue @@ -2,14 +2,14 @@
- 新增 + 新增 只保存本次新增、修改和删除的费用,未改动的行不会重写。
@@ -17,7 +17,7 @@ @@ -25,7 +25,7 @@ @@ -39,7 +39,7 @@ @@ -47,25 +47,27 @@ - + @@ -77,14 +79,14 @@ - +