调整项目-合同-运单-结算逻辑链

This commit is contained in:
2026-08-21 00:17:46 +08:00
parent 8beb542c98
commit 35b3d1b0ed
5 changed files with 286 additions and 56 deletions
@@ -91,6 +91,9 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
public boolean saveDraft(ContractManage contractManage) { public boolean saveDraft(ContractManage contractManage) {
prepare(contractManage); prepare(contractManage);
validateDraft(contractManage); validateDraft(contractManage);
if (!Objects.equals(contractManage.getContractStage(), STAGE_DRAFT)) {
validateContractPartyCombinationUnique(contractManage);
}
if (Func.isNotEmpty(contractManage.getId())) { if (Func.isNotEmpty(contractManage.getId())) {
loadEditable(contractManage.getId()); loadEditable(contractManage.getId());
} }
@@ -110,6 +113,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
prepare(contractManage); prepare(contractManage);
validateDraft(contractManage); validateDraft(contractManage);
validateTemporary(contractManage); validateTemporary(contractManage);
validateContractPartyCombinationUnique(contractManage);
if (Func.isNotEmpty(contractManage.getId())) { if (Func.isNotEmpty(contractManage.getId())) {
loadEditable(contractManage.getId()); loadEditable(contractManage.getId());
} }
@@ -127,6 +131,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
if (!Objects.equals(contractManage.getContractStage(), STAGE_DRAFT) || !Objects.equals(contractManage.getApprovalStatus(), STATUS_DRAFT)) { if (!Objects.equals(contractManage.getContractStage(), STAGE_DRAFT) || !Objects.equals(contractManage.getApprovalStatus(), STATUS_DRAFT)) {
throw new ServiceException("仅草稿合同允许转临时合同"); throw new ServiceException("仅草稿合同允许转临时合同");
} }
validateContractPartyCombinationUnique(contractManage);
contractManage.setContractStage(STAGE_TEMPORARY); contractManage.setContractStage(STAGE_TEMPORARY);
contractManage.setApprovalStatus(STATUS_REVIEWING); contractManage.setApprovalStatus(STATUS_REVIEWING);
contractManage.setCurrentNode("临时合同审批"); contractManage.setCurrentNode("临时合同审批");
@@ -142,6 +147,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
throw new ServiceException("仅临时合同允许转正式合同"); throw new ServiceException("仅临时合同允许转正式合同");
} }
validateFormal(contractManage); validateFormal(contractManage);
validateContractPartyCombinationUnique(contractManage);
contractManage.setContractStage(STAGE_FORMAL); contractManage.setContractStage(STAGE_FORMAL);
contractManage.setApprovalStatus(STATUS_REVIEWING); contractManage.setApprovalStatus(STATUS_REVIEWING);
contractManage.setCurrentNode("正式合同审批"); contractManage.setCurrentNode("正式合同审批");
@@ -214,6 +220,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|| !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(source.getApprovalStatus())) { || !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(source.getApprovalStatus())) {
throw new ServiceException("当前合同状态不允许发起变更"); throw new ServiceException("当前合同状态不允许发起变更");
} }
ContractManage candidate = new ContractManage();
BeanUtil.copyProperties(source, candidate);
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
validateContractPartyCombinationUnique(candidate);
validatePaymentRatios(request.getPaymentRatioJson()); validatePaymentRatios(request.getPaymentRatioJson());
source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB()); source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB());
source.setStartDate(request.getStartDate()); source.setEndDate(request.getEndDate()); source.setContractFormat(request.getContractFormat()); source.setStartDate(request.getStartDate()); source.setEndDate(request.getEndDate()); source.setContractFormat(request.getContractFormat());
@@ -489,6 +499,23 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
} }
} }
private void validateContractPartyCombinationUnique(ContractManage contractManage) {
if (Func.isEmpty(contractManage.getContractCategory())
|| Func.isEmpty(contractManage.getPartyA())
|| Func.isEmpty(contractManage.getPartyB())) {
return;
}
Long count = count(Wrappers.<ContractManage>lambdaQuery()
.eq(ContractManage::getIsDeleted, 0)
.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("相同合同类别、甲方和乙方的合同已存在,不能重复提交");
}
}
private void validateTemporary(ContractManage contractManage) { private void validateTemporary(ContractManage contractManage) {
TransportBusinessSupport.validateRequired(contractManage.getSignType(), "请选择签约类型"); TransportBusinessSupport.validateRequired(contractManage.getSignType(), "请选择签约类型");
} }
@@ -485,7 +485,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
target.setSettlementType(settlementType); target.setProjectId(contract.getProjectId()); target.setProjectName(contract.getProjectName()); target.setSettlementType(settlementType); target.setProjectId(contract.getProjectId()); target.setProjectName(contract.getProjectName());
target.setDeptId(contract.getOrganizationId()); target.setDeptName(contract.getOrganizationName()); target.setContractId(contract.getId()); target.setDeptId(contract.getOrganizationId()); target.setDeptName(contract.getOrganizationName()); target.setContractId(contract.getId());
target.setContractNo(contract.getContractNo()); target.setContractName(contract.getContractName()); target.setContractNo(contract.getContractNo()); target.setContractName(contract.getContractName());
if ("receivable".equals(settlementType)) { target.setPayerName(source.getCustomerName()); target.setPayeeName(contract.getPartyA()); } if ("receivable".equals(settlementType)) { target.setPayerName(source.getCustomerName()); target.setPayeeName(contract.getPartyB()); }
else { target.setPayerName(contract.getPartyA()); target.setPayeeName(source.getCustomerName()); } else { target.setPayerName(contract.getPartyA()); target.setPayeeName(source.getCustomerName()); }
target.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); target.setLocalCurrency("RMB"); target.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); target.setLocalCurrency("RMB");
} }
@@ -22,8 +22,10 @@ import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.wrapper.LoadingManageWrapper; import org.springblade.transport.wrapper.LoadingManageWrapper;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -56,6 +58,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
@Resource @Resource
private WaybillMapper waybillMapper; private WaybillMapper waybillMapper;
@Resource
@Lazy
private IReceivablePayableDetailService receivablePayableDetailService;
@Override @Override
public IPage<LoadingManageVO> selectLoadingManagePage(IPage<LoadingManage> page, LoadingManageVO loadingManage) { public IPage<LoadingManageVO> selectLoadingManagePage(IPage<LoadingManage> page, LoadingManageVO loadingManage) {
IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage)); IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage));
@@ -268,7 +274,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
} }
loadingManage.setBusinessStatus(STATUS_COMPLETED); loadingManage.setBusinessStatus(STATUS_COMPLETED);
boolean result = updateById(loadingManage); boolean result = updateById(loadingManage);
List<Long> associatedWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>()); syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>());
if (result) {
receivablePayableDetailService.generateForCompletedWaybills(associatedWaybillIds);
}
return result; return result;
} }
@@ -1095,8 +1095,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setDeptId(contract.getOrganizationId()); settlement.setDeptId(contract.getOrganizationId());
settlement.setDeptName(contract.getOrganizationName()); settlement.setDeptName(contract.getOrganizationName());
if ("receivable".equals(settlement.getSettlementType())) { if ("receivable".equals(settlement.getSettlementType())) {
settlement.setPayerName(contract.getPartyB()); settlement.setPayerName(contract.getPartyA());
settlement.setPayeeName(contract.getPartyA()); settlement.setPayeeName(contract.getPartyB());
} else { } else {
settlement.setPayerName(contract.getPartyA()); settlement.setPayerName(contract.getPartyA());
settlement.setPayeeName(contract.getPartyB()); settlement.setPayeeName(contract.getPartyB());
@@ -72,6 +72,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -139,9 +140,13 @@ public class ReceivablePayableDetailServiceImpl
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) .eq(ReceivablePayableCargoFee::getDetailId, detail.getId())
.eq(ReceivablePayableCargoFee::getIsDeleted, 0) .eq(ReceivablePayableCargoFee::getIsDeleted, 0)
.orderByAsc(ReceivablePayableCargoFee::getCreateTime)); .orderByAsc(ReceivablePayableCargoFee::getCreateTime));
// 运输量单位取明细对应运单单位,兼容历史费用行误存计费单位的情况 // 费用行按匹配货物保存运输量单位;仅对历史空值回退到明细单位
if (Func.isNotEmpty(detail.getQuantityUnit())) { if (Func.isNotEmpty(detail.getQuantityUnit())) {
rows.forEach(row -> row.setQuantityUnit(detail.getQuantityUnit())); rows.forEach(row -> {
if (Func.isEmpty(row.getQuantityUnit())) {
row.setQuantityUnit(detail.getQuantityUnit());
}
});
} }
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
@@ -463,29 +468,22 @@ public class ReceivablePayableDetailServiceImpl
if (Func.isEmpty(waybillIds)) return; if (Func.isEmpty(waybillIds)) return;
for (Long waybillId : waybillIds) { for (Long waybillId : waybillIds) {
Waybill waybill = waybillService.getById(waybillId); Waybill waybill = waybillService.getById(waybillId);
if (waybill == null || Func.isEmpty(waybill.getContractId())) continue; if (waybill == null) continue;
ContractManage contract = contractManageService.getById(waybill.getContractId()); ContractManage receivableContract = Func.isEmpty(waybill.getContractId())
if (contract == null || !isSystemGeneration(contract)) continue; ? null : contractManageService.getById(waybill.getContractId());
try { try {
String planId = matchedPlanId(waybill, contract); if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory())
if (Func.isEmpty(planId)) continue; && isSystemGeneration(receivableContract)) {
// 自动生成只按合同计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 generateCompletedWaybillDetail(waybill, receivableContract, "receivable");
List<ReceivablePayableCargoFee> matchedFees = calculatedFees(waybill, contract, planId, true); }
if (matchedFees.isEmpty()) continue; if (Func.isNotEmpty(waybill.getCarrierName())) {
BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); ContractManage payableContract = findCarrierContract(waybill);
for (String settlementType : List.of("payable", "receivable")) { if (payableContract != null && isSystemGeneration(payableContract)) {
if (existsByWaybill(waybill.getId(), settlementType)) continue; generateCompletedWaybillDetail(waybill, payableContract, "payable");
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());
cargoFeeMapper.insert(copy);
} }
} }
} catch (Exception exception) { } catch (Exception exception) {
log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, contractId:{}, failureReason:{}", log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, receivableContractId:{}, failureReason:{}",
waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception); waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception);
if (exception instanceof RuntimeException runtimeException) { if (exception instanceof RuntimeException runtimeException) {
throw runtimeException; throw runtimeException;
@@ -495,6 +493,46 @@ public class ReceivablePayableDetailServiceImpl
} }
} }
private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType) {
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());
cargoFeeMapper.insert(copy);
}
}
private ContractManage findCarrierContract(Waybill waybill) {
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId())
|| Func.isEmpty(waybill.getCarrierName())) {
return null;
}
// 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。
ContractManage customerContract = contractManageService.getById(waybill.getContractId());
if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) {
return null;
}
return contractManageService.getOne(Wrappers.<ContractManage>lambdaQuery()
.eq(ContractManage::getIsDeleted, 0)
.eq(ContractManage::getProjectId, waybill.getProjectId())
.eq(ContractManage::getPartyA, customerContract.getPartyA())
.eq(ContractManage::getPartyB, waybill.getCarrierName())
.eq(ContractManage::getContractCategory, "承运商合同")
.in(ContractManage::getApprovalStatus, "approved", "change_approved")
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
.or().ne(ContractManage::getContractStage, "terminated"))
.orderByDesc(ContractManage::getCreateTime), false);
}
@Override @Override
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
rollbackFor = Exception.class) rollbackFor = Exception.class)
@@ -549,8 +587,8 @@ public class ReceivablePayableDetailServiceImpl
private String matchedPlanId(Waybill waybill, ContractManage contract) { private String matchedPlanId(Waybill waybill, ContractManage contract) {
List<Map<String, Object>> plans = parseList(contract.getBillingPlanJson()); List<Map<String, Object>> plans = parseList(contract.getBillingPlanJson());
if (plans.isEmpty()) return null; if (plans.isEmpty()) return null;
Map<String, Object> plan = plans.stream().filter(item -> Boolean.TRUE.equals(item.get("defaultPlan"))).findFirst() Map<String, Object> plan = plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null);
.orElse(plans.get(plans.size() - 1)); if (plan == null) return null;
if (!(plan.get("rules") instanceof List<?> rules)) return null; if (!(plan.get("rules") instanceof List<?> rules)) return null;
boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill)); boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill));
if (!matched) return null; if (!matched) return null;
@@ -559,10 +597,21 @@ public class ReceivablePayableDetailServiceImpl
private boolean matchesRule(Map<?, ?> raw, Waybill waybill) { private boolean matchesRule(Map<?, ?> raw, Waybill waybill) {
Object conditionValue = raw.get("matchCondition"); Object conditionValue = raw.get("matchCondition");
if (!(conditionValue instanceof Map<?, ?> condition)) return true; if (!(conditionValue instanceof Map<?, ?> condition) || !hasConfiguredMatchCondition(condition)) return true;
return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress()) return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress())
&& matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress()) && matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress())
&& matchesCondition(condition.get("cargoType"), waybill.getCargoType()); && matchesCondition(condition.get("transportMode"), waybill.getTransportType())
&& matchesCargoType(condition, waybill);
}
private boolean hasConfiguredMatchCondition(Map<?, ?> condition) {
return !isBlank(condition.get("origin"))
|| !isBlank(condition.get("originCode"))
|| !isBlank(condition.get("destination"))
|| !isBlank(condition.get("destinationCode"))
|| !isBlank(condition.get("transportMode"))
|| !isBlank(condition.get("cargoType"))
|| !isBlank(condition.get("cargoTypeCode"));
} }
private boolean matchesLocation(Map<?, ?> condition, String location, Long addressId, String addressName, private boolean matchesLocation(Map<?, ?> condition, String location, Long addressId, String addressName,
@@ -574,10 +623,19 @@ public class ReceivablePayableDetailServiceImpl
if (!isBlank(expectedCode) && !isBlank(actualCode)) { if (!isBlank(expectedCode) && !isBlank(actualCode)) {
return matchesCondition(expectedCode, actualCode); return matchesCondition(expectedCode, actualCode);
} }
if (!isBlank(expectedCode) && isBlank(expectedName)) return false;
return matchesCondition(expectedName, addressName) return matchesCondition(expectedName, addressName)
|| matchesCondition(expectedName, detailAddress); || matchesCondition(expectedName, detailAddress);
} }
private boolean matchesCargoType(Map<?, ?> condition, Waybill waybill) {
Object expectedName = condition.get("cargoType");
Object expectedCode = condition.get("cargoTypeCode");
if (!isBlank(expectedName)) return matchesCondition(expectedName, waybill.getCargoType());
if (!isBlank(expectedCode)) return matchesCondition(expectedCode, waybill.getCargoType());
return true;
}
private String resolveRegionCode(Long addressId) { private String resolveRegionCode(Long addressId) {
if (addressId == null) return ""; if (addressId == null) return "";
try { try {
@@ -590,6 +648,9 @@ public class ReceivablePayableDetailServiceImpl
} }
private boolean isBlank(Object value) { private boolean isBlank(Object value) {
if (value instanceof Collection<?> collection) {
return collection.isEmpty() || collection.stream().allMatch(this::isBlank);
}
return value == null || String.valueOf(value).trim().isEmpty(); return value == null || String.valueOf(value).trim().isEmpty();
} }
@@ -641,11 +702,30 @@ public class ReceivablePayableDetailServiceImpl
} }
private LambdaQueryWrapper<Waybill> buildWaybillQuery(ReceivablePayableGenerateRequest request) { private LambdaQueryWrapper<Waybill> buildWaybillQuery(ReceivablePayableGenerateRequest request) {
String targetSettlementType = settlementType(request.getSettlementType());
ContractManage contract = contractManageService.getById(request.getContractId());
validateContractSettlementType(contract, targetSettlementType);
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery() LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0) .eq(Waybill::getIsDeleted, 0)
.eq(Waybill::getContractId, request.getContractId())
.eq(Waybill::getBusinessStatus, "completed"); .eq(Waybill::getBusinessStatus, "completed");
String targetSettlementType = settlementType(request.getSettlementType()); if ("receivable".equals(targetSettlementType)) {
wrapper.eq(Waybill::getContractId, contract.getId());
} else {
List<Long> customerContractIds = contractManageService.list(Wrappers.<ContractManage>lambdaQuery()
.select(ContractManage::getId)
.eq(ContractManage::getIsDeleted, 0)
.eq(ContractManage::getProjectId, contract.getProjectId())
.eq(ContractManage::getContractCategory, "客户合同")
.eq(ContractManage::getPartyA, contract.getPartyA()))
.stream().map(ContractManage::getId).toList();
if (customerContractIds.isEmpty()) {
wrapper.eq(Waybill::getId, -1L);
} else {
wrapper.in(Waybill::getContractId, customerContractIds)
.eq(Waybill::getProjectId, contract.getProjectId())
.eq(Waybill::getCarrierName, contract.getPartyB());
}
}
wrapper.notInSql(Waybill::getId, wrapper.notInSql(Waybill::getId,
"select waybill_id from blade_receivable_payable_detail" "select waybill_id from blade_receivable_payable_detail"
+ " where is_deleted = 0 and waybill_id is not null and settlement_type = '" + " where is_deleted = 0 and waybill_id is not null and settlement_type = '"
@@ -687,9 +767,9 @@ public class ReceivablePayableDetailServiceImpl
detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId()); detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId());
detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName()); detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName());
detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate());
detail.setCustomerName("payable".equals(settlementType) detail.setCustomerName(contract == null
? (contract == null ? waybill.getCarrierName() : contract.getPartyA()) ? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName())
: (contract == null ? waybill.getCustomerName() : contract.getPartyB())); : ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA()));
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId()); detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
detail.setContractNo(contract == null ? null : contract.getContractNo()); detail.setContractNo(contract == null ? null : contract.getContractNo());
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName()); detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
@@ -710,8 +790,7 @@ public class ReceivablePayableDetailServiceImpl
detail.setOtherFeeAmount(other); detail.setOtherFeeAmount(other);
detail.setTotalAmount(total); detail.setTotalAmount(total);
detail.setSettlementStatus("pending"); detail.setSettlementStatus("pending");
detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap<String, BigDecimal>::new, detail.setFeeItemsJson(aggregateFeeItemsJson(fees));
(map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll)));
return detail; return detail;
} }
@@ -854,14 +933,20 @@ public class ReceivablePayableDetailServiceImpl
if (matchOnly && !matchesRule(raw, waybill)) continue; if (matchOnly && !matchesRule(raw, waybill)) continue;
BigDecimal amount = calculateRule(rule, waybill); BigDecimal amount = calculateRule(rule, waybill);
if (amount == null) continue; if (amount == null) continue;
String feeItem = stringValue(rule, "feeItem", "费用");
Map<String, String> feeGoods = summarizeFeeGoods(rule, waybill);
ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee();
fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++));
fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType()); fee.setCargoName(feeGoods.getOrDefault("cargoName", waybill.getCargoName()));
fee.setCargoType(feeGoods.getOrDefault("cargoType", waybill.getCargoType()));
fee.setSpecification(feeGoods.getOrDefault("specification", waybill.getSpecification()));
fee.setModel(feeGoods.getOrDefault("model", waybill.getModel()));
fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", ""));
fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(waybill.getQuantityUnit()); fee.setTransportQuantity(measure(rule, waybill));
fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", waybill.getQuantityUnit()));
fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice")));
fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO);
fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount))); fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount)));
fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark()));
result.add(fee); result.add(fee);
} }
@@ -894,16 +979,22 @@ public class ReceivablePayableDetailServiceImpl
private BigDecimal calculateRule(Map<String, Object> rule, Waybill waybill) { private BigDecimal calculateRule(Map<String, Object> rule, Waybill waybill) {
String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", ""); String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", "");
BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice")); BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice"));
if ("按重量".equals(element) || "按吨·公里".equals(element)) {
BigDecimal minimum = decimal(rule.get("minimumBillingWeight"));
if (minimum.signum() > 0 && "按重量".equals(element)) base = base.max(minimum);
if (minimum.signum() > 0 && "按吨·公里".equals(element)) base = base.divide(money(waybill.getQuantity()).max(BigDecimal.ONE), 6, RoundingMode.HALF_UP).multiply(minimum).multiply(money(waybill.getMileage()));
}
if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP);
List<Map<String, Object>> ranges = ranges(rule); List<Map<String, Object>> ranges = ranges(rule);
boolean intervalUnitPrice = "区间单价".equals(type);
boolean intervalFlatPrice = type.contains("区间") && type.contains("一口价");
if (!intervalUnitPrice && !intervalFlatPrice) base = applyMinimum(base, rule, waybill);
if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP);
if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP);
if (type.contains("区间") && type.contains("一口价")) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); if (intervalFlatPrice) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
if ("区间单价".equals(type)) { BigDecimal rangeBase = base; return range(ranges, rangeBase).map(r -> decimal(r.get("unitPrice")).multiply(rangeBase)).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); } if (intervalUnitPrice) {
BigDecimal rangeBase = base;
return range(ranges, rangeBase).map(r -> {
BigDecimal minimum = decimal(r.get("minimumBillingWeight"));
if (minimum.signum() <= 0) minimum = decimal(rule.get("minimumBillingWeight"));
BigDecimal effectiveBase = applyMinimum(rangeBase, element, minimum, waybill);
return decimal(r.get("unitPrice")).multiply(effectiveBase);
}).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
}
if ("阶梯单价".equals(type)) { if ("阶梯单价".equals(type)) {
BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO;
for (Map<String, Object> r : ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList()) { for (Map<String, Object> r : ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList()) {
@@ -916,6 +1007,20 @@ public class ReceivablePayableDetailServiceImpl
return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); return base.multiply(unit).setScale(2, RoundingMode.HALF_UP);
} }
private BigDecimal applyMinimum(BigDecimal base, Map<String, Object> rule, Waybill waybill) {
return applyMinimum(base, stringValue(rule, "billingElement", ""), decimal(rule.get("minimumBillingWeight")), waybill);
}
private BigDecimal applyMinimum(BigDecimal base, String element, BigDecimal minimum, Waybill waybill) {
if (minimum.signum() <= 0) return base;
if ("按重量".equals(element)) return base.max(minimum);
if ("按吨·公里".equals(element)) {
BigDecimal effectiveWeight = money(waybill.getQuantity()).max(minimum);
return effectiveWeight.multiply(money(waybill.getMileage()));
}
return base;
}
private boolean isFreight(ReceivablePayableCargoFee fee) { private boolean isFreight(ReceivablePayableCargoFee fee) {
return fee.getFreightAmount() != null && fee.getFreightAmount().compareTo(BigDecimal.ZERO) > 0; return fee.getFreightAmount() != null && fee.getFreightAmount().compareTo(BigDecimal.ZERO) > 0;
} }
@@ -926,21 +1031,91 @@ public class ReceivablePayableDetailServiceImpl
} }
private BigDecimal measure(Map<String, Object> rule, Waybill waybill) { private BigDecimal measure(Map<String, Object> rule, Waybill waybill) {
return switch (stringValue(rule, "billingElement", "按重量")) { String element = stringValue(rule, "billingElement", "按重量");
case "按体积" -> volume(waybill); List<Map<String, Object>> goods = feeGoods(rule, waybill);
return switch (element) {
case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add);
case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE; case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE;
case "按里程" -> money(waybill.getMileage()); case "按里程" -> money(waybill.getMileage());
case "按吨·公里" -> money(waybill.getQuantity()).multiply(money(waybill.getMileage())); case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage()));
case "按数量" -> money(waybill.getQuantity()); case "按数量" -> goodsQuantity(goods);
default -> money(waybill.getQuantity()); default -> goodsQuantity(goods);
}; };
} }
private List<Map<String, Object>> feeGoods(Map<String, Object> rule, Waybill waybill) {
List<Map<String, Object>> goods = parseList(waybill.getGoodsJson());
if (goods.isEmpty()) return List.of();
String element = stringValue(rule, "billingElement", "按重量");
if ("按体积".equals(element)) {
return goods.stream().filter(this::isVolumeGoods).toList();
}
if ("按重量".equals(element) || "按吨·公里".equals(element)) {
List<Map<String, Object>> weighted = goods.stream().filter(item -> !isVolumeGoods(item)).toList();
return weighted.isEmpty() ? goods : weighted;
}
return goods;
}
private Map<String, String> summarizeFeeGoods(Map<String, Object> rule, Waybill waybill) {
List<Map<String, Object>> goods = feeGoods(rule, waybill);
if (goods.isEmpty()) return Map.of();
Map<String, String> result = new LinkedHashMap<>();
putCommonGoodsValue(result, "cargoName", goods);
putCommonGoodsValue(result, "cargoType", goods);
putCommonGoodsValue(result, "specification", goods);
putCommonGoodsValue(result, "model", goods);
putCommonGoodsValue(result, "quantityUnit", goods);
return result;
}
private void putCommonGoodsValue(Map<String, String> result, String key, List<Map<String, Object>> goods) {
List<String> values = goods.stream().map(item -> stringValue(item, key, "")).filter(Func::isNotEmpty).distinct().toList();
if (values.size() == 1) result.put(key, values.get(0));
else if (!values.isEmpty()) result.put(key, String.join(",", values));
}
private BigDecimal goodsQuantity(List<Map<String, Object>> goods) {
return goods.stream().map(item -> decimal(item.get("quantity"))).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private BigDecimal goodsVolume(Map<String, Object> goods) {
BigDecimal value = decimal(goods.get("volume"));
if (value.signum() == 0) value = decimal(goods.get("cargoVolume"));
if (value.signum() == 0 && isVolumeGoods(goods)) value = decimal(goods.get("quantity"));
return value;
}
private boolean isVolumeGoods(Map<String, Object> goods) {
if (decimal(goods.get("volume")).signum() > 0 || decimal(goods.get("cargoVolume")).signum() > 0) return true;
return isVolumeUnit(stringValue(goods, "quantityUnit", ""));
}
private BigDecimal volume(Waybill waybill) { private BigDecimal volume(Waybill waybill) {
BigDecimal total = BigDecimal.ZERO;
for (Map<String, Object> goods : parseList(waybill.getGoodsJson())) {
BigDecimal value = decimal(goods.get("volume"));
if (value.signum() == 0) value = decimal(goods.get("cargoVolume"));
if (value.signum() == 0 && isVolumeUnit(stringValue(goods, "quantityUnit"))) {
value = decimal(goods.get("quantity"));
}
total = total.add(value);
}
if (total.signum() > 0) return total;
Map<String, Object> goods = parseMap(waybill.getGoodsJson()); Map<String, Object> goods = parseMap(waybill.getGoodsJson());
BigDecimal value = decimal(goods.get("volume")); BigDecimal value = decimal(goods.get("volume"));
if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); if (value.signum() == 0) value = decimal(goods.get("cargoVolume"));
return value; if (value.signum() == 0 && isVolumeUnit(stringValue(goods, "quantityUnit"))) {
value = decimal(goods.get("quantity"));
}
if (value.signum() > 0) return value;
return isVolumeUnit(waybill.getQuantityUnit()) ? money(waybill.getQuantity()) : BigDecimal.ZERO;
}
private boolean isVolumeUnit(String unit) {
String normalized = String.valueOf(unit == null ? "" : unit).trim().toLowerCase();
return normalized.equals("") || normalized.contains("立方")
|| normalized.equals("m3") || normalized.equals("") || normalized.equals("m^3");
} }
private List<Map<String, Object>> ranges(Map<String, Object> rule) { private List<Map<String, Object>> ranges(Map<String, Object> rule) {
@@ -1062,6 +1237,13 @@ public class ReceivablePayableDetailServiceImpl
updateById(detail); updateById(detail);
} }
private String aggregateFeeItemsJson(List<ReceivablePayableCargoFee> fees) {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) ->
feeItems.merge(name, decimal(value), BigDecimal::add)));
return JsonUtil.toJson(feeItems);
}
private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) {
if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) { if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) {
rebuildMasterOrderDetailFee(detail, billingPlanId); rebuildMasterOrderDetailFee(detail, billingPlanId);
@@ -1081,7 +1263,7 @@ public class ReceivablePayableDetailServiceImpl
detail.setOtherFeeAmount(total.subtract(freight)); detail.setOtherFeeAmount(total.subtract(freight));
detail.setTotalAmount(total); detail.setTotalAmount(total);
detail.setUnitPrice(resolveContractUnitPrice(fees)); detail.setUnitPrice(resolveContractUnitPrice(fees));
detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap<String, BigDecimal>::new, (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); detail.setFeeItemsJson(aggregateFeeItemsJson(fees));
updateById(detail); updateById(detail);
} }
@@ -1112,8 +1294,7 @@ public class ReceivablePayableDetailServiceImpl
detail.setOtherFeeAmount(total.subtract(freight)); detail.setOtherFeeAmount(total.subtract(freight));
detail.setTotalAmount(total); detail.setTotalAmount(total);
detail.setUnitPrice(resolveContractUnitPrice(fees)); detail.setUnitPrice(resolveContractUnitPrice(fees));
detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap<String, BigDecimal>::new, detail.setFeeItemsJson(aggregateFeeItemsJson(fees));
(map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll)));
updateById(detail); updateById(detail);
} }
@@ -1198,12 +1379,24 @@ public class ReceivablePayableDetailServiceImpl
if (Func.isEmpty(request.getBillingPlanId())) { if (Func.isEmpty(request.getBillingPlanId())) {
throw new ServiceException("请选择计费方案"); throw new ServiceException("请选择计费方案");
} }
settlementType(request.getSettlementType()); String targetSettlementType = settlementType(request.getSettlementType());
validateContractSettlementType(contractManageService.getById(request.getContractId()), targetSettlementType);
if (requireWaybill && Func.isEmpty(request.getWaybillIds())) { if (requireWaybill && Func.isEmpty(request.getWaybillIds())) {
throw new ServiceException("请选择需要生成费用的运单"); throw new ServiceException("请选择需要生成费用的运单");
} }
} }
private void validateContractSettlementType(ContractManage contract, String settlementType) {
if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) {
throw new ServiceException("合同不存在或已删除");
}
String expectedCategory = "payable".equals(settlementType) ? "承运商合同" : "客户合同";
if (!expectedCategory.equals(contract.getContractCategory())) {
throw new ServiceException(("payable".equals(settlementType) ? "应付" : "应收")
+ "费用只能使用" + expectedCategory);
}
}
private Map<String, Object> waybillMap(Waybill waybill) { private Map<String, Object> waybillMap(Waybill waybill) {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("id", waybill.getId()); map.put("id", waybill.getId());