调整项目-合同-运单-结算逻辑链
This commit is contained in:
+27
@@ -91,6 +91,9 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
public boolean saveDraft(ContractManage contractManage) {
|
||||
prepare(contractManage);
|
||||
validateDraft(contractManage);
|
||||
if (!Objects.equals(contractManage.getContractStage(), STAGE_DRAFT)) {
|
||||
validateContractPartyCombinationUnique(contractManage);
|
||||
}
|
||||
if (Func.isNotEmpty(contractManage.getId())) {
|
||||
loadEditable(contractManage.getId());
|
||||
}
|
||||
@@ -110,6 +113,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
prepare(contractManage);
|
||||
validateDraft(contractManage);
|
||||
validateTemporary(contractManage);
|
||||
validateContractPartyCombinationUnique(contractManage);
|
||||
if (Func.isNotEmpty(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)) {
|
||||
throw new ServiceException("仅草稿合同允许转临时合同");
|
||||
}
|
||||
validateContractPartyCombinationUnique(contractManage);
|
||||
contractManage.setContractStage(STAGE_TEMPORARY);
|
||||
contractManage.setApprovalStatus(STATUS_REVIEWING);
|
||||
contractManage.setCurrentNode("临时合同审批");
|
||||
@@ -142,6 +147,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
throw new ServiceException("仅临时合同允许转正式合同");
|
||||
}
|
||||
validateFormal(contractManage);
|
||||
validateContractPartyCombinationUnique(contractManage);
|
||||
contractManage.setContractStage(STAGE_FORMAL);
|
||||
contractManage.setApprovalStatus(STATUS_REVIEWING);
|
||||
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())) {
|
||||
throw new ServiceException("当前合同状态不允许发起变更");
|
||||
}
|
||||
ContractManage candidate = new ContractManage();
|
||||
BeanUtil.copyProperties(source, candidate);
|
||||
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
|
||||
validateContractPartyCombinationUnique(candidate);
|
||||
validatePaymentRatios(request.getPaymentRatioJson());
|
||||
source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB());
|
||||
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) {
|
||||
TransportBusinessSupport.validateRequired(contractManage.getSignType(), "请选择签约类型");
|
||||
}
|
||||
|
||||
+1
-1
@@ -485,7 +485,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
target.setSettlementType(settlementType); target.setProjectId(contract.getProjectId()); target.setProjectName(contract.getProjectName());
|
||||
target.setDeptId(contract.getOrganizationId()); target.setDeptName(contract.getOrganizationName()); target.setContractId(contract.getId());
|
||||
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()); }
|
||||
target.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); target.setLocalCurrency("RMB");
|
||||
}
|
||||
|
||||
+10
@@ -22,8 +22,10 @@ import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.wrapper.LoadingManageWrapper;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -56,6 +58,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
@Resource
|
||||
private WaybillMapper waybillMapper;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private IReceivablePayableDetailService receivablePayableDetailService;
|
||||
|
||||
@Override
|
||||
public IPage<LoadingManageVO> selectLoadingManagePage(IPage<LoadingManage> page, LoadingManageVO loadingManage) {
|
||||
IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage));
|
||||
@@ -268,7 +274,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
loadingManage.setBusinessStatus(STATUS_COMPLETED);
|
||||
boolean result = updateById(loadingManage);
|
||||
List<Long> associatedWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
|
||||
syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>());
|
||||
if (result) {
|
||||
receivablePayableDetailService.generateForCompletedWaybills(associatedWaybillIds);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1095,8 +1095,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
settlement.setDeptId(contract.getOrganizationId());
|
||||
settlement.setDeptName(contract.getOrganizationName());
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
settlement.setPayerName(contract.getPartyB());
|
||||
settlement.setPayeeName(contract.getPartyA());
|
||||
settlement.setPayerName(contract.getPartyA());
|
||||
settlement.setPayeeName(contract.getPartyB());
|
||||
} else {
|
||||
settlement.setPayerName(contract.getPartyA());
|
||||
settlement.setPayeeName(contract.getPartyB());
|
||||
|
||||
+246
-53
@@ -72,6 +72,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -139,9 +140,13 @@ public class ReceivablePayableDetailServiceImpl
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId())
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getCreateTime));
|
||||
// 运输量的单位取明细对应运单单位,兼容历史费用行误存计费单位的情况。
|
||||
// 费用行按匹配货物保存运输量单位;仅对历史空值回退到明细单位。
|
||||
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);
|
||||
LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
|
||||
@@ -463,29 +468,22 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (Func.isEmpty(waybillIds)) return;
|
||||
for (Long waybillId : waybillIds) {
|
||||
Waybill waybill = waybillService.getById(waybillId);
|
||||
if (waybill == null || Func.isEmpty(waybill.getContractId())) continue;
|
||||
ContractManage contract = contractManageService.getById(waybill.getContractId());
|
||||
if (contract == null || !isSystemGeneration(contract)) continue;
|
||||
if (waybill == null) continue;
|
||||
ContractManage receivableContract = Func.isEmpty(waybill.getContractId())
|
||||
? null : contractManageService.getById(waybill.getContractId());
|
||||
try {
|
||||
String planId = matchedPlanId(waybill, contract);
|
||||
if (Func.isEmpty(planId)) continue;
|
||||
// 自动生成只按合同计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。
|
||||
List<ReceivablePayableCargoFee> matchedFees = calculatedFees(waybill, contract, planId, true);
|
||||
if (matchedFees.isEmpty()) continue;
|
||||
BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees);
|
||||
for (String settlementType : List.of("payable", "receivable")) {
|
||||
if (existsByWaybill(waybill.getId(), settlementType)) continue;
|
||||
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);
|
||||
if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory())
|
||||
&& isSystemGeneration(receivableContract)) {
|
||||
generateCompletedWaybillDetail(waybill, receivableContract, "receivable");
|
||||
}
|
||||
if (Func.isNotEmpty(waybill.getCarrierName())) {
|
||||
ContractManage payableContract = findCarrierContract(waybill);
|
||||
if (payableContract != null && isSystemGeneration(payableContract)) {
|
||||
generateCompletedWaybillDetail(waybill, payableContract, "payable");
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, contractId:{}, failureReason:{}",
|
||||
log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, receivableContractId:{}, failureReason:{}",
|
||||
waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception);
|
||||
if (exception instanceof RuntimeException 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
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
@@ -549,8 +587,8 @@ public class ReceivablePayableDetailServiceImpl
|
||||
private String matchedPlanId(Waybill waybill, ContractManage contract) {
|
||||
List<Map<String, Object>> plans = parseList(contract.getBillingPlanJson());
|
||||
if (plans.isEmpty()) return null;
|
||||
Map<String, Object> plan = plans.stream().filter(item -> Boolean.TRUE.equals(item.get("defaultPlan"))).findFirst()
|
||||
.orElse(plans.get(plans.size() - 1));
|
||||
Map<String, Object> plan = plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null);
|
||||
if (plan == null) return null;
|
||||
if (!(plan.get("rules") instanceof List<?> rules)) return null;
|
||||
boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill));
|
||||
if (!matched) return null;
|
||||
@@ -559,10 +597,21 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
private boolean matchesRule(Map<?, ?> raw, Waybill waybill) {
|
||||
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())
|
||||
&& 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,
|
||||
@@ -574,10 +623,19 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (!isBlank(expectedCode) && !isBlank(actualCode)) {
|
||||
return matchesCondition(expectedCode, actualCode);
|
||||
}
|
||||
if (!isBlank(expectedCode) && isBlank(expectedName)) return false;
|
||||
return matchesCondition(expectedName, addressName)
|
||||
|| 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) {
|
||||
if (addressId == null) return "";
|
||||
try {
|
||||
@@ -590,6 +648,9 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -641,11 +702,30 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
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()
|
||||
.eq(Waybill::getIsDeleted, 0)
|
||||
.eq(Waybill::getContractId, request.getContractId())
|
||||
.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,
|
||||
"select waybill_id from blade_receivable_payable_detail"
|
||||
+ " 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.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName());
|
||||
detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate());
|
||||
detail.setCustomerName("payable".equals(settlementType)
|
||||
? (contract == null ? waybill.getCarrierName() : contract.getPartyA())
|
||||
: (contract == null ? waybill.getCustomerName() : contract.getPartyB()));
|
||||
detail.setCustomerName(contract == null
|
||||
? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName())
|
||||
: ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA()));
|
||||
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
|
||||
detail.setContractNo(contract == null ? null : contract.getContractNo());
|
||||
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
|
||||
@@ -710,8 +790,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setOtherFeeAmount(other);
|
||||
detail.setTotalAmount(total);
|
||||
detail.setSettlementStatus("pending");
|
||||
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));
|
||||
return detail;
|
||||
}
|
||||
|
||||
@@ -854,14 +933,20 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (matchOnly && !matchesRule(raw, waybill)) continue;
|
||||
BigDecimal amount = calculateRule(rule, waybill);
|
||||
if (amount == null) continue;
|
||||
String feeItem = stringValue(rule, "feeItem", "费用");
|
||||
Map<String, String> feeGoods = summarizeFeeGoods(rule, waybill);
|
||||
ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee();
|
||||
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.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.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()));
|
||||
result.add(fee);
|
||||
}
|
||||
@@ -894,16 +979,22 @@ public class ReceivablePayableDetailServiceImpl
|
||||
private BigDecimal calculateRule(Map<String, Object> rule, Waybill waybill) {
|
||||
String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", "");
|
||||
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);
|
||||
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 (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 ("区间单价".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 (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 (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)) {
|
||||
BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO;
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return switch (stringValue(rule, "billingElement", "按重量")) {
|
||||
case "按体积" -> volume(waybill);
|
||||
String element = stringValue(rule, "billingElement", "按重量");
|
||||
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 "按里程" -> money(waybill.getMileage());
|
||||
case "按吨·公里" -> money(waybill.getQuantity()).multiply(money(waybill.getMileage()));
|
||||
case "按数量" -> money(waybill.getQuantity());
|
||||
default -> money(waybill.getQuantity());
|
||||
case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage()));
|
||||
case "按数量" -> goodsQuantity(goods);
|
||||
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) {
|
||||
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());
|
||||
BigDecimal value = decimal(goods.get("volume"));
|
||||
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("m³") || normalized.equals("m^3");
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> ranges(Map<String, Object> rule) {
|
||||
@@ -1062,6 +1237,13 @@ public class ReceivablePayableDetailServiceImpl
|
||||
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) {
|
||||
if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) {
|
||||
rebuildMasterOrderDetailFee(detail, billingPlanId);
|
||||
@@ -1081,7 +1263,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setOtherFeeAmount(total.subtract(freight));
|
||||
detail.setTotalAmount(total);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1112,8 +1294,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setOtherFeeAmount(total.subtract(freight));
|
||||
detail.setTotalAmount(total);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1198,12 +1379,24 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (Func.isEmpty(request.getBillingPlanId())) {
|
||||
throw new ServiceException("请选择计费方案");
|
||||
}
|
||||
settlementType(request.getSettlementType());
|
||||
String targetSettlementType = settlementType(request.getSettlementType());
|
||||
validateContractSettlementType(contractManageService.getById(request.getContractId()), targetSettlementType);
|
||||
if (requireWaybill && Func.isEmpty(request.getWaybillIds())) {
|
||||
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) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", waybill.getId());
|
||||
|
||||
Reference in New Issue
Block a user