This commit is contained in:
2026-08-21 14:30:59 +08:00
parent bd2c53fc15
commit 7683ad804a
18 changed files with 576 additions and 119 deletions
@@ -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;
@@ -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;
@@ -0,0 +1,58 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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;
}
@@ -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;
}
@@ -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<List<LoadingCarrierContractVO>> carrierContracts(
@Parameter(description = "运单ID集合", required = true) @RequestParam List<Long> waybillIds) {
return R.data(loadingManageService.carrierContracts(waybillIds));
}
@GetMapping("/list")
@ApiOperationSupport(order = 2)
@Operation(summary = "分页", description = "传入loadingManage")
@@ -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<MasterOrderVO> detail(@RequestParam Long id) { return R.data(masterOrderService.detail(id)); }
@GetMapping("/carriers") @ApiOperationSupport(order = 2) @Operation(summary = "调度可选承运商") public R<List<MasterOrderCarrierVO>> carriers(@RequestParam Long id) { return R.data(masterOrderService.carriers(id)); }
@GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页") public R<IPage<MasterOrderVO>> list(MasterOrderVO query, Query page) { return R.data(masterOrderService.selectPage(Condition.getPage(page), query)); }
@PostMapping("/submit") @ApiOperationSupport(order = 3) @Operation(summary = "确认创建或编辑") public R<MasterOrderVO> submit(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, false)); }
@PostMapping("/draft") @ApiOperationSupport(order = 4) @Operation(summary = "暂存草稿") public R<MasterOrderVO> draft(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, true)); }
@@ -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<LoadingManage> {
LoadingManageVO detail(Long id);
List<LoadingCarrierContractVO> carrierContracts(List<Long> waybillIds);
boolean saveDraft(LoadingManage loadingManage);
boolean submit(LoadingManage loadingManage);
@@ -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<MasterOrder> {
IPage<MasterOrderVO> selectPage(IPage<MasterOrder> page, MasterOrderVO query);
MasterOrderVO detail(Long id);
List<MasterOrderCarrierVO> carriers(Long id);
MasterOrderVO submit(MasterOrderVO masterOrder, boolean draft);
MasterOrderVO copy(Long id);
boolean removeMasterOrder(Long id);
@@ -77,6 +77,9 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
void generateForCompletedWaybills(List<Long> waybillIds);
/** 关闭总单调度后按合同系统计费模式自动生成总单应收、应付明细。 */
/** 完成配载单后按其记录的承运商合同自动生成应付明细。 */
void generateForCompletedLoading(List<Long> waybillIds, Long carrierContractId);
/** 关闭总单调度后生成总单客户合同应收,并按所属运单记录的承运商合同生成应付。 */
void generateForClosedMasterOrder(MasterOrder masterOrder);
}
@@ -507,12 +507,15 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
Long count = count(Wrappers.<ContractManage>lambdaQuery()
.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("相同项目、合同类别、甲方和乙方的合同已存在,不能重复提交");
}
}
@@ -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<LoadingManageMappe
private static final String STATUS_COMPLETED = "completed";
private static final String STATUS_CANCELLED = "cancelled";
private static final String CARRIER_SELF = "自运";
private static final String CARRIER_COMPANY = "承运商";
private static final String CARRIER_PLATFORM = "网签平台";
private static final String CARRIER_PLATFORM = "网货平台";
@Resource
private WaybillMapper waybillMapper;
@Resource
private IContractManageService contractManageService;
@Resource
@Lazy
private IReceivablePayableDetailService receivablePayableDetailService;
@@ -77,6 +85,17 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return loadingManageVO;
}
@Override
public List<LoadingCarrierContractVO> carrierContracts(List<Long> 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<LoadingManageMappe
loadingManage.setLoadingNo(nextCode());
}
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, false);
boolean result = saveOrUpdate(loadingManage);
syncAssociatedWaybills(loadingManage, STATUS_DRAFT, oldWaybillIds);
return result;
@@ -120,6 +140,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setLoadingNo(nextCode());
}
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, true);
boolean result = saveOrUpdate(loadingManage);
syncAssociatedWaybills(loadingManage, STATUS_PENDING, oldWaybillIds);
return result;
@@ -187,6 +208,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
target.setEscortPhone(source.getEscortPhone());
target.setCarrierType(source.getCarrierType());
target.setCarrierName(source.getCarrierName());
target.setCarrierContractId(source.getCarrierContractId());
target.setDepartureAddress(source.getDepartureAddress());
target.setTransitAddress(source.getTransitAddress());
target.setArrivalAddress(source.getArrivalAddress());
@@ -229,6 +251,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setDeptName(oldRecord.getDeptName());
loadingManage.setBusinessStatus(STATUS_PENDING);
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, true);
boolean result = updateById(loadingManage);
syncAssociatedWaybills(loadingManage, STATUS_PENDING, oldWaybillIds);
return result;
@@ -267,6 +290,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
@Transactional(rollbackFor = Exception.class)
public boolean complete(Long id) {
LoadingManage loadingManage = loadEditable(id, true);
validateCompletedCarrierContract(loadingManage);
if (!Objects.equals(loadingManage.getBusinessStatus(), STATUS_RUNNING)
&& !(Objects.equals(loadingManage.getBusinessStatus(), STATUS_PENDING)
&& allAssociatedWaybillsCompletable(loadingManage))) {
@@ -277,7 +301,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
List<Long> 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<LoadingManageMappe
|| Objects.equals(loadingManage.getBusinessStatus(), STATUS_DRAFT)) {
return false;
}
validateCompletedCarrierContract(loadingManage);
List<Long> waybillIdList = waybillIds(loadingManage.getWaybillIdsJson());
if (Func.isEmpty(waybillIdList)) {
return false;
@@ -311,7 +337,12 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return false;
}
loadingManage.setBusinessStatus(STATUS_COMPLETED);
return updateById(loadingManage);
boolean result = updateById(loadingManage);
if (result) {
receivablePayableDetailService.generateForCompletedLoading(
waybillIdList, loadingManage.getCarrierContractId());
}
return result;
}
@Override
@@ -478,11 +509,14 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
TransportBusinessSupport.validateRequired(loadingManage.getVehicleNo(), "车/船/航班/班列不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getDepartureAddress(), "发货地不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getArrivalAddress(), "到货地不能为空");
if (Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) {
if (!Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)
&& Func.isEmpty(loadingManage.getCarrierContractId())) {
throw new ServiceException("请选择承运商合同");
}
if (Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)
|| Objects.equals(loadingManage.getCarrierType(), CARRIER_PLATFORM)) {
TransportBusinessSupport.validateRequired(loadingManage.getDriverName(), "司机不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getDriverPhone(), "联系电话不能为空");
} else if (Objects.equals(loadingManage.getCarrierType(), CARRIER_COMPANY) || Objects.equals(loadingManage.getCarrierType(), CARRIER_PLATFORM)) {
TransportBusinessSupport.validateRequired(loadingManage.getCarrierName(), "承运商不能为空");
}
TransportBusinessSupport.validateLength(loadingManage.getLoadingNo(), 100, "配载单号不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getLoadingSubNos(), 200, "配载子单号不能超过200字");
@@ -557,6 +591,97 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
}
private void validateCarrierContract(LoadingManage loadingManage, boolean required) {
if (Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) {
loadingManage.setCarrierContractId(null);
loadingManage.setCarrierName(null);
return;
}
if (Func.isEmpty(loadingManage.getCarrierContractId())) {
if (required) {
throw new ServiceException("请选择承运商合同");
}
loadingManage.setCarrierName(null);
return;
}
ContractManage carrierContract = findCarrierContracts(
waybillIds(loadingManage.getWaybillIdsJson())).stream()
.filter(contract -> 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<ContractManage> findCarrierContracts(List<Long> waybillIds) {
if (Func.isEmpty(waybillIds)) {
return new ArrayList<>();
}
List<Long> distinctWaybillIds = waybillIds.stream()
.filter(Objects::nonNull)
.distinct()
.toList();
if (Func.isEmpty(distinctWaybillIds)) {
return new ArrayList<>();
}
List<Waybill> waybillList = waybillMapper.selectList(Wrappers.<Waybill>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<Long> customerContractIds = waybillList.stream()
.map(Waybill::getContractId)
.distinct()
.toList();
Map<Long, ContractManage> 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.<ContractManage>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<Long> oldWaybillIds) {
List<Long> newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
if (Func.isNotEmpty(oldWaybillIds)) {
@@ -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<MasterOrderMapper, M
return toVO(masterOrder);
}
@Override
public List<MasterOrderCarrierVO> 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<MasterOrderMapper, M
waybill.setContractId(masterOrder.getContractId()); waybill.setContractName(masterOrder.getContractName());
waybill.setCustomerName(masterOrder.getCustomerName()); waybill.setMasterNo(masterOrder.getMasterNo());
waybill.setTransportType(string(dispatch, "transportType")); waybill.setCarrierType(string(dispatch, "carrierType"));
waybill.setCarrierName(string(dispatch, "carrierName")); waybill.setDriverName(string(dispatch, "driverName"));
waybill.setCarrierContractId(longValue(dispatch, "carrierContractId")); waybill.setCarrierName(string(dispatch, "carrierName")); waybill.setDriverName(string(dispatch, "driverName"));
waybill.setDriverPhone(string(dispatch, "driverPhone")); waybill.setVehicleNo(string(dispatch, "vehicleNo")); waybill.setTrailerVehicleNo(string(dispatch, "trailerVehicleNo"));
waybill.setCaptainName(string(dispatch, "captainName")); waybill.setCabinNo(string(dispatch, "cabinNo")); waybill.setContainerNo(string(dispatch, "containerNo"));
waybill.setEscortName(string(dispatch, "escortName")); waybill.setEscortPhone(string(dispatch, "escortPhone")); waybill.setMileage(nullableDecimal(dispatch, "mileage"));
@@ -343,6 +355,8 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
private List<Map<String, Object>> 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<Map<String, Object>> dispatches) {
Map<String, BigDecimal> available = availableGoods(masterOrder);
Map<Long, ContractManage> availableCarrierContracts = availableCarrierContracts(masterOrder).stream()
.collect(java.util.stream.Collectors.toMap(ContractManage::getId, contract -> contract));
Map<String, Map<String, BigDecimal>> usedBySegment = new LinkedHashMap<>();
for (Map<String, Object> dispatch : dispatches) {
String segmentNo = string(dispatch, "segmentNo");
@@ -357,7 +371,9 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
LocalDate startDate = date(dispatch, "estimatedStartTime"); LocalDate endDate = date(dispatch, "estimatedEndTime");
if (startDate == null || endDate == null) throw new ServiceException("预计发货日期和预计完成日期不能为空");
if (startDate.isAfter(endDate)) throw new ServiceException("预计发货日期不能晚于预计完成日期");
if ("运单".equals(string(dispatch, "documentType", "运单"))) validateCarrier(dispatch);
if ("运单".equals(string(dispatch, "documentType", "运单"))) {
validateCarrier(dispatch, availableCarrierContracts);
}
Map<String, BigDecimal> 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<MasterOrderMapper, M
Map<String, BigDecimal> dispatched = dispatchedGoods(masterNo, segmentNo);
return available.entrySet().stream().allMatch(item -> dispatched.getOrDefault(item.getKey(), BigDecimal.ZERO).compareTo(item.getValue()) >= 0);
}
private void validateCarrier(Map<String, Object> dispatch) {
private List<ContractManage> 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<String, ContractManage> contractsByCarrier = new LinkedHashMap<>();
contractManageService.list(new LambdaQueryWrapper<ContractManage>()
.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<String, Object> dispatch, Map<Long, ContractManage> 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<MasterOrderMapper, M
freight.put("currency", string(dispatch, "currency", "CNY"));
return JsonUtil.toJson(freight);
}
private String waybillGroupKey(Map<String, Object> 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<String, Object> 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<Map<String, Object>> 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<String, Object> values, String key) { try { return new BigDecimal(string(values, key, "0")); } catch (Exception exception) { return BigDecimal.ZERO; } }
private BigDecimal nullableDecimal(Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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("日期格式不正确"); } }
@@ -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<Long> 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;
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<Long> 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<Waybill> loadWaybills(List<Long> waybillIds) {
if (Func.isEmpty(waybillIds)) return List.of();
return waybillService.list(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIds));
}
private void generateAutomaticWaybillDetails(List<Waybill> waybills, boolean generateReceivable,
Function<Waybill, ContractManage> payableContractResolver,
Long currentUserId, Date generateTime) {
for (Waybill waybill : waybills) {
try {
if (generateReceivable) {
ContractManage receivableContract = Func.isEmpty(waybill.getContractId())
? null : contractManageService.getById(waybill.getContractId());
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);
}
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,
private void generateAutomaticWaybillDetail(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<ReceivablePayableCargoFee> 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);
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<Waybill> masterGoods = masterOrderGoods(masterOrder);
if (masterGoods.isEmpty()) return;
List<ReceivablePayableCargoFee> 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<Waybill> 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<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0)
.eq(Waybill::getMasterNo, masterOrder.getMasterNo())
.isNotNull(Waybill::getCarrierContractId));
if (waybills.isEmpty()) return;
Map<Long, ContractManage> 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<ReceivablePayableCargoFee> calculateAutomaticFees(List<Waybill> waybills,
ContractManage contract) {
List<ReceivablePayableCargoFee> 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<Waybill> billingWaybills,
BiFunction<List<ReceivablePayableCargoFee>, BigDecimal,
ReceivablePayableDetail> detailBuilder,
boolean clearWaybillId, Long currentUserId, Date generateTime) {
if (!isAutomaticContract(contract, settlementType) || exists.getAsBoolean()) return;
List<ReceivablePayableCargoFee> 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<ReceivablePayableCargoFee> 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<ReceivablePayableCargoFee> 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<ReceivablePayableCargoFee> result = new ArrayList<>();
int line = 1;
Map<List<String>, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>();
Map<List<String>, Map<String, BigDecimal>> feeItemsByCargo = new LinkedHashMap<>();
Set<List<String>> freightBillingCargoKeys = new LinkedHashSet<>();
for (Object value : (List<?>) plan.get("rules")) {
if (!(value instanceof Map<?, ?> raw)) continue;
Map<String, Object> rule = new LinkedHashMap<>();
@@ -1020,23 +1101,67 @@ public class ReceivablePayableDetailServiceImpl
if (amount == null) continue;
String feeItem = stringValue(rule, "feeItem", "费用");
Map<String, String> feeGoods = summarizeFeeGoods(rule, feeWaybill);
List<String> cargoKey = cargoFeeKey(feeWaybill, feeGoods);
ReceivablePayableCargoFee fee = feesByCargo.computeIfAbsent(cargoKey,
key -> buildCalculatedCargoFee(waybill, feeWaybill, feeGoods, rule));
Map<String, BigDecimal> 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<ReceivablePayableCargoFee> 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<String, String> feeGoods, Map<String, Object> rule) {
ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee();
fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++));
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.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);
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<String, Object> 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")));
}
return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result;
private List<String> cargoFeeKey(Waybill waybill, Map<String, String> 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<Waybill> feeWaybills(Map<String, Object> rule, Waybill waybill) {
@@ -1354,22 +1479,26 @@ public class ReceivablePayableDetailServiceImpl
if (rules.isEmpty()) {
throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用");
}
Map<BigDecimal, Map<String, Object>> results = new LinkedHashMap<>();
Map<String, BigDecimal> calculatedAmounts = new LinkedHashMap<>();
BigDecimal calculatedFreight = BigDecimal.ZERO;
boolean freightRuleMatched = false;
for (Map<String, Object> rule : rules) {
BigDecimal amount = calculateRule(rule, adjustedWaybill);
if (amount != null) results.putIfAbsent(money(amount), rule);
}
if (results.size() != 1) {
throw new ServiceException("费用明细对应多个计费结果,无法唯一试算,请先更新费用");
}
Map.Entry<BigDecimal, Map<String, Object>> calculated = results.entrySet().iterator().next();
BigDecimal amount = calculated.getKey();
Map<String, Object> rule = calculated.getValue();
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 (calculatedAmounts.isEmpty()) {
throw new ServiceException("费用明细对应的合同计费规则无法试算,请先更新费用");
}
Map<String, BigDecimal> 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<Map<String, Object>> matchingAdjustedRules(ContractManage contract,
ReceivablePayableCargoFee fee, Waybill waybill) {
Set<String> feeItemNames = parseMap(fee.getFeeItemsJson()).keySet();
List<Map<String, Object>> candidates = new ArrayList<>();
List<Map<String, Object>> firstCandidates = List.of();
List<Map<String, Object>> defaultCandidates = List.of();
List<Map<String, Object>> billingMatchedCandidates = List.of();
for (Map<String, Object> plan : parseList(contract.getBillingPlanJson())) {
if (!(plan.get("rules") instanceof List<?> rules)) continue;
List<Map<String, Object>> candidates = new ArrayList<>();
for (Object value : rules) {
if (!(value instanceof Map<?, ?> raw)) continue;
Map<String, Object> 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<Map<String, Object>> unitPriceMatched = candidates.stream()
.filter(rule -> decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0)
.toList();
if (!unitPriceMatched.isEmpty()) candidates = unitPriceMatched;
List<Map<String, Object>> 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<String, Object> 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<String, BigDecimal> feeItems) {
@@ -1488,7 +1627,8 @@ public class ReceivablePayableDetailServiceImpl
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId);
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>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);
@@ -235,6 +235,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
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());
@@ -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='配载管理';
@@ -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;
+4
View File
@@ -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='配载管理';
@@ -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;