🐛 修复项目部门可见性、费用项翻倍与配载合同匹配
业务部门变更后本部门可查项目;清洗 freightJson 误写入的费用项并修正预结算合计;配载承运商按客户合同乙方匹配。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -56,7 +56,7 @@ public class LoadingManageController extends BladeController {
|
||||
|
||||
@GetMapping("/carrier-contracts")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同甲方匹配后对乙方取交集")
|
||||
@Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同乙方=承运商合同甲方匹配后对乙方取交集")
|
||||
public R<List<LoadingCarrierContractVO>> carrierContracts(
|
||||
@RequestParam List<Long> projectIds,
|
||||
@RequestParam(required = false) List<Long> waybillIds) {
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
|
||||
/**
|
||||
* 查询项目下已审核生效的承运商合同。
|
||||
* <p>
|
||||
* 传入 waybillIds 时,按各子运单客户合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。
|
||||
* 传入 waybillIds 时,按各子运单客户合同乙方=承运商合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。
|
||||
*/
|
||||
List<LoadingCarrierContractVO> carrierContracts(List<Long> projectIds, List<Long> waybillIds);
|
||||
|
||||
|
||||
+24
-1
@@ -97,6 +97,7 @@ import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeArBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
@@ -1878,7 +1879,29 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, Map.class);
|
||||
if (parsed instanceof Map<?, ?> map) {
|
||||
map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value)));
|
||||
BigDecimal otherFeeAmount = BigDecimal.ZERO;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String key = String.valueOf(entry.getKey());
|
||||
Object value = entry.getValue();
|
||||
if (Func.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
|
||||
otherFeeAmount = otherFeeAmount.add(decimal(value));
|
||||
continue;
|
||||
}
|
||||
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof java.util.Collection<?>) {
|
||||
continue;
|
||||
}
|
||||
result.put(key, decimal(value));
|
||||
}
|
||||
if (otherFeeAmount.signum() > 0 && result.keySet().stream()
|
||||
.noneMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")))) {
|
||||
result.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// 兼容历史费用JSON,不影响正式结算保存。
|
||||
|
||||
+11
-10
@@ -907,7 +907,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一配载单子运单除同项目外,还必须存在共同的承运商合同乙方(按客户合同甲方匹配后取交集)。
|
||||
* 同一配载单子运单除同项目外,还必须存在共同的承运商合同乙方(按客户合同乙方=承运商合同甲方匹配后取交集)。
|
||||
*/
|
||||
private void validateCompatibleCarrierPartyB(List<Waybill> waybills) {
|
||||
if (Func.isEmpty(waybills) || waybills.size() < 2) {
|
||||
@@ -1008,14 +1008,14 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
|
||||
/**
|
||||
* 各子运单按「客户合同甲方 = 承运商合同甲方」匹配可用承运商合同,再对乙方名称取交集。
|
||||
* 例:子单1可选 A/B/C,子单2可选 B,则交集为 B。
|
||||
* 各子运单按「客户合同乙方 = 承运商合同甲方」匹配可用承运商合同,再对乙方名称取交集。
|
||||
* 例:客户合同 盛达(甲)-捷联(乙),承运商合同 捷联(甲)-驰远(乙);子单1可选 A/B/C,子单2可选 B,则交集为 B。
|
||||
*/
|
||||
private Set<String> intersectCarrierPartyBs(List<Waybill> waybills, List<ContractManage> projectContracts) {
|
||||
Set<String> intersection = null;
|
||||
Map<Long, String> customerPartyAMap = loadCustomerContractPartyAMap(waybills);
|
||||
Map<Long, String> customerPartyBMap = loadCustomerContractPartyBMap(waybills);
|
||||
for (Waybill waybill : waybills) {
|
||||
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyAMap);
|
||||
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyBMap);
|
||||
if (intersection == null) {
|
||||
intersection = new LinkedHashSet<>(available);
|
||||
} else {
|
||||
@@ -1029,21 +1029,22 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
|
||||
private Set<String> availableCarrierPartyBs(Waybill waybill, List<ContractManage> projectContracts,
|
||||
Map<Long, String> customerPartyAMap) {
|
||||
Map<Long, String> customerPartyBMap) {
|
||||
if (waybill == null || Func.isEmpty(projectContracts)) {
|
||||
return Set.of();
|
||||
}
|
||||
String partyA = customerPartyAMap.get(waybill.getContractId());
|
||||
// 客户合同乙方即我方,应等于承运商合同甲方
|
||||
String partyB = customerPartyBMap.get(waybill.getContractId());
|
||||
return projectContracts.stream()
|
||||
.filter(contract -> Objects.equals(contract.getProjectId(), waybill.getProjectId()))
|
||||
.filter(contract -> Func.isEmpty(partyA) || Objects.equals(normalizePartyName(contract.getPartyA()), partyA))
|
||||
.filter(contract -> Func.isEmpty(partyB) || Objects.equals(normalizePartyName(contract.getPartyA()), partyB))
|
||||
.map(ContractManage::getPartyB)
|
||||
.map(this::normalizePartyName)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private Map<Long, String> loadCustomerContractPartyAMap(List<Waybill> waybills) {
|
||||
private Map<Long, String> loadCustomerContractPartyBMap(List<Waybill> waybills) {
|
||||
List<Long> contractIds = waybills.stream()
|
||||
.map(Waybill::getContractId)
|
||||
.filter(Objects::nonNull)
|
||||
@@ -1057,7 +1058,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.in(ContractManage::getId, contractIds))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ContractManage::getId,
|
||||
contract -> normalizePartyName(contract.getPartyA()),
|
||||
contract -> normalizePartyName(contract.getPartyB()),
|
||||
(left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
+126
-14
@@ -70,6 +70,7 @@ import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
@@ -919,11 +920,12 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
detail.setBatchNo(source.getBatchNo());
|
||||
detail.setUnitPrice(source.getUnitPrice());
|
||||
detail.setFreightAmount(money(source.getFreightAmount()));
|
||||
detail.setFeeItemsJson(source.getFeeItemsJson());
|
||||
detail.setFeeItemsJson(sanitizeFeeItemsJson(source.getFeeItemsJson()));
|
||||
BigDecimal originalAmount = sourceOriginalAmount(source);
|
||||
BigDecimal settlementAmount = resolveSourceSettlementAmount(source, originalAmount);
|
||||
detail.setOriginalAmount(originalAmount);
|
||||
detail.setAdjustAmount(money(source.getTotalAmount()).subtract(originalAmount));
|
||||
detail.setSettlementAmountTax(money(source.getTotalAmount()));
|
||||
detail.setAdjustAmount(settlementAmount.subtract(originalAmount));
|
||||
detail.setSettlementAmountTax(settlementAmount);
|
||||
detail.setCurrency(Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency());
|
||||
detail.setRemark(source.getRemark());
|
||||
Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId());
|
||||
@@ -950,10 +952,13 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, source.getId())
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0));
|
||||
if (sourceFees.isEmpty() || sourceFees.stream().noneMatch(item -> item.getOriginalAmount() != null)) {
|
||||
return money(source.getTotalAmount());
|
||||
return resolveSourceSettlementAmount(source, money(source.getTotalAmount()));
|
||||
}
|
||||
return sourceFees.stream().map(ReceivablePayableCargoFee::getOriginalAmount)
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (ReceivablePayableCargoFee sourceFee : sourceFees) {
|
||||
total = total.add(resolveCargoFeeAmount(sourceFee, sourceFee.getOriginalAmount()));
|
||||
}
|
||||
return money(total);
|
||||
}
|
||||
|
||||
private void copySourceFees(PreSettlementDetail detail, ReceivablePayableDetail source) {
|
||||
@@ -973,10 +978,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
fee.setMileage(source.getMileage());
|
||||
fee.setUnitPrice(source.getUnitPrice());
|
||||
fee.setFreightAmount(money(source.getFreightAmount()));
|
||||
fee.setFeeItemsJson(source.getFeeItemsJson());
|
||||
fee.setOriginalAmount(money(source.getTotalAmount()));
|
||||
fee.setFeeItemsJson(sanitizeFeeItemsJson(source.getFeeItemsJson()));
|
||||
BigDecimal amount = resolveSourceSettlementAmount(source, money(source.getTotalAmount()));
|
||||
fee.setOriginalAmount(amount);
|
||||
fee.setAdjustAmount(BigDecimal.ZERO.setScale(2));
|
||||
fee.setSettlementAmountTax(money(source.getTotalAmount()));
|
||||
fee.setSettlementAmountTax(amount);
|
||||
fee.setRemark(source.getRemark());
|
||||
detailFeeMapper.insert(fee);
|
||||
return;
|
||||
@@ -987,6 +993,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
fee.setId(null);
|
||||
fee.setPreSettlementDetailId(detail.getId());
|
||||
fee.setSourceFeeId(sourceFee.getId());
|
||||
fee.setFeeItemsJson(sanitizeFeeItemsJson(sourceFee.getFeeItemsJson()));
|
||||
// 历史应付费用可能没有 after_amount,不能在首次打开明细调整时被当成 0。
|
||||
BigDecimal settlementAmount = sourceFee.getAfterAmount() != null
|
||||
? money(sourceFee.getAfterAmount())
|
||||
@@ -994,8 +1001,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
? money(source.getTotalAmount())
|
||||
: sourceFee.getOriginalAmount() != null
|
||||
? money(sourceFee.getOriginalAmount()) : BigDecimal.ZERO.setScale(2);
|
||||
settlementAmount = resolveCargoFeeAmount(sourceFee, settlementAmount);
|
||||
BigDecimal originalAmount = sourceFee.getOriginalAmount() == null
|
||||
? settlementAmount : money(sourceFee.getOriginalAmount());
|
||||
? settlementAmount : resolveCargoFeeAmount(sourceFee, sourceFee.getOriginalAmount());
|
||||
fee.setOriginalAmount(originalAmount);
|
||||
fee.setAdjustAmount(settlementAmount.subtract(originalAmount));
|
||||
fee.setSettlementAmountTax(settlementAmount);
|
||||
@@ -1012,14 +1020,23 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
.filter(row -> !Integer.valueOf(1).equals(row.getManualFlag()))
|
||||
.collect(Collectors.toMap(this::summaryKey, Function.identity(), (left, right) -> left));
|
||||
Map<String, BigDecimal> aggregates = new LinkedHashMap<>();
|
||||
List<Long> detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList();
|
||||
List<PreSettlementDetail> details = listDetails(settlementId);
|
||||
List<Long> detailIds = details.stream().map(PreSettlementDetail::getId).toList();
|
||||
List<PreSettlementDetailFee> feeRows = detailIds.isEmpty() ? List.of()
|
||||
: detailFeeMapper.selectList(Wrappers.<PreSettlementDetailFee>lambdaQuery()
|
||||
.in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds)
|
||||
.eq(PreSettlementDetailFee::getIsDeleted, 0)
|
||||
.orderByAsc(PreSettlementDetailFee::getCreateTime));
|
||||
Set<Long> repairedDetailIds = new LinkedHashSet<>();
|
||||
for (PreSettlementDetailFee feeRow : feeRows) {
|
||||
boolean polluted = isPollutedFeeItemsJson(feeRow.getFeeItemsJson());
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(feeRow.getFeeItemsJson());
|
||||
if (polluted) {
|
||||
repairDetailFeeAmounts(feeRow, feeItems);
|
||||
if (feeRow.getPreSettlementDetailId() != null) {
|
||||
repairedDetailIds.add(feeRow.getPreSettlementDetailId());
|
||||
}
|
||||
}
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal componentAmount = feeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
@@ -1035,8 +1052,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryKey(feeTypeMap.getOrDefault(feeItem,
|
||||
""), feeItem),
|
||||
money(amount), BigDecimal::add));
|
||||
BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax())
|
||||
.subtract(componentAmount));
|
||||
// 脏数据已在上面修复结算金额;正常明细才用 settlementAmountTax 与费用项差额补「其他费用」
|
||||
BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax()).subtract(componentAmount));
|
||||
if (residualAmount.signum() != 0) {
|
||||
aggregates.merge(
|
||||
summaryKey(feeTypeMap.getOrDefault("其他费用", ""), "其他费用"),
|
||||
@@ -1083,6 +1100,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryFeeMapper.insert(row);
|
||||
}
|
||||
renumberSummaryFees(settlementId);
|
||||
if (!repairedDetailIds.isEmpty()) {
|
||||
details.stream()
|
||||
.filter(detail -> repairedDetailIds.contains(detail.getId()))
|
||||
.forEach(this::refreshDetail);
|
||||
}
|
||||
}
|
||||
|
||||
private void applySummaryRequest(Long settlementId, List<PreSettlementSaveRequest.SummaryFee> requestRows) {
|
||||
@@ -1684,7 +1706,29 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, Map.class);
|
||||
if (parsed instanceof Map<?, ?> map) {
|
||||
map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value)));
|
||||
BigDecimal otherFeeAmount = BigDecimal.ZERO;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String key = String.valueOf(entry.getKey());
|
||||
Object value = entry.getValue();
|
||||
if (Func.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
|
||||
otherFeeAmount = otherFeeAmount.add(decimal(value));
|
||||
continue;
|
||||
}
|
||||
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof java.util.Collection<?>) {
|
||||
continue;
|
||||
}
|
||||
result.put(key, decimal(value));
|
||||
}
|
||||
if (otherFeeAmount.signum() > 0 && result.keySet().stream()
|
||||
.noneMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")))) {
|
||||
result.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 历史脏数据不影响结算单展示。
|
||||
@@ -1692,6 +1736,74 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isPollutedFeeItemsJson(String json) {
|
||||
if (Func.isEmpty(json)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, Map.class);
|
||||
if (!(parsed instanceof Map<?, ?> map)) {
|
||||
return false;
|
||||
}
|
||||
return map.keySet().stream()
|
||||
.map(String::valueOf)
|
||||
.anyMatch(TransportBusinessSupport::isFreightJsonMetaKey);
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFeeItemsJson(String json) {
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(json);
|
||||
return JsonUtil.toJson(feeItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源应付明细金额:若 fee_items_json 混入 freightJson 结构字段,按清洗后费用项重算,避免转预结算翻倍。
|
||||
*/
|
||||
private BigDecimal resolveSourceSettlementAmount(ReceivablePayableDetail source, BigDecimal fallback) {
|
||||
if (source == null) {
|
||||
return money(fallback);
|
||||
}
|
||||
if (!isPollutedFeeItemsJson(source.getFeeItemsJson())) {
|
||||
return money(fallback);
|
||||
}
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(source.getFeeItemsJson());
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return money(containsFreight ? feeItemTotal : money(source.getFreightAmount()).add(feeItemTotal));
|
||||
}
|
||||
|
||||
private BigDecimal resolveCargoFeeAmount(ReceivablePayableCargoFee sourceFee, BigDecimal fallback) {
|
||||
if (sourceFee == null) {
|
||||
return money(fallback);
|
||||
}
|
||||
if (!isPollutedFeeItemsJson(sourceFee.getFeeItemsJson())) {
|
||||
return money(fallback);
|
||||
}
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(sourceFee.getFeeItemsJson());
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return money(containsFreight ? feeItemTotal : money(sourceFee.getFreightAmount()).add(feeItemTotal));
|
||||
}
|
||||
|
||||
/** 重建合计时顺带修复已落库的脏费用行金额与 fee_items_json。 */
|
||||
private void repairDetailFeeAmounts(PreSettlementDetailFee feeRow, Map<String, BigDecimal> sanitizedFeeItems) {
|
||||
boolean containsFreight = sanitizedFeeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal feeItemTotal = sanitizedFeeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal corrected = money(containsFreight
|
||||
? feeItemTotal
|
||||
: money(feeRow.getFreightAmount()).add(feeItemTotal));
|
||||
feeRow.setFeeItemsJson(JsonUtil.toJson(sanitizedFeeItems));
|
||||
feeRow.setOriginalAmount(corrected);
|
||||
feeRow.setSettlementAmountTax(corrected);
|
||||
feeRow.setAdjustAmount(BigDecimal.ZERO.setScale(2));
|
||||
detailFeeMapper.updateById(feeRow);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> normalizeFeeItems(Map<String, BigDecimal> feeItems) {
|
||||
Map<String, BigDecimal> result = new LinkedHashMap<>();
|
||||
if (feeItems == null) return result;
|
||||
|
||||
+5
-2
@@ -417,11 +417,13 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
applyUndertakeDeptContains(queryWrapper, projectApply.getUndertakeDeptId());
|
||||
}
|
||||
} else if (!Objects.equals(projectApply.getAllDept(), 1)) {
|
||||
// 非超管默认收口:业务部门、平台公司、创建人/经办人/负责人可见
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> {
|
||||
if (currentDeptId != null) {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
wrapper.eq(ProjectApply::getBusinessDeptId, currentDeptId)
|
||||
.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
}
|
||||
wrapper.or().eq(ProjectApply::getCreateUser, currentUserId)
|
||||
.or().eq(ProjectApply::getHandlerUserId, currentUserId)
|
||||
@@ -1091,7 +1093,8 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
return containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
|
||||
return Objects.equals(projectApply.getBusinessDeptId(), currentDeptId)
|
||||
|| containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
|
||||
|| Objects.equals(projectApply.getCreateUser(), currentUserId)
|
||||
|| Objects.equals(projectApply.getHandlerUserId(), currentUserId)
|
||||
|| Objects.equals(projectApply.getPrincipalUserId(), currentUserId);
|
||||
|
||||
+10
-2
@@ -238,11 +238,13 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
|
||||
applyUndertakeDeptContains(queryWrapper, projectChange.getUndertakeDeptId());
|
||||
}
|
||||
} else if (!Objects.equals(projectChange.getAllDept(), 1)) {
|
||||
// 非超管默认收口:业务部门、平台公司、创建人/经办人/负责人可见
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> {
|
||||
if (currentDeptId != null) {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
wrapper.eq(ProjectChange::getBusinessDeptId, currentDeptId)
|
||||
.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
}
|
||||
wrapper.or().eq(ProjectChange::getCreateUser, currentUserId)
|
||||
.or().eq(ProjectChange::getHandlerUserId, currentUserId)
|
||||
@@ -502,7 +504,13 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
|
||||
return true;
|
||||
}
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
|
||||
if (currentDeptId == null || Func.isBlank(projectChange.getUndertakeDeptId())) {
|
||||
if (currentDeptId == null) {
|
||||
return false;
|
||||
}
|
||||
if (Objects.equals(projectChange.getBusinessDeptId(), currentDeptId)) {
|
||||
return true;
|
||||
}
|
||||
if (Func.isBlank(projectChange.getUndertakeDeptId())) {
|
||||
return false;
|
||||
}
|
||||
return List.of(projectChange.getUndertakeDeptId().split(",")).stream()
|
||||
|
||||
+130
-10
@@ -61,6 +61,7 @@ import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -478,6 +479,8 @@ public class ReceivablePayableDetailServiceImpl
|
||||
? FEE_SOURCE_MANUAL : FEE_SOURCE_AUTO);
|
||||
}
|
||||
});
|
||||
// 历史脏数据:fee_items_json 误含 freightJson 结构字段导致结算金额翻倍,打开详情时自动修复
|
||||
repairPollutedFreightFeeItems(detail, rows);
|
||||
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
|
||||
// 只展示本明细已生成的费用项。合同上其他计费方案的费用项不能叠到当前明细。
|
||||
return result;
|
||||
@@ -1265,15 +1268,15 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) {
|
||||
return null;
|
||||
}
|
||||
// 承运商合同甲方应与运单绑定的客户合同甲方一致,乙方必须是运单选择的承运商。
|
||||
// 承运商合同甲方应与运单绑定的客户合同乙方(我方)一致,乙方必须是运单选择的承运商。
|
||||
ContractManage customerContract = contractManageService.getById(waybill.getContractId());
|
||||
if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) {
|
||||
if (customerContract == null || Func.isEmpty(customerContract.getPartyB())) {
|
||||
return null;
|
||||
}
|
||||
return contractManageService.getOne(Wrappers.<ContractManage>lambdaQuery()
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.eq(ContractManage::getProjectId, waybill.getProjectId())
|
||||
.eq(ContractManage::getPartyA, customerContract.getPartyA())
|
||||
.eq(ContractManage::getPartyA, customerContract.getPartyB())
|
||||
.eq(ContractManage::getPartyB, carrierName)
|
||||
.eq(ContractManage::getContractCategory, "承运商合同")
|
||||
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
|
||||
@@ -2005,12 +2008,15 @@ public class ReceivablePayableDetailServiceImpl
|
||||
private ReceivablePayableCargoFee buildCargoFee(Long detailId, Waybill waybill) {
|
||||
BigDecimal quantity = money(waybill.getQuantity());
|
||||
BigDecimal unitPrice = money(waybill.getUnitPrice());
|
||||
BigDecimal freightAmount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal otherFeeAmount = money(waybill.getOtherFeeTotal());
|
||||
Map<String, Object> feeItems = parseMap(waybill.getFreightJson());
|
||||
if (feeItems.isEmpty() && otherFeeAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
feeItems.put("其它费用", otherFeeAmount);
|
||||
// 优先取运单 freightJson 中的运费;没有再回退数量×单价,避免与费用项重复累加
|
||||
BigDecimal freightAmount = waybillFreight(waybill);
|
||||
if (freightAmount == null) {
|
||||
freightAmount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
|
||||
} else {
|
||||
freightAmount = money(freightAmount);
|
||||
}
|
||||
// freightJson 是币种/运费合计等结构体,不能整包当作费用项目 Map
|
||||
Map<String, Object> feeItems = resolveWaybillFeeItems(waybill);
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::decimal).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = freightAmount.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
ReceivablePayableCargoFee cargoFee = new ReceivablePayableCargoFee();
|
||||
@@ -2513,7 +2519,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (Func.isEmpty(vo.getDataSource())) {
|
||||
vo.setDataSource(isManualFee(row) ? FEE_SOURCE_MANUAL : FEE_SOURCE_AUTO);
|
||||
}
|
||||
Map<String, Object> feeItems = parseMap(row.getFeeItemsJson());
|
||||
Map<String, Object> feeItems = sanitizeFeeItems(parseMap(row.getFeeItemsJson()));
|
||||
feeItemNames.addAll(feeItems.keySet());
|
||||
vo.setFeeItems(feeItems);
|
||||
vo.setTransportQuantityText(formatQuantity(row.getTransportQuantity(), row.getQuantityUnit()));
|
||||
@@ -2530,6 +2536,52 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复误把运单 freightJson 结构字段写入 fee_items_json 的历史费用行,
|
||||
* 并按「运输费字段 + 清洗后费用项」重算结算金额,消除翻倍。
|
||||
*/
|
||||
private void repairPollutedFreightFeeItems(ReceivablePayableDetail detail, List<ReceivablePayableCargoFee> rows) {
|
||||
if (detail == null || rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean repaired = false;
|
||||
for (ReceivablePayableCargoFee row : rows) {
|
||||
Map<String, Object> rawFeeItems = parseMap(row.getFeeItemsJson());
|
||||
if (!isPollutedFreightFeeItems(rawFeeItems)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> sanitized = sanitizeFeeItems(rawFeeItems);
|
||||
Map<String, BigDecimal> feeItemAmounts = new LinkedHashMap<>();
|
||||
sanitized.forEach((name, value) -> feeItemAmounts.put(name, decimal(value)));
|
||||
BigDecimal afterAmount = adjustedAfterAmount(money(row.getFreightAmount()), feeItemAmounts);
|
||||
row.setFeeItemsJson(JsonUtil.toJson(sanitized));
|
||||
row.setOriginalAmount(afterAmount);
|
||||
row.setAfterAmount(afterAmount);
|
||||
row.setAdjustAmount(BigDecimal.ZERO);
|
||||
cargoFeeMapper.updateById(row);
|
||||
repaired = true;
|
||||
}
|
||||
if (!repaired) {
|
||||
return;
|
||||
}
|
||||
BigDecimal freight = rows.stream().map(fee -> money(fee.getFreightAmount()))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = rows.stream().map(fee -> money(fee.getAfterAmount()))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
detail.setFreightAmount(freight);
|
||||
detail.setOtherFeeAmount(total.subtract(freight).setScale(2, RoundingMode.HALF_UP));
|
||||
detail.setTotalAmount(total);
|
||||
detail.setFeeItemsJson(aggregateFeeItemsJson(rows));
|
||||
updateById(detail);
|
||||
}
|
||||
|
||||
private boolean isPollutedFreightFeeItems(Map<String, Object> feeItems) {
|
||||
if (feeItems == null || feeItems.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return feeItems.keySet().stream().anyMatch(TransportBusinessSupport::isFreightJsonMetaKey);
|
||||
}
|
||||
|
||||
private List<String> contractFeeItemNames(Long contractId) {
|
||||
if (contractId == null) return List.of();
|
||||
ContractManage contract = contractManageService.getById(contractId);
|
||||
@@ -3000,7 +3052,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
private String aggregateFeeItemsJson(List<ReceivablePayableCargoFee> fees) {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) ->
|
||||
fees.forEach(fee -> sanitizeFeeItems(parseMap(fee.getFeeItemsJson())).forEach((name, value) ->
|
||||
feeItems.merge(name, decimal(value), BigDecimal::add)));
|
||||
return JsonUtil.toJson(feeItems);
|
||||
}
|
||||
@@ -3456,6 +3508,74 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从运单 freightJson / otherFeeTotal 解析费用项目(名称→金额)。
|
||||
* 仅保留真正的费用项;结构字段(currency、totalFreightAmount 等)不会写入。
|
||||
*/
|
||||
private Map<String, Object> resolveWaybillFeeItems(Waybill waybill) {
|
||||
Map<String, Object> feeItems = new LinkedHashMap<>();
|
||||
Map<String, Object> freight = waybillFreightMap(waybill);
|
||||
freight.forEach((key, value) -> {
|
||||
if (Func.isBlank(key) || TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
return;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof Collection<?>) {
|
||||
return;
|
||||
}
|
||||
// 兼容历史「费用名→金额」扁平 JSON(键名通常含「费」)
|
||||
if (!key.contains("费")) {
|
||||
return;
|
||||
}
|
||||
feeItems.put(key, decimal(value));
|
||||
});
|
||||
BigDecimal otherFeeAmount = money(waybillOtherFee(waybill));
|
||||
if (otherFeeAmount.compareTo(BigDecimal.ZERO) > 0 && !containsOtherFeeItem(feeItems.keySet())) {
|
||||
feeItems.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
return feeItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗已落库的 fee_items_json:过滤误写入的 freightJson 结构字段,
|
||||
* 并将 otherFreightAmount / otherFeeTotal 归一为「其它费用」。
|
||||
*/
|
||||
private Map<String, Object> sanitizeFeeItems(Map<String, Object> raw) {
|
||||
Map<String, Object> feeItems = new LinkedHashMap<>();
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return feeItems;
|
||||
}
|
||||
BigDecimal otherFeeAmount = BigDecimal.ZERO;
|
||||
for (Map.Entry<String, Object> entry : raw.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (Func.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
|
||||
otherFeeAmount = otherFeeAmount.add(decimal(value));
|
||||
continue;
|
||||
}
|
||||
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof Collection<?>) {
|
||||
continue;
|
||||
}
|
||||
feeItems.put(key, decimal(value));
|
||||
}
|
||||
if (otherFeeAmount.compareTo(BigDecimal.ZERO) > 0 && !containsOtherFeeItem(feeItems.keySet())) {
|
||||
feeItems.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
return feeItems;
|
||||
}
|
||||
|
||||
private boolean containsOtherFeeItem(Collection<String> names) {
|
||||
if (names == null || names.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return names.stream().anyMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")));
|
||||
}
|
||||
|
||||
private Map<String, Object> parseMap(String json) {
|
||||
if (Func.isEmpty(json)) {
|
||||
return new LinkedHashMap<>();
|
||||
|
||||
+16
@@ -37,6 +37,7 @@ import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -87,9 +88,24 @@ public final class TransportBusinessSupport {
|
||||
/** 临时额度管理菜单编码 */
|
||||
public static final String MENU_TEMPORARY_CREDIT_LIMIT = "temporary_credit_limit";
|
||||
|
||||
/**
|
||||
* 运单 freightJson 结构字段,不是费用项目名。
|
||||
* 误写入 fee_items_json / 预结算合计时需过滤,否则会出现 totalFreightAmount 等英文费用项并导致金额翻倍。
|
||||
*/
|
||||
public static final Set<String> FREIGHT_JSON_META_KEYS = Set.of(
|
||||
"currency", "totalFreightAmount", "otherFreightAmount", "freightItems",
|
||||
"freightTotal", "totalFreight", "totalAmount", "freight", "freightAmount",
|
||||
"transportFee", "otherFeeTotal", "otherAmount"
|
||||
);
|
||||
|
||||
private TransportBusinessSupport() {
|
||||
}
|
||||
|
||||
/** 是否运单运费 JSON 的结构字段名(不可作为费用项展示)。 */
|
||||
public static boolean isFreightJsonMetaKey(String key) {
|
||||
return Func.isNotBlank(key) && FREIGHT_JSON_META_KEYS.contains(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否超级管理员:当前用户任一角色别名等于 administrator 即为超级管理员。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user