🐛 修复进度预付驳回重提,并校正结算费用与运输计划组织

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-20 01:50:55 +08:00
parent 766100a55c
commit 87222f2e8c
8 changed files with 302 additions and 111 deletions
@@ -36,6 +36,8 @@ public class PaymentApplicationSaveRequest implements Serializable {
private List<Long> settlementIds;
private Long preSettlementId;
private List<Long> preSettlementIds;
/** 预结算单号。驳回重提时用于在主键失真后找回预结算单 */
private String preSettlementNo;
private Long projectId;
private String projectName;
private Long deptId;
@@ -61,8 +61,9 @@ public class BillLedgerController extends BladeController {
@ApiOperationSupport(order = 4)
@Operation(summary = "付款申请可用汇票")
public R<List<BillLedgerVO>> availableOptions(@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId) {
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId));
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId,
@RequestParam(required = false) String payerName, @RequestParam(required = false) String payeeName) {
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId, payerName, payeeName));
}
@GetMapping("/available-page")
@@ -70,8 +71,10 @@ public class BillLedgerController extends BladeController {
@Operation(summary = "付款申请可用汇票分页")
public R<IPage<BillLedgerVO>> availablePage(Query pageQuery,
@RequestParam(required = false) String keyword, @RequestParam(required = false) Long deptId,
@RequestParam(required = false) Long selectedId) {
return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId));
@RequestParam(required = false) Long selectedId, @RequestParam(required = false) String payerName,
@RequestParam(required = false) String payeeName) {
return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId,
payerName, payeeName));
}
@PostMapping("/submit")
@@ -18,8 +18,9 @@ public interface IBillLedgerService extends BaseService<BillLedger> {
IPage<BillLedgerVO> selectPage(IPage<BillLedger> page, BillLedgerVO query);
BillLedgerVO detail(Long id);
Map<String, Long> expiryCounts();
List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId);
IPage<BillLedgerVO> availablePage(IPage<BillLedger> page, String keyword, Long deptId, Long selectedId);
List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId, String payerName, String payeeName);
IPage<BillLedgerVO> availablePage(IPage<BillLedger> page, String keyword, Long deptId, Long selectedId,
String payerName, String payeeName);
Long submit(BillLedgerSaveRequest request);
void removeLedger(Long id);
}
@@ -86,14 +86,15 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
}
@Override
public List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId) {
return availableList(keyword, deptId, selectedId);
public List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId, String payerName,
String payeeName) {
return availableList(keyword, deptId, selectedId, payerName, payeeName);
}
@Override
public IPage<BillLedgerVO> availablePage(IPage<BillLedger> page, String keyword, Long deptId,
Long selectedId) {
List<BillLedgerVO> available = availableList(keyword, deptId, selectedId);
Long selectedId, String payerName, String payeeName) {
List<BillLedgerVO> available = availableList(keyword, deptId, selectedId, payerName, payeeName);
long current = Math.max(page.getCurrent(), 1);
long size = Math.max(page.getSize(), 1);
long from = Math.min((current - 1) * size, available.size());
@@ -103,27 +104,50 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
return result;
}
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId) {
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId, String payerName,
String payeeName) {
LocalDate today = LocalDate.now();
// 付款申请仅可选:收票类型、未到期、有可用余额;已选中的汇票始终带回以便回显
// 未到期且有余额;收票且部门可用,或出票/收票单位与结算收付款方一致(开票、收票都可)
return list(Wrappers.<BillLedger>lambdaQuery()
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword)
.or().like(BillLedger::getIssuerName, keyword)
.or().like(BillLedger::getReceiverName, keyword))
.and(wrapper -> wrapper
.and(available -> available
.eq(BillLedger::getBillType, "received")
.ge(BillLedger::getMaturityDate, today)
.gt(BillLedger::getAvailableBalance, BigDecimal.ZERO))
.or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId)))
.orderByAsc(BillLedger::getMaturityDate)
.orderByDesc(BillLedger::getCreateTime)).stream()
.filter(item -> selectedId != null && Objects.equals(item.getId(), selectedId)
|| departmentAvailable(item, deptId))
|| selectableBill(item, deptId, payerName, payeeName))
.map(item -> BillLedgerWrapper.build().entityVO(item))
.toList();
}
/**
* 收票且当前部门在可用范围内,或出票单位、收票单位与结算付款方、收款方一致(顺序不限)。
*/
private boolean selectableBill(BillLedger ledger, Long deptId, String payerName, String payeeName) {
boolean receivedAndDept = "received".equals(ledger.getBillType()) && departmentAvailable(ledger, deptId);
return receivedAndDept || partiesMatch(ledger.getIssuerName(), ledger.getReceiverName(), payerName, payeeName);
}
private boolean partiesMatch(String issuerName, String receiverName, String payerName, String payeeName) {
String issuer = trimName(issuerName);
String receiver = trimName(receiverName);
String payer = trimName(payerName);
String payee = trimName(payeeName);
if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) {
return false;
}
return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer));
}
private String trimName(String value) {
return value == null ? "" : value.trim();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long submit(BillLedgerSaveRequest request) {
@@ -141,6 +165,11 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
}
CustomerArchive issuer = customer(request.getIssuerId(), "出票单位");
CustomerArchive feeBearer = customer(request.getFeeBearerId(), "费用承担方");
String receiverName = required(request.getReceiverName(), "收票单位", 100);
if (!Objects.equals(feeBearer.getId(), issuer.getId())
&& !customerName(feeBearer).equals(receiverName)) {
throw new ServiceException("费用承担方只能选择收票单位或出票单位");
}
BigDecimal faceAmount = positive(request.getFaceAmount(), "票面金额").setScale(2, RoundingMode.HALF_UP);
BigDecimal usedAmount = entity.getId() == null ? BigDecimal.ZERO : activeUsedAmount(entity.getId());
if (faceAmount.compareTo(usedAmount) < 0) {
@@ -150,7 +179,7 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
entity.setBillNo(billNo);
entity.setIssuerId(issuer.getId());
entity.setIssuerName(customerName(issuer));
entity.setReceiverName(required(request.getReceiverName(), "收票单位", 100));
entity.setReceiverName(receiverName);
entity.setBillType(request.getBillType());
entity.setFaceAmount(faceAmount);
entity.setAvailableBalance(faceAmount.subtract(usedAmount).setScale(2, RoundingMode.HALF_UP));
@@ -374,8 +374,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount()));
}
entity.setMatchedInvoiceAmount(matchedInvoiceAmount);
// 进度预付只关联预结算单。编辑回传的 settlementId 可能是预结算单 ID,不能拿去查正式结算单。
normalizeSettlementReference(request);
// 进度预付只关联预结算单。编辑回传的 settlementId 可能是预结算单 ID 或已失效的正式结算单 ID。
normalizeSettlementReference(request, entity);
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementId() == null
&& request.getSettlementIds() != null && !request.getSettlementIds().isEmpty()) {
request.setSettlementId(request.getSettlementIds().get(0));
@@ -1897,16 +1897,19 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
/**
* 进度预付只认预结算单;尾款付款若 settlementId 实际是预结算单 ID 或正式结算单已不存在,改为按预结算单保存。
*/
private void normalizeSettlementReference(PaymentApplicationSaveRequest request) {
if ("progress_advance".equals(request.getPaymentType())) {
if (request.getPreSettlementId() == null && request.getPreSettlementIds() != null) {
request.setPreSettlementId(request.getPreSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
private void normalizeSettlementReference(PaymentApplicationSaveRequest request, PaymentApplication entity) {
if ("progress_advance".equals(request.getPaymentType())
|| (entity != null && "progress_advance".equals(entity.getPaymentType())
&& Func.isEmpty(request.getPaymentType()))) {
if (Func.isEmpty(request.getPaymentType())) {
request.setPaymentType("progress_advance");
}
if (request.getPreSettlementId() == null) {
request.setPreSettlementId(request.getSettlementId());
}
if (request.getPreSettlementId() == null && request.getSettlementIds() != null) {
request.setPreSettlementId(request.getSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
Long preSettlementId = resolveProgressPreSettlementId(request, entity);
request.setPreSettlementId(preSettlementId);
if (preSettlementId == null) {
request.setPreSettlementIds(null);
} else if (request.getPreSettlementIds() == null || request.getPreSettlementIds().stream().noneMatch(Objects::nonNull)) {
request.setPreSettlementIds(List.of(preSettlementId));
}
request.setSettlementId(null);
request.setSettlementIds(null);
@@ -1963,8 +1966,61 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setSettlementNo(null);
}
/**
* 进度预付驳回重提时,请求里的结算主键可能是失效正式结算单或精度失真后的 ID。
* 依次用预结算主键、正式结算位上的预结算主键、单据已保存的预结算主键和预结算单号找回。
*/
private Long resolveProgressPreSettlementId(PaymentApplicationSaveRequest request, PaymentApplication entity) {
List<Long> candidates = new java.util.ArrayList<>();
if (request.getPreSettlementId() != null) {
candidates.add(request.getPreSettlementId());
}
if (request.getPreSettlementIds() != null) {
request.getPreSettlementIds().stream().filter(Objects::nonNull).forEach(candidates::add);
}
if (request.getSettlementId() != null) {
candidates.add(request.getSettlementId());
}
if (request.getSettlementIds() != null) {
request.getSettlementIds().stream().filter(Objects::nonNull).forEach(candidates::add);
}
if (entity != null && entity.getPreSettlementId() != null) {
candidates.add(entity.getPreSettlementId());
}
for (Long candidate : candidates) {
PreSettlement preSettlement = preSettlementMapper.selectById(candidate);
if (preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1)) {
return preSettlement.getId();
}
}
String preSettlementNo = request.getPreSettlementNo();
if (Func.isEmpty(preSettlementNo) && entity != null) {
preSettlementNo = entity.getPreSettlementNo();
}
if (Func.isNotEmpty(preSettlementNo)) {
String settlementNo = preSettlementNo.split("、")[0].trim();
PreSettlement preSettlement = preSettlementMapper.selectOne(Wrappers.<PreSettlement>lambdaQuery()
.eq(PreSettlement::getPreSettlementNo, settlementNo)
.eq(PreSettlement::getIsDeleted, 0)
.last("limit 1"));
if (preSettlement != null) {
return preSettlement.getId();
}
}
return null;
}
private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) {
if ("project_advance".equals(request.getPaymentType())) {
if ("progress_advance".equals(request.getPaymentType())) {
Long preSettlementId = resolveProgressPreSettlementId(request, entity);
request.setPreSettlementId(preSettlementId);
if (preSettlementId == null) {
throw new ServiceException("进度预付必须关联预结算单");
}
request.setSettlementId(null);
request.setSettlementIds(null);
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
} else if ("project_advance".equals(request.getPaymentType())) {
ProjectApply project = projectApplyMapper.selectById(request.getProjectId());
if (project == null || Objects.equals(project.getIsDeleted(), 1)) throw new ServiceException("所属项目不存在");
entity.setSettlementId(null); entity.setSettlementNo(null); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
@@ -1981,6 +2037,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
} else if (request.getPreSettlementId() != null) {
request.setSettlementId(null);
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
} else if (entity.getPreSettlementId() != null) {
request.setSettlementId(null);
request.setPreSettlementId(entity.getPreSettlementId());
fillPreSettlementReference(entity, requireApprovedPreSettlement(entity.getPreSettlementId()));
} else {
throw new ServiceException("正式结算单不存在");
}
@@ -2487,7 +2547,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return;
}
BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId(),
entity.getSettlementId(), entity.getPreSettlementId());
entity.setBillLedgerId(ledger.getId());
entity.setBillNo(ledger.getBillNo());
}
@@ -2495,25 +2556,57 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private void validateSelectedBill(PaymentApplication entity, boolean locked) {
if (!isBillPayment(entity.getPaymentMethod())) return;
BillLedger ledger = locked ? lockedBill(entity.getBillLedgerId()) : billLedgerMapper.selectById(entity.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId(),
entity.getSettlementId(), entity.getPreSettlementId());
}
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity, Long paymentApplicationId) {
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity,
Long paymentApplicationId, Long settlementId, Long preSettlementId) {
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
if (!"received".equals(ledger.getBillType())) {
throw new ServiceException("汇票付款仅可选择收票类型的汇票");
boolean partyMatched = settlementPartiesMatch(ledger, settlementId, preSettlementId);
if (!"received".equals(ledger.getBillType()) && !partyMatched) {
throw new ServiceException("汇票付款仅可选择收票类型的汇票,或出票单位与收票单位均为当前结算收付款方的汇票");
}
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
throw new ServiceException("所选汇票已到期");
}
BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId()));
if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额");
if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
if (!partyMatched && !departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
}
/** 出票单位、收票单位与结算付款方、收款方一致(顺序不限) */
private boolean settlementPartiesMatch(BillLedger ledger, Long settlementId, Long preSettlementId) {
String payerName = null;
String payeeName = null;
if (settlementId != null) {
FormalSettlement settlement = formalSettlementMapper.selectById(settlementId);
if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) {
payerName = settlement.getPayerName();
payeeName = settlement.getPayeeName();
}
}
if ((payerName == null || payeeName == null) && preSettlementId != null) {
PreSettlement settlement = preSettlementMapper.selectById(preSettlementId);
if (settlement != null && !Objects.equals(settlement.getIsDeleted(), 1)) {
payerName = settlement.getPayerName();
payeeName = settlement.getPayeeName();
}
}
String issuer = ledger.getIssuerName() == null ? "" : ledger.getIssuerName().trim();
String receiver = ledger.getReceiverName() == null ? "" : ledger.getReceiverName().trim();
String payer = payerName == null ? "" : payerName.trim();
String payee = payeeName == null ? "" : payeeName.trim();
if (issuer.isEmpty() || receiver.isEmpty() || payer.isEmpty() || payee.isEmpty()) {
return false;
}
return (issuer.equals(payer) && receiver.equals(payee)) || (issuer.equals(payee) && receiver.equals(payer));
}
private void useBillBalance(PaymentApplication entity) {
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null);
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null,
entity.getSettlementId(), entity.getPreSettlementId());
BillLedgerUsage exists = lockedBillUsage(entity.getId());
if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) {
throw new ServiceException("该付款申请已生成汇票使用记录");
@@ -206,6 +206,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<PreSettlementChangeRecord>lambdaQuery()
.eq(PreSettlementChangeRecord::getPreSettlementId, id)
.eq(PreSettlementChangeRecord::getIsDeleted, 0)
.and(wrapper -> wrapper.isNull(PreSettlementChangeRecord::getChangeContent)
.or()
.ne(PreSettlementChangeRecord::getChangeContent, "预结算审批通过"))
.orderByDesc(PreSettlementChangeRecord::getChangeTime)));
vo.setPrintTemplates(resolvePrintTemplates(settlement));
return vo;
@@ -355,8 +358,10 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
synchronizeDetails(settlement, request.getSourceDetailIds(), Boolean.TRUE.equals(request.getAllowSourceMismatch()));
}
settlement.setExchangeRate(normalizeRate(request.getExchangeRate(), settlement.getCurrency()));
List<PreSettlementSummaryFee> summaryBefore = created ? new ArrayList<>() : listSummaryFees(settlement.getId());
rebuildSummaryFees(settlement.getId(), true);
applySummaryRequest(settlement.getId(), contractChanged ? List.of() : request.getSummaryFees());
recordSummaryFeeChanges(settlement.getId(), summaryBefore);
refreshSettlementAmount(settlement);
projectAdvanceWriteOffService.replaceWriteOffs(
TransportBusiConstant.SETTLEMENT_BILL_PRE,
@@ -425,7 +430,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setCurrentProcessor(AuthUtil.getUserName());
settlement.setApprovedTime(LocalDateTime.now());
updateById(settlement);
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批通过", request.getReason());
}
@Override
@@ -1129,11 +1133,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
throw new ServiceException("存在无效的手工费用行");
}
boolean creating = row.getId() == null;
String beforeFeeType = creating ? null : row.getFeeType();
String beforeFeeItem = creating ? null : row.getFeeItem();
BigDecimal beforeAdjustAmount = creating ? null : money(row.getAdjustAmount());
BigDecimal beforeSettlementAmount = creating ? null : money(row.getSettlementAmount());
String beforeRemark = creating ? null : row.getRemark();
row.setPreSettlementId(settlementId);
row.setLineNo(lineNo++);
row.setFeeType(feeType);
@@ -1149,8 +1148,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
summaryFeeMapper.updateById(row);
}
retainedManualIds.add(row.getId());
saveManualSummaryFeeChange(settlementId, creating, beforeFeeType, beforeFeeItem,
beforeAdjustAmount, beforeSettlementAmount, beforeRemark, row);
continue;
}
String feeItem = requiredText(requestRow.getFeeItem(), "费用项");
@@ -1175,45 +1172,16 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
row.setFeeItem(feeItem);
// 生成费用行的三个金额必须来源于结算明细快照,不能接受前端回传值覆盖原金额。
// rebuildSummaryFees 已按明细重建 original/adjust/settlement,此处仅保留备注。
BigDecimal before = money(row.getAdjustAmount());
row.setOriginalAmount(money(row.getOriginalAmount()));
row.setSettlementAmount(money(row.getSettlementAmount()));
row.setAdjustAmount(money(row.getSettlementAmount().subtract(row.getOriginalAmount())));
row.setRemark(limitRemark(requestRow.getRemark(), 50));
if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row);
retainedGeneratedIds.add(row.getId());
if (before.compareTo(row.getAdjustAmount()) != 0) {
Map<String, Object> beforeData = new LinkedHashMap<>();
Map<String, Object> afterData = new LinkedHashMap<>();
beforeData.put("费用项", row.getFeeItem());
beforeData.put("调整金额", before);
beforeData.put("结算金额", money(row.getOriginalAmount().add(before)));
afterData.put("费用项", row.getFeeItem());
afterData.put("调整金额", row.getAdjustAmount());
afterData.put("结算金额", money(row.getSettlementAmount()));
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整",
"【" + row.getFeeItem() + "-调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】",
"", beforeData, afterData);
}
}
for (PreSettlementSummaryFee manualRow : manualRows) {
if (!retainedManualIds.contains(manualRow.getId())) {
summaryFeeMapper.deleteById(manualRow.getId());
Map<String, Object> beforeData = new LinkedHashMap<>();
Map<String, Object> afterData = new LinkedHashMap<>();
beforeData.put("费用类型", manualRow.getFeeType());
beforeData.put("费用项", manualRow.getFeeItem());
beforeData.put("调整金额", money(manualRow.getAdjustAmount()));
beforeData.put("结算金额", money(manualRow.getSettlementAmount()));
beforeData.put("备注", manualRow.getRemark());
afterData.put("费用类型", "");
afterData.put("费用项", "");
afterData.put("调整金额", BigDecimal.ZERO.setScale(2));
afterData.put("结算金额", BigDecimal.ZERO.setScale(2));
afterData.put("备注", "");
saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除",
"删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】",
"", beforeData, afterData);
}
}
for (PreSettlementSummaryFee generatedRow : existingGeneratedRows) {
@@ -1563,38 +1531,87 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
}
}
private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType,
String beforeFeeItem, BigDecimal beforeAdjustAmount, BigDecimal beforeSettlementAmount, String beforeRemark,
PreSettlementSummaryFee row) {
List<String> changes = new ArrayList<>();
private void recordSummaryFeeChanges(Long settlementId, List<PreSettlementSummaryFee> beforeRows) {
List<Map<String, Object>> beforeLines = summaryFeeLines(beforeRows);
List<Map<String, Object>> afterLines = summaryFeeLines(listSummaryFees(settlementId));
if (beforeLines.equals(afterLines)) {
return;
}
String content = summaryFeeChangeContent(beforeLines, afterLines);
if (Func.isEmpty(content)) {
return;
}
Map<String, Object> beforeData = new LinkedHashMap<>();
Map<String, Object> afterData = new LinkedHashMap<>();
if (creating) {
changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】");
beforeData.put("费用类型", "");
beforeData.put("费用项", "");
beforeData.put("调整金额", BigDecimal.ZERO.setScale(2));
beforeData.put("结算金额", BigDecimal.ZERO.setScale(2));
beforeData.put("备注", "");
afterData.put("费用类型", row.getFeeType());
afterData.put("费用项", row.getFeeItem());
afterData.put("调整金额", money(row.getAdjustAmount()));
afterData.put("结算金额", money(row.getSettlementAmount()));
afterData.put("备注", row.getRemark());
saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "",
beforeData, afterData);
return;
beforeData.put("结算合计", beforeLines);
afterData.put("结算合计", afterLines);
String operationType = beforeLines.isEmpty() ? "新增" : afterLines.isEmpty() ? "删除" : "调整";
saveChange(settlementId, "结算合计", null, operationType, content, "", beforeData, afterData);
}
private List<Map<String, Object>> summaryFeeLines(List<PreSettlementSummaryFee> rows) {
Map<String, String> categoryNames = DictBizCache.getList("fee_category").stream()
.collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first));
List<Map<String, Object>> lines = new ArrayList<>();
for (PreSettlementSummaryFee row : rows) {
String feeType = row.getFeeType() == null ? "" : row.getFeeType();
Map<String, Object> line = new LinkedHashMap<>();
line.put("来源", Integer.valueOf(1).equals(row.getManualFlag()) ? "手工新增" : "系统归集");
line.put("费用类型", categoryNames.getOrDefault(feeType, feeType));
line.put("费用项", row.getFeeItem() == null ? "" : row.getFeeItem());
line.put("原金额", money(row.getOriginalAmount()));
line.put("调整金额", money(row.getAdjustAmount()));
line.put("结算金额", money(row.getSettlementAmount()));
line.put("备注", row.getRemark() == null ? "" : row.getRemark());
lines.add(line);
}
appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType());
appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem());
appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount());
appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount());
appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark());
if (changes.isEmpty()) {
return;
return lines;
}
private String summaryFeeChangeContent(List<Map<String, Object>> beforeLines, List<Map<String, Object>> afterLines) {
Map<String, Map<String, Object>> beforeMap = indexSummaryLines(beforeLines);
Map<String, Map<String, Object>> afterMap = indexSummaryLines(afterLines);
List<String> changes = new ArrayList<>();
afterMap.forEach((key, afterLine) -> {
Map<String, Object> beforeLine = beforeMap.get(key);
String label = summaryLineLabel(afterLine);
if (beforeLine == null) {
changes.add("新增结算合计【" + label + "】结算金额【" + afterLine.get("结算金额") + "】");
return;
}
if (!Objects.equals(beforeLine.get("原金额"), afterLine.get("原金额"))
|| !Objects.equals(beforeLine.get("调整金额"), afterLine.get("调整金额"))
|| !Objects.equals(beforeLine.get("结算金额"), afterLine.get("结算金额"))
|| !Objects.equals(String.valueOf(beforeLine.get("备注")), String.valueOf(afterLine.get("备注")))) {
changes.add("调整结算合计【" + label + "】结算金额从【" + beforeLine.get("结算金额")
+ "】调整为【" + afterLine.get("结算金额") + "】");
}
});
beforeMap.forEach((key, beforeLine) -> {
if (!afterMap.containsKey(key)) {
changes.add("删除结算合计【" + summaryLineLabel(beforeLine) + "】结算金额【" + beforeLine.get("结算金额") + "】");
}
});
return String.join(";", changes);
}
private Map<String, Map<String, Object>> indexSummaryLines(List<Map<String, Object>> lines) {
Map<String, Map<String, Object>> indexed = new LinkedHashMap<>();
Map<String, Integer> counts = new HashMap<>();
for (Map<String, Object> line : lines) {
String base = String.valueOf(line.get("来源")) + "|" + line.get("费用类型") + "|" + line.get("费用项");
int index = counts.merge(base, 1, Integer::sum);
indexed.put(base + "#" + index, line);
}
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "",
beforeData, afterData);
return indexed;
}
private String summaryLineLabel(Map<String, Object> line) {
String feeType = String.valueOf(line.getOrDefault("费用类型", ""));
String feeItem = String.valueOf(line.getOrDefault("费用项", ""));
String source = String.valueOf(line.getOrDefault("来源", ""));
String name = feeType.isBlank() ? feeItem : feeType + "-" + feeItem;
return source + " " + name;
}
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
@@ -424,9 +424,7 @@ public class ReceivablePayableDetailServiceImpl
}
});
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
feeItemNames.addAll(result.getFeeItemNames());
result.setFeeItemNames(new ArrayList<>(feeItemNames));
// 只展示本明细已生成的费用项。合同上其他计费方案的费用项不能叠到当前明细。
return result;
}
@@ -1968,21 +1966,35 @@ public class ReceivablePayableDetailServiceImpl
}
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId) {
return calculatedFees(waybill, contract, planId, false);
return calculatedFees(waybill, contract, planId, false, false);
}
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) {
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId,
boolean matchOnly) {
return calculatedFees(waybill, contract, planId, matchOnly, false);
}
/**
* @param strictPlan 更新费用时为 true:选定方案有规则但未命中时,不再回退运单历史费用,避免旧费用项叠加
*/
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId,
boolean matchOnly, boolean strictPlan) {
List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson());
Map<String, Object> plan = "__matched__".equals(planId)
? resolveDefaultBillingPlan(plans, waybill.getTransportType())
: resolveBillingPlan(plans, planId);
return calculatedFees(waybill, plan, matchOnly);
return calculatedFees(waybill, plan, matchOnly, strictPlan);
}
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, Map<String, Object> plan,
boolean matchOnly) {
return calculatedFees(waybill, plan, matchOnly, false);
}
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, Map<String, Object> plan,
boolean matchOnly, boolean strictPlan) {
if (plan == null || !(plan.get("rules") instanceof List<?>)) {
return matchOnly ? List.of() : buildCargoFees(waybill);
return matchOnly || strictPlan ? List.of() : buildCargoFees(waybill);
}
Map<List<String>, ReceivablePayableCargoFee> feesByCargo = new LinkedHashMap<>();
Map<List<String>, Map<String, BigDecimal>> feeItemsByCargo = new LinkedHashMap<>();
@@ -2024,7 +2036,11 @@ public class ReceivablePayableDetailServiceImpl
ReceivablePayableCargoFee fee = result.get(index);
fee.setLineNo(String.format("%04d", index + 1));
}
return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result;
// 已选定计费方案时,只保留该方案算出的费用项,不用运单上的历史费用回填。
if (strictPlan || matchOnly) {
return result;
}
return result.isEmpty() ? buildCargoFees(waybill) : result;
}
private ReceivablePayableCargoFee buildCalculatedCargoFee(Waybill waybill, Waybill feeWaybill,
@@ -2934,7 +2950,7 @@ public class ReceivablePayableDetailServiceImpl
throw new ServiceException("关联运单不存在");
}
ContractManage contract = contractManageService.getById(detail.getContractId());
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId);
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId, false, true);
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); });
BigDecimal freight = fees.stream().map(fee -> money(fee.getFreightAmount()))
@@ -2959,7 +2975,7 @@ public class ReceivablePayableDetailServiceImpl
ContractManage contract = contractManageService.getById(detail.getContractId());
List<Waybill> masterGoods = masterOrderGoods(masterOrder);
List<ReceivablePayableCargoFee> fees = masterGoods.stream()
.flatMap(goods -> calculatedFees(goods, contract, billingPlanId).stream()).toList();
.flatMap(goods -> calculatedFees(goods, contract, billingPlanId, false, true).stream()).toList();
normalizeMasterFeeLines(fees);
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
@@ -2995,7 +3011,7 @@ public class ReceivablePayableDetailServiceImpl
}
ContractManage contract = contractManageService.getById(detail.getContractId());
List<ReceivablePayableCargoFee> fees = waybills.stream()
.flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId).stream()).toList();
.flatMap(waybill -> calculatedFees(waybill, contract, billingPlanId, false, true).stream()).toList();
normalizeWaybillFeeLines(fees);
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
@@ -81,6 +81,8 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
@Override
public IPage<TransportPlanVO> selectTransportPlanPage(IPage<TransportPlan> page, TransportPlanVO transportPlan) {
// 历史手工创建未写入 dept_id,allDept=0 按组织过滤时会被排除
backfillMissingDept(transportPlan);
IPage<TransportPlan> entityPage = page(page, buildQuery(transportPlan));
IPage<TransportPlanVO> resultPage = TransportPlanWrapper.build().pageVO(entityPage);
fillCustomerNamesFromContracts(resultPage.getRecords());
@@ -151,6 +153,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
if (created) {
transportPlan.setPlanNo(nextCode());
}
// 新建或历史空组织一律落到当前组织,避免列表按 dept_id 过滤后看不到
if (created || Func.isEmpty(transportPlan.getDeptId()) || transportPlan.getDeptId() <= 0) {
Dept dept = TransportBusinessSupport.currentDept("运输计划");
transportPlan.setDeptId(dept.getId());
transportPlan.setDeptName(dept.getDeptName());
}
validate(transportPlan);
return saveOrUpdate(transportPlan);
}
@@ -662,11 +670,33 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
));
}
/**
* 把所属组织为空、且创建组织等于当前组织的运输计划回填 dept_id,使 allDept=0 列表能查到。
*/
private void backfillMissingDept(TransportPlanVO transportPlan) {
if (Objects.equals(transportPlan.getAllDept(), 1)) {
return;
}
Dept dept = TransportBusinessSupport.currentDept("运输计划");
if (dept == null || dept.getId() == null) {
return;
}
update(Wrappers.<TransportPlan>lambdaUpdate()
.set(TransportPlan::getDeptId, dept.getId())
.set(TransportPlan::getDeptName, dept.getDeptName())
.isNull(TransportPlan::getDeptId)
.eq(TransportPlan::getCreateDept, dept.getId())
.eq(TransportPlan::getIsDeleted, 0));
}
private LambdaQueryWrapper<TransportPlan> buildQuery(TransportPlanVO transportPlan) {
TransportBusinessSupport.validateAllDept(transportPlan.getAllDept(), "运输计划");
LambdaQueryWrapper<TransportPlan> queryWrapper = Wrappers.<TransportPlan>lambdaQuery().eq(TransportPlan::getIsDeleted, 0);
if (!Objects.equals(transportPlan.getAllDept(), 1)) {
queryWrapper.eq(TransportPlan::getDeptId, TransportBusinessSupport.currentDeptId("运输计划"));
Long currentDeptId = TransportBusinessSupport.currentDeptId("运输计划");
// dept_id 为空的历史数据按创建组织归入当前组织
queryWrapper.and(wrapper -> wrapper.eq(TransportPlan::getDeptId, currentDeptId)
.or(item -> item.isNull(TransportPlan::getDeptId).eq(TransportPlan::getCreateDept, currentDeptId)));
} else if (Func.isNotEmpty(transportPlan.getDeptId())) {
queryWrapper.eq(TransportPlan::getDeptId, transportPlan.getDeptId());
}