From 7683ad804a19f529de41af49304c460470e1ba62 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 21 Aug 2026 14:30:59 +0800 Subject: [PATCH] fix bug --- .../transport/pojo/entity/LoadingManage.java | 3 + .../transport/pojo/entity/Waybill.java | 3 + .../pojo/vo/LoadingCarrierContractVO.java | 58 +++ .../pojo/vo/MasterOrderCarrierVO.java | 37 ++ .../controller/LoadingManageController.java | 9 + .../controller/MasterOrderController.java | 4 + .../service/ILoadingManageService.java | 3 + .../service/IMasterOrderService.java | 2 + .../IReceivablePayableDetailService.java | 5 +- .../impl/ContractManageServiceImpl.java | 5 +- .../impl/LoadingManageServiceImpl.java | 139 ++++++- .../service/impl/MasterOrderServiceImpl.java | 62 ++- .../ReceivablePayableDetailServiceImpl.java | 352 ++++++++++++------ .../service/impl/WaybillServiceImpl.java | 1 + .../blade_loading_manage_20260804.sql | 2 + ...ading_manage_carrier_contract_20260821.sql | 3 + doc/sql/transport/blade_tms_business.sql | 4 + ...lade_waybill_carrier_contract_20260821.sql | 3 + 18 files changed, 576 insertions(+), 119 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java create mode 100644 doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql create mode 100644 doc/sql/transport/blade_waybill_carrier_contract_20260821.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java index 2488c5f..6393fc0 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java @@ -80,6 +80,9 @@ public class LoadingManage extends TenantEntity { @Schema(description = "承运商") private String carrierName; + @Schema(description = "承运商合同ID") + private Long carrierContractId; + @Schema(description = "发货地址") private String departureAddress; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index cbd2b07..e712ce0 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -127,6 +127,9 @@ public class Waybill extends TenantEntity { @Schema(description = "承运商名称") private String carrierName; + @Schema(description = "承运商合同ID") + private Long carrierContractId; + @Schema(description = "司机ID") private Long driverId; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java new file mode 100644 index 0000000..22a40b8 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 配载可选承运商合同视图类 + * + * @author Chill + */ +@Data +@Schema(description = "配载可选承运商合同") +public class LoadingCarrierContractVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "承运商合同ID") + private Long id; + + @Schema(description = "承运商合同名称") + private String contractName; + + @Schema(description = "承运商名称") + private String carrierName; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java new file mode 100644 index 0000000..1ce202d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 总单调度可选承运商视图类 + * + * @author Chill + */ +@Data +@Schema(description = "总单调度可选承运商") +public class MasterOrderCarrierVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "承运商合同ID") + private Long contractId; + + @Schema(description = "承运商合同名称") + private String contractName; + + @Schema(description = "承运商名称") + private String carrierName; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java index 939ac35..39b5263 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java @@ -21,6 +21,7 @@ import org.springblade.core.tool.utils.DateUtil; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.service.ILoadingManageService; import org.springframework.web.bind.annotation.GetMapping; @@ -53,6 +54,14 @@ public class LoadingManageController extends BladeController { return R.data(loadingManageService.detail(id)); } + @GetMapping("/carrier-contracts") + @ApiOperationSupport(order = 13) + @Operation(summary = "可选承运商合同", description = "根据运单ID集合查询共同可用的承运商合同") + public R> carrierContracts( + @Parameter(description = "运单ID集合", required = true) @RequestParam List waybillIds) { + return R.data(loadingManageService.carrierContracts(waybillIds)); + } + @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页", description = "传入loadingManage") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java index 9b5151c..bfeb237 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java @@ -15,6 +15,7 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; import org.springblade.transport.excel.MasterOrderWaybillExcel; +import org.springblade.transport.pojo.vo.MasterOrderCarrierVO; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.service.IMasterOrderService; import org.springframework.web.bind.annotation.GetMapping; @@ -24,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** * 多联总单控制器 * @@ -38,6 +41,7 @@ public class MasterOrderController extends BladeController { private final IMasterOrderService masterOrderService; @GetMapping("/detail") @ApiOperationSupport(order = 1) @Operation(summary = "详情") public R detail(@RequestParam Long id) { return R.data(masterOrderService.detail(id)); } + @GetMapping("/carriers") @ApiOperationSupport(order = 2) @Operation(summary = "调度可选承运商") public R> carriers(@RequestParam Long id) { return R.data(masterOrderService.carriers(id)); } @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页") public R> list(MasterOrderVO query, Query page) { return R.data(masterOrderService.selectPage(Condition.getPage(page), query)); } @PostMapping("/submit") @ApiOperationSupport(order = 3) @Operation(summary = "确认创建或编辑") public R submit(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, false)); } @PostMapping("/draft") @ApiOperationSupport(order = 4) @Operation(summary = "暂存草稿") public R draft(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, true)); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 50452e8..dbb4539 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -9,6 +9,7 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; import java.util.List; @@ -24,6 +25,8 @@ public interface ILoadingManageService extends BaseService { LoadingManageVO detail(Long id); + List carrierContracts(List waybillIds); + boolean saveDraft(LoadingManage loadingManage); boolean submit(LoadingManage loadingManage); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java index bb78280..f178ad0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest; import org.springblade.transport.pojo.entity.MasterOrder; +import org.springblade.transport.pojo.vo.MasterOrderCarrierVO; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.excel.MasterOrderWaybillExcel; @@ -18,6 +19,7 @@ import java.util.List; public interface IMasterOrderService extends BaseService { IPage selectPage(IPage page, MasterOrderVO query); MasterOrderVO detail(Long id); + List carriers(Long id); MasterOrderVO submit(MasterOrderVO masterOrder, boolean draft); MasterOrderVO copy(Long id); boolean removeMasterOrder(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index 0cae341..cf922ac 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -77,6 +77,9 @@ public interface IReceivablePayableDetailService extends BaseService waybillIds); - /** 关闭总单调度后按合同系统计费模式自动生成总单应收、应付明细。 */ + /** 完成配载单后按其记录的承运商合同自动生成应付明细。 */ + void generateForCompletedLoading(List waybillIds, Long carrierContractId); + + /** 关闭总单调度后生成总单客户合同应收,并按所属运单记录的承运商合同生成应付。 */ void generateForClosedMasterOrder(MasterOrder masterOrder); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index b1e79eb..d8c6f37 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -507,12 +507,15 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaQuery() .eq(ContractManage::getIsDeleted, 0) + .eq(Func.isNotEmpty(contractManage.getProjectId()), ContractManage::getProjectId, + contractManage.getProjectId()) + .isNull(Func.isEmpty(contractManage.getProjectId()), ContractManage::getProjectId) .eq(ContractManage::getContractCategory, contractManage.getContractCategory()) .eq(ContractManage::getPartyA, contractManage.getPartyA()) .eq(ContractManage::getPartyB, contractManage.getPartyB()) .ne(Func.isNotEmpty(contractManage.getId()), ContractManage::getId, contractManage.getId())); if (count > 0) { - throw new ServiceException("相同合同类别、甲方和乙方的合同已存在,不能重复提交"); + throw new ServiceException("相同项目、合同类别、甲方和乙方的合同已存在,不能重复提交"); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index b2a61a7..28cf212 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -17,10 +17,13 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.mapper.LoadingManageMapper; import org.springblade.transport.mapper.WaybillMapper; +import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; +import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.support.TransportBusinessSupport; @@ -36,7 +39,10 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 配载管理 服务实现类 @@ -52,12 +58,14 @@ public class LoadingManageServiceImpl extends BaseServiceImpl carrierContracts(List waybillIds) { + return findCarrierContracts(waybillIds).stream().map(contract -> { + LoadingCarrierContractVO option = new LoadingCarrierContractVO(); + option.setId(contract.getId()); + option.setContractName(contract.getContractName()); + option.setCarrierName(contract.getPartyB()); + return option; + }).toList(); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean saveDraft(LoadingManage loadingManage) { @@ -96,6 +115,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl associatedWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>()); if (result) { - receivablePayableDetailService.generateForCompletedWaybills(associatedWaybillIds); + receivablePayableDetailService.generateForCompletedLoading( + associatedWaybillIds, loadingManage.getCarrierContractId()); } return result; } @@ -298,6 +323,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isEmpty(waybillIdList)) { return false; @@ -311,7 +337,12 @@ public class LoadingManageServiceImpl extends BaseServiceImpl Objects.equals(contract.getId(), loadingManage.getCarrierContractId())) + .findFirst() + .orElseThrow(() -> new ServiceException("所选承运商合同与运单绑定的客户合同不匹配或已失效")); + loadingManage.setCarrierName(carrierContract.getPartyB()); + } + + private void validateCompletedCarrierContract(LoadingManage loadingManage) { + if (Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) { + loadingManage.setCarrierContractId(null); + loadingManage.setCarrierName(null); + return; + } + if (Func.isEmpty(loadingManage.getCarrierContractId())) { + throw new ServiceException("配载单未记录承运商合同,请先重新派单并选择承运商合同"); + } + } + + private List findCarrierContracts(List waybillIds) { + if (Func.isEmpty(waybillIds)) { + return new ArrayList<>(); + } + List distinctWaybillIds = waybillIds.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + if (Func.isEmpty(distinctWaybillIds)) { + return new ArrayList<>(); + } + List waybillList = waybillMapper.selectList(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, distinctWaybillIds)); + if (waybillList.size() != distinctWaybillIds.size()) { + throw new ServiceException("待配载运单不存在或已删除"); + } + waybillList.forEach(waybill -> + TransportBusinessSupport.assertCurrentDept(waybill.getDeptId(), "运单管理")); + if (waybillList.stream().anyMatch(waybill -> Func.isEmpty(waybill.getContractId()))) { + throw new ServiceException("所选运单存在未绑定客户合同的数据"); + } + List customerContractIds = waybillList.stream() + .map(Waybill::getContractId) + .distinct() + .toList(); + Map customerContractMap = contractManageService.listByIds(customerContractIds) + .stream() + .filter(contract -> Objects.equals(contract.getIsDeleted(), 0)) + .collect(Collectors.toMap(ContractManage::getId, Function.identity())); + if (customerContractMap.size() != customerContractIds.size()) { + throw new ServiceException("所选运单绑定的客户合同不存在或已删除"); + } + Waybill firstWaybill = waybillList.get(0); + ContractManage firstCustomerContract = customerContractMap.get(firstWaybill.getContractId()); + if (!Objects.equals(firstCustomerContract.getContractCategory(), "客户合同") + || Func.isEmpty(firstCustomerContract.getPartyA())) { + throw new ServiceException("所选运单绑定的客户合同信息不完整"); + } + boolean relationMismatch = waybillList.stream().anyMatch(waybill -> { + ContractManage customerContract = customerContractMap.get(waybill.getContractId()); + return !Objects.equals(customerContract.getContractCategory(), "客户合同") + || !Objects.equals(waybill.getProjectId(), firstWaybill.getProjectId()) + || !Objects.equals(customerContract.getPartyA(), firstCustomerContract.getPartyA()); + }); + if (relationMismatch) { + throw new ServiceException("所选运单的客户合同不属于同一项目和甲方,无法共用承运商合同"); + } + return contractManageService.list(Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, firstWaybill.getProjectId()) + .eq(ContractManage::getPartyA, firstCustomerContract.getPartyA()) + .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) + .or().ne(ContractManage::getContractStage, "terminated")) + .orderByDesc(ContractManage::getCreateTime)); + } + private void syncAssociatedWaybills(LoadingManage loadingManage, String status, List oldWaybillIds) { List newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isNotEmpty(oldWaybillIds)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index 8ec2c59..38cdfa5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -20,6 +20,7 @@ import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.TransportPlan; import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.MasterOrderCarrierVO; import org.springblade.transport.pojo.vo.MasterOrderVO; import org.springblade.transport.service.IMasterOrderService; import org.springblade.transport.service.IContractManageService; @@ -77,6 +78,17 @@ public class MasterOrderServiceImpl extends BaseServiceImpl carriers(Long id) { + return availableCarrierContracts(getRequired(id)).stream().map(contract -> { + MasterOrderCarrierVO carrier = new MasterOrderCarrierVO(); + carrier.setContractId(contract.getId()); + carrier.setContractName(contract.getContractName()); + carrier.setCarrierName(contract.getPartyB()); + return carrier; + }).toList(); + } + @Override @Transactional(rollbackFor = Exception.class) public MasterOrderVO submit(MasterOrderVO request, boolean draft) { @@ -203,7 +215,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl> parseArray(String json) { if (Func.isEmpty(json)) return new ArrayList<>(); try { return JsonUtil.parse(json, List.class); } catch (Exception exception) { return new ArrayList<>(); } } private void validateDispatches(MasterOrder masterOrder, List> dispatches) { Map available = availableGoods(masterOrder); + Map availableCarrierContracts = availableCarrierContracts(masterOrder).stream() + .collect(java.util.stream.Collectors.toMap(ContractManage::getId, contract -> contract)); Map> usedBySegment = new LinkedHashMap<>(); for (Map dispatch : dispatches) { String segmentNo = string(dispatch, "segmentNo"); @@ -357,7 +371,9 @@ public class MasterOrderServiceImpl extends BaseServiceImpl used = usedBySegment.computeIfAbsent(segmentNo, value -> dispatchedGoods(masterOrder.getMasterNo(), value)); used.merge(key, quantity, BigDecimal::add); if (used.get(key).compareTo(available.get(key)) > 0) throw new ServiceException("货物【" + string(dispatch, "cargoName") + "】的调度数量超过可调度数量"); @@ -372,8 +388,45 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatched = dispatchedGoods(masterNo, segmentNo); return available.entrySet().stream().allMatch(item -> dispatched.getOrDefault(item.getKey(), BigDecimal.ZERO).compareTo(item.getValue()) >= 0); } - private void validateCarrier(Map dispatch) { + private List availableCarrierContracts(MasterOrder masterOrder) { + if (Func.isEmpty(masterOrder.getContractId())) { + throw new ServiceException("总单未绑定客户合同"); + } + ContractManage customerContract = contractManageService.getById(masterOrder.getContractId()); + if (customerContract == null || !Objects.equals(customerContract.getContractCategory(), "客户合同") + || Func.isEmpty(customerContract.getProjectId())) { + throw new ServiceException("总单绑定的客户合同不存在或项目信息不完整"); + } + if (!Objects.equals(masterOrder.getProjectId(), customerContract.getProjectId())) { + throw new ServiceException("总单项目与客户合同所属项目不一致"); + } + Map contractsByCarrier = new LinkedHashMap<>(); + contractManageService.list(new LambdaQueryWrapper() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, customerContract.getProjectId()) + .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) + .or().ne(ContractManage::getContractStage, "terminated")) + .orderByDesc(ContractManage::getCreateTime)) + .stream().filter(contract -> Func.isNotEmpty(contract.getPartyB())) + .forEach(contract -> contractsByCarrier.putIfAbsent(contract.getPartyB(), contract)); + return new ArrayList<>(contractsByCarrier.values()); + } + + private void validateCarrier(Map dispatch, Map availableCarrierContracts) { String carrierType = string(dispatch, "carrierType", "承运商"); + if ("自运".equals(carrierType)) { + dispatch.remove("carrierContractId"); + dispatch.remove("carrierName"); + } + String carrierName = string(dispatch, "carrierName"); + Long carrierContractId = longValue(dispatch, "carrierContractId"); + ContractManage carrierContract = carrierContractId == null ? null : availableCarrierContracts.get(carrierContractId); + if (!"自运".equals(carrierType) && (carrierContract == null + || !Objects.equals(carrierContract.getPartyB(), carrierName))) { + throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙方"); + } boolean road = string(dispatch, "transportType", "").toLowerCase().contains("road") || string(dispatch, "transportType", "").contains("公路"); if (!road) { if (Func.isEmpty(string(dispatch, "vehicleNo")) || Func.isEmpty(string(dispatch, "captainName")) || Func.isEmpty(string(dispatch, "driverPhone")) || Func.isEmpty(string(dispatch, "containerNo")) || Func.isEmpty(string(dispatch, "cabinNo")) || Func.isEmpty(string(dispatch, "mileage")) || decimal(dispatch, "mileage").compareTo(BigDecimal.ZERO) <= 0 || ("承运商".equals(carrierType) && Func.isEmpty(string(dispatch, "carrierName")))) { @@ -453,10 +506,11 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "driverPhone", ""), string(dispatch, "vehicleNo", ""), string(dispatch, "captainName", ""), string(dispatch, "containerNo", ""), string(dispatch, "cabinNo", "")); } + private String waybillGroupKey(Map dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierContractId", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "driverPhone", ""), string(dispatch, "vehicleNo", ""), string(dispatch, "captainName", ""), string(dispatch, "containerNo", ""), string(dispatch, "cabinNo", "")); } private String joinGoodsField(List> dispatches, String field) { return dispatches.stream().map(item -> string(item, field, "")).filter(Func::isNotEmpty).distinct().reduce((left, right) -> left + "、" + right).orElse(""); } private BigDecimal decimal(Map values, String key) { try { return new BigDecimal(string(values, key, "0")); } catch (Exception exception) { return BigDecimal.ZERO; } } private BigDecimal nullableDecimal(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return new BigDecimal(value); } catch (Exception exception) { return null; } } + private Long longValue(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return Long.valueOf(value); } catch (Exception exception) { return null; } } private String goodsKey(Map values) { return goodsKey(string(values, "cargoName"), string(values, "cargoType")); } private String goodsKey(String cargoName, String cargoType) { return String.valueOf(cargoName) + "\u0000" + String.valueOf(cargoType); } private LocalDate date(Map values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return LocalDate.parse(value.substring(0, 10)); } catch (Exception exception) { throw new ServiceException("日期格式不正确"); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 8a0135d..4aef6e3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -84,6 +84,10 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 应收应付明细服务实现类 @@ -535,25 +539,46 @@ public class ReceivablePayableDetailServiceImpl @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void generateForCompletedWaybills(List waybillIds) { - if (Func.isEmpty(waybillIds)) return; - Long currentUserId = AuthUtil.getUserId(); - Date generateTime = new Date(); - for (Long waybillId : waybillIds) { - Waybill waybill = waybillService.getById(waybillId); - if (waybill == null) continue; - ContractManage receivableContract = Func.isEmpty(waybill.getContractId()) - ? null : contractManageService.getById(waybill.getContractId()); + generateAutomaticWaybillDetails(loadWaybills(waybillIds), true, + this::resolveWaybillCarrierContract, AuthUtil.getUserId(), new Date()); + } + + @Override + @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, + rollbackFor = Exception.class) + public void generateForCompletedLoading(List waybillIds, Long carrierContractId) { + ContractManage carrierContract = carrierContractId == null + ? null : contractManageService.getById(carrierContractId); + if (carrierContractId != null && (carrierContract == null + || !"承运商合同".equals(carrierContract.getContractCategory()))) { + throw new ServiceException("配载单记录的承运商合同不存在或合同类别不正确"); + } + generateAutomaticWaybillDetails(loadWaybills(waybillIds), true, + waybill -> carrierContract, AuthUtil.getUserId(), new Date()); + } + + private List loadWaybills(List waybillIds) { + if (Func.isEmpty(waybillIds)) return List.of(); + return waybillService.list(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .in(Waybill::getId, waybillIds)); + } + + private void generateAutomaticWaybillDetails(List waybills, boolean generateReceivable, + Function payableContractResolver, + Long currentUserId, Date generateTime) { + for (Waybill waybill : waybills) { try { - if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory()) - && isSystemGeneration(receivableContract)) { - generateCompletedWaybillDetail(waybill, receivableContract, "receivable", currentUserId, generateTime); - } - if (Func.isNotEmpty(waybill.getCarrierName())) { - ContractManage payableContract = findCarrierContract(waybill); - if (payableContract != null && isSystemGeneration(payableContract)) { - generateCompletedWaybillDetail(waybill, payableContract, "payable", currentUserId, generateTime); - } + if (generateReceivable) { + ContractManage receivableContract = Func.isEmpty(waybill.getContractId()) + ? null : contractManageService.getById(waybill.getContractId()); + generateAutomaticWaybillDetail(waybill, receivableContract, "receivable", + currentUserId, generateTime); } + ContractManage payableContract = payableContractResolver == null + ? null : payableContractResolver.apply(waybill); + generateAutomaticWaybillDetail(waybill, payableContract, "payable", + currentUserId, generateTime); } catch (Exception exception) { log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, receivableContractId:{}, failureReason:{}", waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception); @@ -565,24 +590,24 @@ public class ReceivablePayableDetailServiceImpl } } - private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType, - Long currentUserId, Date generateTime) { - if (existsByWaybill(waybill.getId(), settlementType)) return; - String planId = matchedPlanId(waybill, contract); - if (Func.isEmpty(planId)) return; - // 自动生成只按合同默认计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 - List matchedFees = calculatedFees(waybill, contract, planId, true); - if (matchedFees.isEmpty()) return; - BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); - ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); - save(detail); - for (ReceivablePayableCargoFee fee : matchedFees) { - ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); - copy.setId(null); - copy.setDetailId(detail.getId()); - fillGeneratedAuditFields(copy, currentUserId, generateTime); - cargoFeeMapper.insert(copy); + private void generateAutomaticWaybillDetail(Waybill waybill, ContractManage contract, String settlementType, + Long currentUserId, Date generateTime) { + generateAutomaticDetail(contract, settlementType, + () -> existsByWaybill(waybill.getId(), settlementType), List.of(waybill), + (fees, unitPrice) -> buildDetail(waybill, contract, settlementType, fees, unitPrice), + false, currentUserId, generateTime); + } + + private ContractManage resolveWaybillCarrierContract(Waybill waybill) { + if (Func.isNotEmpty(waybill.getLoadingNo())) return null; + if (Func.isNotEmpty(waybill.getCarrierContractId())) { + ContractManage contract = contractManageService.getById(waybill.getCarrierContractId()); + if (contract == null || !"承运商合同".equals(contract.getContractCategory())) { + throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确"); + } + return contract; } + return findCarrierContract(waybill); } private ContractManage findCarrierContract(Waybill waybill) { @@ -611,39 +636,14 @@ public class ReceivablePayableDetailServiceImpl @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void generateForClosedMasterOrder(MasterOrder masterOrder) { - if (masterOrder == null || Func.isEmpty(masterOrder.getId()) || Func.isEmpty(masterOrder.getContractId())) { + if (masterOrder == null || Func.isEmpty(masterOrder.getId()) || Func.isEmpty(masterOrder.getMasterNo())) { return; } - ContractManage contract = contractManageService.getById(masterOrder.getContractId()); - if (contract == null || !isSystemGeneration(contract)) return; Long currentUserId = AuthUtil.getUserId(); Date generateTime = new Date(); try { - List masterGoods = masterOrderGoods(masterOrder); - if (masterGoods.isEmpty()) return; - List matchedFees = new ArrayList<>(); - for (Waybill masterGoodsItem : masterGoods) { - String planId = matchedPlanId(masterGoodsItem, contract); - if (Func.isEmpty(planId)) continue; - matchedFees.addAll(calculatedFees(masterGoodsItem, contract, planId, true)); - } - if (matchedFees.isEmpty()) return; - normalizeMasterFeeLines(matchedFees); - BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); - for (String settlementType : List.of("payable", "receivable")) { - if (existsByMasterOrder(masterOrder.getMasterNo(), settlementType)) continue; - ReceivablePayableDetail detail = buildMasterOrderDetail(masterOrder, contract, masterGoods, - settlementType, matchedFees, contractUnitPrice); - save(detail); - for (ReceivablePayableCargoFee fee : matchedFees) { - ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); - copy.setId(null); - copy.setDetailId(detail.getId()); - copy.setWaybillId(null); - fillGeneratedAuditFields(copy, currentUserId, generateTime); - cargoFeeMapper.insert(copy); - } - } + generateClosedMasterOrderReceivable(masterOrder, currentUserId, generateTime); + generateClosedMasterOrderPayables(masterOrder, currentUserId, generateTime); } catch (Exception exception) { log.error("自动生成总单费用明细失败,masterOrderId:{}, masterNo:{}, contractId:{}, failureReason:{}", masterOrder.getId(), masterOrder.getMasterNo(), masterOrder.getContractId(), exception.getMessage(), exception); @@ -654,6 +654,85 @@ public class ReceivablePayableDetailServiceImpl } } + private void generateClosedMasterOrderReceivable(MasterOrder masterOrder, Long currentUserId, + Date generateTime) { + if (Func.isEmpty(masterOrder.getContractId())) return; + ContractManage customerContract = contractManageService.getById(masterOrder.getContractId()); + List masterGoods = masterOrderGoods(masterOrder); + if (masterGoods.isEmpty()) return; + generateAutomaticDetail(customerContract, "receivable", + () -> existsByMasterOrder(masterOrder.getMasterNo(), "receivable"), masterGoods, + (fees, unitPrice) -> { + normalizeMasterFeeLines(fees); + return buildMasterOrderDetail(masterOrder, customerContract, masterGoods, + "receivable", fees, unitPrice); + }, true, currentUserId, generateTime); + } + + private void generateClosedMasterOrderPayables(MasterOrder masterOrder, Long currentUserId, + Date generateTime) { + List waybills = waybillService.list(Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getMasterNo, masterOrder.getMasterNo()) + .isNotNull(Waybill::getCarrierContractId)); + if (waybills.isEmpty()) return; + Map carrierContracts = contractManageService.listByIds(waybills.stream() + .map(Waybill::getCarrierContractId).distinct().toList()).stream() + .collect(Collectors.toMap(ContractManage::getId, contract -> contract)); + generateAutomaticWaybillDetails(waybills, false, waybill -> { + ContractManage carrierContract = carrierContracts.get(waybill.getCarrierContractId()); + if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) { + throw new ServiceException("运单【" + waybill.getWaybillNo() + "】记录的承运商合同不存在或合同类别不正确"); + } + return carrierContract; + }, currentUserId, generateTime); + } + + private boolean isAutomaticContract(ContractManage contract, String settlementType) { + if (contract == null) return false; + String expectedCategory = "payable".equals(settlementType) ? "承运商合同" : "客户合同"; + return Objects.equals(expectedCategory, contract.getContractCategory()) && isSystemGeneration(contract); + } + + private List calculateAutomaticFees(List waybills, + ContractManage contract) { + List matchedFees = new ArrayList<>(); + for (Waybill waybill : waybills) { + String planId = matchedPlanId(waybill, contract); + if (Func.isEmpty(planId)) continue; + // 自动生成统一使用合同默认计费方案,且禁止回退读取运单自身其他费用。 + matchedFees.addAll(calculatedFees(waybill, contract, planId, true)); + } + return matchedFees; + } + + private void generateAutomaticDetail(ContractManage contract, String settlementType, + BooleanSupplier exists, List billingWaybills, + BiFunction, BigDecimal, + ReceivablePayableDetail> detailBuilder, + boolean clearWaybillId, Long currentUserId, Date generateTime) { + if (!isAutomaticContract(contract, settlementType) || exists.getAsBoolean()) return; + List matchedFees = calculateAutomaticFees(billingWaybills, contract); + if (matchedFees.isEmpty()) return; + BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); + ReceivablePayableDetail detail = detailBuilder.apply(matchedFees, contractUnitPrice); + saveAutomaticDetail(detail, matchedFees, clearWaybillId, currentUserId, generateTime); + } + + private void saveAutomaticDetail(ReceivablePayableDetail detail, + List fees, boolean clearWaybillId, + Long currentUserId, Date generateTime) { + save(detail); + for (ReceivablePayableCargoFee fee : fees) { + ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); + copy.setId(null); + copy.setDetailId(detail.getId()); + if (clearWaybillId) copy.setWaybillId(null); + fillGeneratedAuditFields(copy, currentUserId, generateTime); + cargoFeeMapper.insert(copy); + } + } + private boolean isSystemGeneration(ContractManage contract) { if (Func.isNotEmpty(contract.getFeeGenerationMode())) { return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode()); @@ -833,7 +912,8 @@ public class ReceivablePayableDetailServiceImpl private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, List fees, BigDecimal unitPrice) { - BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); ReceivablePayableDetail detail = new ReceivablePayableDetail(); @@ -1008,8 +1088,9 @@ public class ReceivablePayableDetailServiceImpl if (plan == null || !(plan.get("rules") instanceof List)) { return matchOnly ? List.of() : buildCargoFees(waybill); } - List result = new ArrayList<>(); - int line = 1; + Map, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>(); + Map, Map> feeItemsByCargo = new LinkedHashMap<>(); + Set> freightBillingCargoKeys = new LinkedHashSet<>(); for (Object value : (List) plan.get("rules")) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); @@ -1020,25 +1101,69 @@ public class ReceivablePayableDetailServiceImpl if (amount == null) continue; String feeItem = stringValue(rule, "feeItem", "费用"); Map feeGoods = summarizeFeeGoods(rule, feeWaybill); - ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); - fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); - fee.setCargoName(feeGoods.getOrDefault("cargoName", feeWaybill.getCargoName())); - fee.setCargoType(feeGoods.getOrDefault("cargoType", feeWaybill.getCargoType())); - fee.setSpecification(feeGoods.getOrDefault("specification", feeWaybill.getSpecification())); - fee.setModel(feeGoods.getOrDefault("model", feeWaybill.getModel())); - fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); - fee.setTransportQuantity(measure(rule, feeWaybill)); - fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", feeWaybill.getQuantityUnit())); - fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); - fee.setMileage(normalizeGeneratedMileage(feeWaybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); - fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); - fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", feeWaybill.getRemark())); - result.add(fee); + List cargoKey = cargoFeeKey(feeWaybill, feeGoods); + ReceivablePayableCargoFee fee = feesByCargo.computeIfAbsent(cargoKey, + key -> buildCalculatedCargoFee(waybill, feeWaybill, feeGoods, rule)); + Map feeItems = feeItemsByCargo.computeIfAbsent(cargoKey, + key -> new LinkedHashMap<>()); + feeItems.merge(feeItem, amount, BigDecimal::add); + fee.setFeeItemsJson(JsonUtil.toJson(feeItems)); + fee.setOriginalAmount(money(fee.getOriginalAmount()).add(amount)); + fee.setAfterAmount(fee.getOriginalAmount()); + if (isFreightRule(rule)) { + fee.setFreightAmount(money(fee.getFreightAmount()).add(amount)); + if (freightBillingCargoKeys.add(cargoKey)) { + fillCalculatedBillingFields(fee, feeWaybill, rule); + } + } } } + List result = new ArrayList<>(feesByCargo.values()); + for (int index = 0; index < result.size(); index++) { + ReceivablePayableCargoFee fee = result.get(index); + fee.setLineNo(String.format("%04d", index + 1)); + } return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result; } + private ReceivablePayableCargoFee buildCalculatedCargoFee(Waybill waybill, Waybill feeWaybill, + Map feeGoods, Map rule) { + ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); + fee.setWaybillId(waybill.getId()); + fee.setCargoName(feeGoods.getOrDefault("cargoName", feeWaybill.getCargoName())); + fee.setCargoType(feeGoods.getOrDefault("cargoType", feeWaybill.getCargoType())); + fee.setSpecification(feeGoods.getOrDefault("specification", feeWaybill.getSpecification())); + fee.setModel(feeGoods.getOrDefault("model", feeWaybill.getModel())); + fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", feeWaybill.getQuantityUnit())); + fillCalculatedBillingFields(fee, feeWaybill, rule); + fee.setMileage(normalizeGeneratedMileage(feeWaybill.getMileage())); + fee.setFreightAmount(BigDecimal.ZERO); + fee.setOriginalAmount(BigDecimal.ZERO); + fee.setAdjustAmount(BigDecimal.ZERO); + fee.setAfterAmount(BigDecimal.ZERO); + fee.setRemark(stringValue(rule, "remark", feeWaybill.getRemark())); + return fee; + } + + private void fillCalculatedBillingFields(ReceivablePayableCargoFee fee, Waybill feeWaybill, + Map rule) { + fee.setBillingFactor(stringValue(rule, "billingElement", "")); + fee.setBillingType(stringValue(rule, "billingType", "")); + fee.setTransportQuantity(measure(rule, feeWaybill)); + fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit())); + fee.setUnitPrice(decimal(rule.get("unitPrice"))); + } + + private List cargoFeeKey(Waybill waybill, Map feeGoods) { + return List.of( + Objects.toString(feeGoods.getOrDefault("cargoName", waybill.getCargoName()), ""), + Objects.toString(feeGoods.getOrDefault("cargoType", waybill.getCargoType()), ""), + Objects.toString(feeGoods.getOrDefault("specification", waybill.getSpecification()), ""), + Objects.toString(feeGoods.getOrDefault("model", waybill.getModel()), ""), + Objects.toString(feeGoods.getOrDefault("quantityUnit", waybill.getQuantityUnit()), ""), + money(waybill.getQuantity()).stripTrailingZeros().toPlainString()); + } + private List feeWaybills(Map rule, Waybill waybill) { String element = stringValue(rule, "billingElement", "按重量"); if (!List.of("按重量", "按体积", "按吨·公里", "按数量").contains(element)) { @@ -1354,22 +1479,26 @@ public class ReceivablePayableDetailServiceImpl if (rules.isEmpty()) { throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用"); } - Map> results = new LinkedHashMap<>(); + Map calculatedAmounts = new LinkedHashMap<>(); + BigDecimal calculatedFreight = BigDecimal.ZERO; + boolean freightRuleMatched = false; for (Map rule : rules) { BigDecimal amount = calculateRule(rule, adjustedWaybill); - if (amount != null) results.putIfAbsent(money(amount), rule); + if (amount == null) continue; + String feeItem = stringValue(rule, "feeItem", "费用"); + calculatedAmounts.merge(feeItem, amount, BigDecimal::add); + if (isFreightRule(rule)) { + calculatedFreight = calculatedFreight.add(amount); + freightRuleMatched = true; + } } - if (results.size() != 1) { - throw new ServiceException("费用明细对应多个计费结果,无法唯一试算,请先更新费用"); + if (calculatedAmounts.isEmpty()) { + throw new ServiceException("费用明细对应的合同计费规则无法试算,请先更新费用"); } - Map.Entry> calculated = results.entrySet().iterator().next(); - BigDecimal amount = calculated.getKey(); - Map rule = calculated.getValue(); - String feeItem = stringValue(rule, "feeItem", "费用"); Map calculatedFeeItems = new LinkedHashMap<>(feeItems); - calculatedFeeItems.put(feeItem, amount); - BigDecimal calculatedFreight = isFreightRule(rule) ? amount : freightAmount; - return new AdjustedFeeCalculation(calculatedFreight, calculatedFeeItems); + calculatedAmounts.forEach(calculatedFeeItems::put); + return new AdjustedFeeCalculation(freightRuleMatched ? calculatedFreight : freightAmount, + calculatedFeeItems); } private Waybill adjustedWaybill(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee, @@ -1402,28 +1531,38 @@ public class ReceivablePayableDetailServiceImpl private List> matchingAdjustedRules(ContractManage contract, ReceivablePayableCargoFee fee, Waybill waybill) { Set feeItemNames = parseMap(fee.getFeeItemsJson()).keySet(); - List> candidates = new ArrayList<>(); + List> firstCandidates = List.of(); + List> defaultCandidates = List.of(); + List> billingMatchedCandidates = List.of(); for (Map plan : parseList(contract.getBillingPlanJson())) { if (!(plan.get("rules") instanceof List rules)) continue; + List> candidates = new ArrayList<>(); for (Object value : rules) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); - if (!Objects.equals(stringValue(rule, "billingElement"), fee.getBillingFactor()) - || !Objects.equals(stringValue(rule, "billingType"), fee.getBillingType()) - || !feeItemNames.contains(stringValue(rule, "feeItem"))) continue; - if (Func.isNotEmpty(fee.getPriceUnit()) - && !Objects.equals(stringValue(rule, "billingUnit"), fee.getPriceUnit())) continue; + if (!feeItemNames.contains(stringValue(rule, "feeItem")) || !matchesRule(rule, waybill)) continue; candidates.add(rule); } + if (candidates.isEmpty()) continue; + if (firstCandidates.isEmpty()) firstCandidates = candidates; + if (isDefaultPlan(plan)) defaultCandidates = candidates; + if (candidates.stream().anyMatch(rule -> matchesBillingFields(rule, fee))) { + if (billingMatchedCandidates.isEmpty() || isDefaultPlan(plan)) { + billingMatchedCandidates = candidates; + } + } } - List> unitPriceMatched = candidates.stream() - .filter(rule -> decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0) - .toList(); - if (!unitPriceMatched.isEmpty()) candidates = unitPriceMatched; - List> conditionMatched = candidates.stream() - .filter(rule -> matchesRule(rule, waybill)).toList(); - return conditionMatched.isEmpty() ? candidates : conditionMatched; + if (!billingMatchedCandidates.isEmpty()) return billingMatchedCandidates; + return defaultCandidates.isEmpty() ? firstCandidates : defaultCandidates; + } + + private boolean matchesBillingFields(Map rule, ReceivablePayableCargoFee fee) { + if (!Objects.equals(stringValue(rule, "billingElement"), fee.getBillingFactor()) + || !Objects.equals(stringValue(rule, "billingType"), fee.getBillingType())) return false; + if (Func.isNotEmpty(fee.getPriceUnit()) + && !Objects.equals(stringValue(rule, "billingUnit"), fee.getPriceUnit())) return false; + return decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0; } private BigDecimal adjustedAfterAmount(BigDecimal freightAmount, Map feeItems) { @@ -1488,7 +1627,8 @@ public class ReceivablePayableDetailServiceImpl List fees = calculatedFees(waybill, contract, billingPlanId); cargoFeeMapper.delete(Wrappers.lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId())); fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); }); - BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); detail.setFreightAmount(freight); detail.setOtherFeeAmount(total.subtract(freight)); @@ -1517,8 +1657,8 @@ public class ReceivablePayableDetailServiceImpl fee.setWaybillId(null); cargoFeeMapper.insert(fee); }); - BigDecimal freight = fees.stream().filter(this::isFreight) - .map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount())) + .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); detail.setFreightAmount(freight); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index b9df29f..d3acfab 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -235,6 +235,7 @@ public class WaybillServiceImpl extends BaseServiceImpl target.setCarrierType(source.getCarrierType()); target.setCarrierId(source.getCarrierId()); target.setCarrierName(source.getCarrierName()); + target.setCarrierContractId(source.getCarrierContractId()); target.setDriverId(source.getDriverId()); target.setDriverName(source.getDriverName()); target.setDriverPhone(source.getDriverPhone()); diff --git a/doc/sql/transport/blade_loading_manage_20260804.sql b/doc/sql/transport/blade_loading_manage_20260804.sql index 3096e6d..ffadac6 100644 --- a/doc/sql/transport/blade_loading_manage_20260804.sql +++ b/doc/sql/transport/blade_loading_manage_20260804.sql @@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` ( `escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号', `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', `carrier_name` varchar(100) DEFAULT NULL COMMENT '承运商', + `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID', `departure_address` varchar(100) DEFAULT NULL COMMENT '发货地', `transit_address` varchar(200) DEFAULT NULL COMMENT '途经地', `arrival_address` varchar(100) DEFAULT NULL COMMENT '到货地', @@ -47,6 +48,7 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` ( `business_status` varchar(100) DEFAULT NULL COMMENT '业务状态', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE, + KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE, KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理'; diff --git a/doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql b/doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql new file mode 100644 index 0000000..97efaf1 --- /dev/null +++ b/doc/sql/transport/blade_loading_manage_carrier_contract_20260821.sql @@ -0,0 +1,3 @@ +ALTER TABLE `blade_loading_manage` + ADD COLUMN `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID' AFTER `carrier_name`, + ADD KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE; diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index 3d5ec50..f6f44c8 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -244,6 +244,7 @@ CREATE TABLE `blade_waybill` ( `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', `carrier_id` bigint(20) DEFAULT NULL COMMENT '承运商ID', `carrier_name` varchar(100) DEFAULT NULL COMMENT '承运商名称', + `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID', `driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID', `driver_name` varchar(100) DEFAULT NULL COMMENT '司机姓名', `driver_phone` varchar(50) DEFAULT NULL COMMENT '司机手机号', @@ -285,6 +286,7 @@ CREATE TABLE `blade_waybill` ( PRIMARY KEY (`id`) USING BTREE, KEY `idx_waybill_dept` (`dept_id`) USING BTREE, KEY `idx_waybill_plan_id` (`plan_id`) USING BTREE, + KEY `idx_waybill_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_waybill_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运单管理'; @@ -319,6 +321,7 @@ CREATE TABLE `blade_loading_manage` ( `escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号', `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', `carrier_name` varchar(100) DEFAULT NULL COMMENT '承运商', + `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID', `departure_address` varchar(100) DEFAULT NULL COMMENT '发货地', `transit_address` varchar(200) DEFAULT NULL COMMENT '途经地', `arrival_address` varchar(100) DEFAULT NULL COMMENT '到货地', @@ -341,6 +344,7 @@ CREATE TABLE `blade_loading_manage` ( `business_status` varchar(100) DEFAULT NULL COMMENT '业务状态', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE, + KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE, KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理'; diff --git a/doc/sql/transport/blade_waybill_carrier_contract_20260821.sql b/doc/sql/transport/blade_waybill_carrier_contract_20260821.sql new file mode 100644 index 0000000..02c5261 --- /dev/null +++ b/doc/sql/transport/blade_waybill_carrier_contract_20260821.sql @@ -0,0 +1,3 @@ +ALTER TABLE `blade_waybill` + ADD COLUMN `carrier_contract_id` bigint(20) DEFAULT NULL COMMENT '承运商合同ID' AFTER `carrier_name`, + ADD KEY `idx_waybill_carrier_contract_id` (`carrier_contract_id`) USING BTREE;