🐛 修复应付承运商、汇票占用、一单多货结算与运输对账完成校验

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-17 08:31:33 +08:00
parent c168bf276a
commit 6f73e3fbf9
6 changed files with 465 additions and 43 deletions
@@ -6,6 +6,8 @@
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -41,6 +43,7 @@ public class TransportReconciliationExternal extends TenantEntity {
private BigDecimal freightAmount;
private String feeItemsJson;
private BigDecimal settlementAmount;
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private Boolean suspectedDuplicate;
private String matchStatus;
private Long matchedInternalId;
@@ -403,10 +403,19 @@ public final class MkProcessBizHandlers {
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
if ("reviewing".equals(entity.getApprovalStatus())) {
PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest();
request.setId(id);
service.returnBill(request);
}
PaymentApplication updated = service.getById(id);
if (updated == null) {
return false;
}
updated.setApprovalStatus("returned");
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(updated);
}
}
@@ -314,6 +314,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo));
fillDetailWaybillAddresses(details);
fillDetailCargoNames(details);
vo.setDetails(details);
vo.setSummaryFees(listSummaryFees(id));
vo.setPayments(paymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
@@ -1663,6 +1664,117 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.set(ReceivablePayableDetail::getSettlementStatus, "formal_settled"));
if (affected != 1) throw new ServiceException("单据" + source.getDocumentNo() + "已被其他结算单选择");
}
persistDetailCargoNames(settlement.getId());
}
/**
* 一单多货时运单主表只保留首行货物。结算明细按运单货物清单回填全部货物名称、货物类型。
*/
private void fillDetailCargoNames(List<FormalSettlementDetail> details) {
if (details == null || details.isEmpty()) {
return;
}
List<Long> waybillIds = details.stream().map(FormalSettlementDetail::getWaybillId)
.filter(Objects::nonNull).distinct().toList();
Map<Long, Waybill> waybillMap = waybillIds.isEmpty() ? Map.of() : waybillService.listByIds(waybillIds).stream()
.collect(Collectors.toMap(Waybill::getId, Function.identity(), (left, right) -> left));
List<Long> detailIds = details.stream().map(FormalSettlementDetail::getId).filter(Objects::nonNull).toList();
Map<Long, List<FormalSettlementDetailFee>> feesByDetail = detailIds.isEmpty() ? Map.of()
: detailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)
.orderByAsc(FormalSettlementDetailFee::getLineNo)).stream()
.collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId, LinkedHashMap::new, Collectors.toList()));
for (FormalSettlementDetail detail : details) {
Waybill waybill = waybillMap.get(detail.getWaybillId());
LinkedHashSet<String> goodsNames = new LinkedHashSet<>();
LinkedHashSet<String> goodsTypes = new LinkedHashSet<>();
if (waybill != null) {
collectGoodsCargo(waybill.getGoodsJson(), goodsNames, goodsTypes);
}
LinkedHashSet<String> feeNames = new LinkedHashSet<>();
LinkedHashSet<String> feeTypes = new LinkedHashSet<>();
for (FormalSettlementDetailFee fee : feesByDetail.getOrDefault(detail.getId(), List.of())) {
addCargoToken(feeNames, fee.getCargoName(), true);
addCargoToken(feeTypes, fee.getCargoType(), true);
}
LinkedHashSet<String> names = goodsNames.size() > 1 ? goodsNames : feeNames.size() > 1 ? feeNames : goodsNames;
LinkedHashSet<String> types = goodsTypes.size() > 1 ? goodsTypes : feeTypes.size() > 1 ? feeTypes : goodsTypes;
if (names.isEmpty()) {
addCargoToken(names, detail.getCargoName(), true);
}
if (types.isEmpty()) {
addCargoToken(types, detail.getCargoType(), true);
}
if (names.size() > 1) {
detail.setCargoName(joinCargo(names));
}
if (types.size() > 1) {
detail.setCargoType(joinCargo(types));
}
}
}
private void persistDetailCargoNames(Long settlementId) {
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, settlementId)
.orderByAsc(FormalSettlementDetail::getLineNo));
Map<Long, String> cargoNames = new LinkedHashMap<>();
Map<Long, String> cargoTypes = new LinkedHashMap<>();
for (FormalSettlementDetail detail : details) {
cargoNames.put(detail.getId(), detail.getCargoName());
cargoTypes.put(detail.getId(), detail.getCargoType());
}
fillDetailCargoNames(details);
for (FormalSettlementDetail detail : details) {
if (Objects.equals(cargoNames.get(detail.getId()), detail.getCargoName())
&& Objects.equals(cargoTypes.get(detail.getId()), detail.getCargoType())) {
continue;
}
detailMapper.update(null, Wrappers.<FormalSettlementDetail>lambdaUpdate()
.eq(FormalSettlementDetail::getId, detail.getId())
.set(FormalSettlementDetail::getCargoName, detail.getCargoName())
.set(FormalSettlementDetail::getCargoType, detail.getCargoType()));
}
}
private void collectGoodsCargo(String goodsJson, Set<String> names, Set<String> types) {
if (Func.isEmpty(goodsJson)) {
return;
}
try {
Object parsed = JsonUtil.parse(goodsJson, List.class);
if (!(parsed instanceof List<?> rows)) {
return;
}
for (Object row : rows) {
if (!(row instanceof Map<?, ?> goods)) {
continue;
}
addCargoToken(names, goods.get("cargoName") == null ? null : String.valueOf(goods.get("cargoName")), false);
addCargoToken(types, goods.get("cargoType") == null ? null : String.valueOf(goods.get("cargoType")), false);
}
} catch (RuntimeException ignored) {
// 货物清单异常时保留明细上已有的货物名称。
}
}
private void addCargoToken(Set<String> target, String raw, boolean splitJoined) {
if (Func.isEmpty(raw)) {
return;
}
String[] parts = splitJoined ? raw.split("[,,]") : new String[]{raw};
for (String part : parts) {
String token = part.trim();
if (token.isEmpty() || "手工调差".equals(token)) {
continue;
}
target.add(token);
}
}
private String joinCargo(Set<String> values) {
String joined = String.join(",", values);
return joined.length() > 500 ? joined.substring(0, 500) : joined;
}
private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) {
@@ -383,6 +383,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
fillBillLedger(entity, request);
validateQuota(entity);
saveOrUpdate(entity);
// 汇票付款在生成申请时即占用可用余额;编辑时按差额调整,避免重复扣减
syncBillOccupation(entity);
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
.eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId()));
int lineNo = 1;
@@ -419,7 +421,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
@Override @Transactional(rollbackFor = Exception.class)
public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.<PaymentApplicationRecord>lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); removeById(entity); }
public void removeDraft(Long id) {
PaymentApplication entity = editable(id);
if (isBillPayment(entity.getPaymentMethod())) {
releaseBillBalanceIfOccupied(entity);
}
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id));
recordMapper.delete(Wrappers.<PaymentApplicationRecord>lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id));
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id));
removeById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submit(PaymentApplicationStatusRequest request) {
@@ -434,13 +445,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
validateProjectAdvanceContract(entity);
validateSelectedBill(entity, false);
validateQuota(entity);
// 驳回后可用余额已退回,再次提交时重新占用
if (isBillPayment(entity.getPaymentMethod()) && activeBillUsage(entity.getId()) == null) {
syncBillOccupation(entity);
}
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
entity.setCurrentNode("财务审核");
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
refreshFormalSettlement(entity);
}
@Override public void returnBill(PaymentApplicationStatusRequest request) { change(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.RETURNED, "已驳回", request.getReason()); }
@Override
@Transactional(rollbackFor = Exception.class)
public void returnBill(PaymentApplicationStatusRequest request) {
PaymentApplication entity = lockedPayment(request.getId());
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不允许此操作");
}
if (isBillPayment(entity.getPaymentMethod())) {
releaseBillBalanceIfOccupied(entity);
}
entity.setApprovalStatus(TransportBusiConstant.RETURNED);
entity.setCurrentNode("已驳回");
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200));
updateById(entity);
refreshFormalSettlement(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void voidBill(PaymentApplicationStatusRequest request) {
@@ -464,7 +495,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
public void approve(PaymentApplicationStatusRequest request) {
PaymentApplication entity = lockedPayment(request.getId());
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批中的付款申请允许审核");
if (isBillPayment(entity.getPaymentMethod())) useBillBalance(entity);
// 生成申请时已扣减;仅历史未占用单据在审批通过时补扣,避免重复扣减
if (isBillPayment(entity.getPaymentMethod()) && activeBillUsage(entity.getId()) == null) {
useBillBalance(entity);
}
entity.setApprovalStatus(TransportBusiConstant.APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
refreshFormalSettlement(entity);
@@ -2016,9 +2050,17 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private BigDecimal sumApplied(LambdaQueryWrapper<PaymentApplication> wrapper) {
return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
.filter(item -> !releasedBillPayment(item))
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
}
/** 汇票付款被驳回后不再占用结算单可付款金额,删除草稿由逻辑删除自然释放。 */
private boolean releasedBillPayment(PaymentApplication payment) {
return payment != null
&& isBillPayment(payment.getPaymentMethod())
&& TransportBusiConstant.RETURNED.equals(payment.getApprovalStatus());
}
private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) {
if ("progress_advance".equals(paymentType)) {
return cumulativePreSettlementApplied(referenceId, excludeId);
@@ -2053,7 +2095,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
continue;
}
coveredPaymentIds.add(payment.getId());
if (Objects.equals(payment.getId(), excludeId) || TransportBusiConstant.VOIDED.equals(payment.getApprovalStatus())) {
if (Objects.equals(payment.getId(), excludeId) || TransportBusiConstant.VOIDED.equals(payment.getApprovalStatus())
|| releasedBillPayment(payment)) {
continue;
}
relationAmount = relationAmount.add(money(relation.getAppliedAmount()));
@@ -2064,6 +2107,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.ne(excludeId != null, PaymentApplication::getId, excludeId)
.notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)
.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
.filter(item -> !releasedBillPayment(item))
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
return relationAmount.add(legacyAmount);
}
@@ -2093,7 +2137,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return;
}
BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
entity.setBillLedgerId(ledger.getId());
entity.setBillNo(ledger.getBillNo());
}
@@ -2101,37 +2145,127 @@ 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);
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
}
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity) {
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity, Long paymentApplicationId) {
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
throw new ServiceException("所选汇票已到期");
}
if (money(amount).compareTo(money(ledger.getAvailableBalance())) > 0) 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("当前使用部门不在汇票可用部门范围内");
}
private void useBillBalance(PaymentApplication entity) {
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE"));
if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录");
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null);
BillLedgerUsage exists = lockedBillUsage(entity.getId());
if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) {
throw new ServiceException("该付款申请已生成汇票使用记录");
}
BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount));
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount).setScale(2, RoundingMode.HALF_UP));
billLedgerMapper.updateById(ledger);
if (exists == null) {
BillLedgerUsage usage = new BillLedgerUsage();
usage.setBillLedgerId(ledger.getId());
usage.setPaymentApplicationId(entity.getId());
usage.setApplicationNo(entity.getPaymentNo());
usage.setUsedAmount(amount);
usage.setUseDeptId(entity.getDeptId());
usage.setUseDeptName(entity.getDeptName());
usage.setUsageStatus(TransportBusiConstant.APPROVED);
usage.setStatus(1);
billLedgerUsageMapper.insert(usage);
return;
}
exists.setBillLedgerId(ledger.getId());
exists.setApplicationNo(entity.getPaymentNo());
exists.setUsedAmount(amount);
exists.setUseDeptId(entity.getDeptId());
exists.setUseDeptName(entity.getDeptName());
exists.setUsageStatus(TransportBusiConstant.APPROVED);
billLedgerUsageMapper.updateById(exists);
}
/**
* 汇票付款申请生成后占用汇票可用余额。同一申请重复保存时只调整差额;
* 更换汇票或改为非汇票付款时,先退回原占用。
*/
private void syncBillOccupation(PaymentApplication entity) {
BillLedgerUsage usage = lockedBillUsage(entity.getId());
boolean occupy = isBillPayment(entity.getPaymentMethod()) && entity.getBillLedgerId() != null;
if (!occupy) {
if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) {
releaseUsage(usage);
}
return;
}
BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())
&& Objects.equals(usage.getBillLedgerId(), entity.getBillLedgerId())) {
adjustOccupiedAmount(usage, entity, amount);
return;
}
if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) {
releaseUsage(usage);
}
useBillBalance(entity);
}
private void adjustOccupiedAmount(BillLedgerUsage usage, PaymentApplication entity, BigDecimal amount) {
BigDecimal delta = amount.subtract(money(usage.getUsedAmount())).setScale(2, RoundingMode.HALF_UP);
BillLedger ledger = lockedBill(usage.getBillLedgerId());
if (delta.compareTo(BigDecimal.ZERO) > 0 && delta.compareTo(money(ledger.getAvailableBalance())) > 0) {
throw new ServiceException("申请付款金额超过汇票可用余额");
}
BigDecimal available = money(ledger.getAvailableBalance()).subtract(delta).setScale(2, RoundingMode.HALF_UP);
if (available.compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("申请付款金额超过汇票可用余额");
}
BigDecimal faceAmount = money(ledger.getFaceAmount());
if (available.compareTo(faceAmount) > 0) {
available = faceAmount;
}
ledger.setAvailableBalance(available);
billLedgerMapper.updateById(ledger);
BillLedgerUsage usage = new BillLedgerUsage();
usage.setBillLedgerId(ledger.getId());
usage.setPaymentApplicationId(entity.getId());
usage.setApplicationNo(entity.getPaymentNo());
usage.setUsedAmount(amount);
usage.setUseDeptId(entity.getDeptId());
usage.setUseDeptName(entity.getDeptName());
usage.setUsageStatus(TransportBusiConstant.APPROVED);
usage.setStatus(1);
billLedgerUsageMapper.insert(usage);
billLedgerUsageMapper.updateById(usage);
}
private BigDecimal occupiedAmount(Long paymentApplicationId, Long billLedgerId) {
if (paymentApplicationId == null || billLedgerId == null) {
return BigDecimal.ZERO;
}
BillLedgerUsage usage = activeBillUsage(paymentApplicationId);
if (usage == null || !Objects.equals(usage.getBillLedgerId(), billLedgerId)) {
return BigDecimal.ZERO;
}
return money(usage.getUsedAmount());
}
private BillLedgerUsage activeBillUsage(Long paymentApplicationId) {
if (paymentApplicationId == null) {
return null;
}
return billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId)
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED)
.last("limit 1"));
}
private BillLedgerUsage lockedBillUsage(Long paymentApplicationId) {
if (paymentApplicationId == null) {
return null;
}
return billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId)
.last("limit 1 FOR UPDATE"));
}
private void releaseBillBalance(PaymentApplication entity) {
@@ -2139,9 +2273,23 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE"));
if (usage == null) throw new ServiceException("未找到对应的汇票使用记录,无法作废");
releaseUsage(usage);
}
private void releaseBillBalanceIfOccupied(PaymentApplication entity) {
BillLedgerUsage usage = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE"));
if (usage == null) {
return;
}
releaseUsage(usage);
}
private void releaseUsage(BillLedgerUsage usage) {
BillLedger ledger = lockedBill(usage.getBillLedgerId());
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount()))
.min(money(ledger.getFaceAmount())));
.min(money(ledger.getFaceAmount())).setScale(2, RoundingMode.HALF_UP));
billLedgerMapper.updateById(ledger);
usage.setUsageStatus("released");
billLedgerUsageMapper.updateById(usage);
@@ -836,6 +836,12 @@ public class ReceivablePayableDetailServiceImpl
if (existsByWaybill(waybill.getId(), targetSettlementType)) {
continue;
}
if ("payable".equals(targetSettlementType)
&& trimToNull(waybill.getCarrierName()) != null
&& !matchesWaybillCarrier(contract, waybill)) {
throw new ServiceException("运单【" + Func.toStr(waybill.getWaybillNo(), String.valueOf(waybill.getId()))
+ "】的承运商不是所选承运商合同的乙方,不能使用项目内其他承运商合同生成应付");
}
ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), targetSettlementType);
detail.setSourceType(SOURCE_MANUAL_GENERATION);
save(detail);
@@ -1064,11 +1070,20 @@ public class ReceivablePayableDetailServiceImpl
List<Waybill> payableWaybills = waybills.stream()
.filter(waybill -> !isSelfTransport(waybill)).toList();
if (payableWaybills.isEmpty()) return;
List<Waybill> carrierMatchedWaybills = payableWaybills.stream()
.filter(waybill -> matchesWaybillCarrier(carrierContract, waybill)).toList();
List<Waybill> otherCarrierWaybills = payableWaybills.stream()
.filter(waybill -> !matchesWaybillCarrier(carrierContract, waybill)).toList();
if (!otherCarrierWaybills.isEmpty()) {
generateAutomaticWaybillDetails(otherCarrierWaybills, false,
this::resolveCarrierContract, currentUserId, generateTime);
}
if (carrierMatchedWaybills.isEmpty()) return;
generateAutomaticDetail(carrierContract, "payable",
() -> existsByLoading(loadingNo, "payable"), payableWaybills,
() -> existsByLoading(loadingNo, "payable"), carrierMatchedWaybills,
(fees, unitPrice) -> {
normalizeWaybillFeeLines(fees);
return buildLoadingDetail(loadingNo, carrierContract, payableWaybills, fees, unitPrice);
return buildLoadingDetail(loadingNo, carrierContract, carrierMatchedWaybills, fees, unitPrice);
},
false, currentUserId, generateTime);
}
@@ -1118,22 +1133,54 @@ public class ReceivablePayableDetailServiceImpl
private ContractManage resolveWaybillCarrierContract(Waybill waybill) {
if (Func.isNotEmpty(waybill.getLoadingNo())) return null;
return resolveCarrierContract(waybill);
}
/**
* 应付只认运单选择的承运商。
* 运单已记录承运商合同时,乙方必须就是该承运商;否则按承运商名称回查同一项目下的承运商合同。
* 不允许落到项目里其他承运商合同的客商。
*/
private ContractManage resolveCarrierContract(Waybill waybill) {
if (waybill == null || isSelfTransport(waybill)) {
return null;
}
String carrierName = trimToNull(waybill.getCarrierName());
if (Func.isNotEmpty(waybill.getCarrierContractId())) {
ContractManage contract = contractManageService.getById(waybill.getCarrierContractId());
if (contract == null || !"承运商合同".equals(contract.getContractCategory())) {
ContractManage recorded = contractManageService.getById(waybill.getCarrierContractId());
if (recorded == null || !"承运商合同".equals(recorded.getContractCategory())) {
throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确");
}
return contract;
if (carrierName == null || matchesWaybillCarrier(recorded, waybill)) {
return recorded;
}
}
return findCarrierContract(waybill);
ContractManage matched = findCarrierContract(waybill);
if (matched != null && matchesWaybillCarrier(matched, waybill)) {
return matched;
}
return null;
}
private boolean matchesWaybillCarrier(ContractManage contract, Waybill waybill) {
if (contract == null || waybill == null || !"承运商合同".equals(contract.getContractCategory())) {
return false;
}
if (waybill.getProjectId() != null && contract.getProjectId() != null
&& !Objects.equals(waybill.getProjectId(), contract.getProjectId())) {
return false;
}
String carrierName = trimToNull(waybill.getCarrierName());
String partyB = trimToNull(contract.getPartyB());
return carrierName != null && carrierName.equals(partyB);
}
private ContractManage findCarrierContract(Waybill waybill) {
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId())
|| Func.isEmpty(waybill.getCarrierName())) {
String carrierName = trimToNull(waybill.getCarrierName());
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())) {
return null;
@@ -1142,13 +1189,38 @@ public class ReceivablePayableDetailServiceImpl
.eq(ContractManage::getIsDeleted, 0)
.eq(ContractManage::getProjectId, waybill.getProjectId())
.eq(ContractManage::getPartyA, customerContract.getPartyA())
.eq(ContractManage::getPartyB, waybill.getCarrierName())
.eq(ContractManage::getPartyB, carrierName)
.eq(ContractManage::getContractCategory, "承运商合同")
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
.or().ne(ContractManage::getContractStage, "terminated"))
.orderByDesc(ContractManage::getCreateTime), false);
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
/**
* 应付客商取运单选择的承运商,避免写成项目内其他承运商合同的乙方。
*/
private String resolveCounterpartyName(Waybill waybill, ContractManage contract, String settlementType) {
if ("payable".equals(settlementType)) {
String carrierName = trimToNull(waybill == null ? null : waybill.getCarrierName());
if (carrierName != null) {
return carrierName;
}
return contract == null ? null : contract.getPartyB();
}
if (contract != null && Func.isNotEmpty(contract.getPartyA())) {
return contract.getPartyA();
}
return waybill == null ? null : waybill.getCustomerName();
}
@Override
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
@@ -1201,12 +1273,24 @@ public class ReceivablePayableDetailServiceImpl
.collect(Collectors.toMap(ContractManage::getId, contract -> contract));
Map<Long, List<Waybill>> carrierWaybills = waybills.stream().collect(Collectors.groupingBy(
Waybill::getCarrierContractId, LinkedHashMap::new, Collectors.toList()));
List<Waybill> mismatchedCarrierWaybills = waybills.stream()
.filter(waybill -> !matchesWaybillCarrier(carrierContracts.get(waybill.getCarrierContractId()), waybill))
.toList();
if (!mismatchedCarrierWaybills.isEmpty()) {
generateAutomaticWaybillDetails(mismatchedCarrierWaybills, false,
this::resolveCarrierContract, currentUserId, generateTime);
}
for (Map.Entry<Long, List<Waybill>> entry : carrierWaybills.entrySet()) {
ContractManage carrierContract = carrierContracts.get(entry.getKey());
if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) {
throw new ServiceException("总单【" + masterOrder.getMasterNo() + "】记录的承运商合同不存在或合同类别不正确");
}
List<Waybill> currentCarrierWaybills = entry.getValue();
List<Waybill> currentCarrierWaybills = entry.getValue().stream()
.filter(waybill -> matchesWaybillCarrier(carrierContract, waybill))
.toList();
if (currentCarrierWaybills.isEmpty()) {
continue;
}
generateAutomaticDetail(carrierContract, "payable",
() -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()),
currentCarrierWaybills,
@@ -1635,9 +1719,7 @@ 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(contract == null
? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName())
: ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA()));
detail.setCustomerName(resolveCounterpartyName(waybill, contract, settlementType));
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
detail.setContractNo(contract == null ? null : contract.getContractNo());
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
@@ -1652,8 +1734,8 @@ public class ReceivablePayableDetailServiceImpl
detail.setArrivalContact(waybill.getArrivalContact());
detail.setArrivalPhone(waybill.getArrivalPhone());
detail.setTransportType(waybill.getTransportType());
detail.setCargoName(waybill.getCargoName());
detail.setCargoType(waybill.getCargoType());
detail.setCargoName(joinDistinctGoods(waybill, "cargoName", waybill.getCargoName()));
detail.setCargoType(joinDistinctGoods(waybill, "cargoType", waybill.getCargoType()));
detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill));
detail.setQuantityUnit(waybill.getQuantityUnit());
detail.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
@@ -1668,6 +1750,21 @@ public class ReceivablePayableDetailServiceImpl
return detail;
}
/** 一单多货时主表只保留首行货物,应付明细改记全部货物名称、货物类型。 */
private String joinDistinctGoods(Waybill waybill, String field, String fallback) {
LinkedHashSet<String> values = new LinkedHashSet<>();
for (Map<String, Object> goods : parseList(waybill.getGoodsJson())) {
String value = stringValue(goods, field, "");
if (Func.isNotEmpty(value)) {
values.add(value.trim());
}
}
if (values.isEmpty()) {
return fallback;
}
return String.join(",", values);
}
/**
* 多货物运单优先按 goodsJson 数量合计作为运输总量,避免主表 quantity 只落了首行。
*/
@@ -1233,14 +1233,67 @@ public class TransportReconciliationServiceImpl
private List<TransportReconciliationInternal> assertAllMatched(TransportReconciliation bill) {
List<TransportReconciliationInternal> internals = internalRows(bill.getId());
List<TransportReconciliationExternal> externals = externalRows(bill.getId());
if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size()
|| internals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchResult()))
|| externals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchStatus()) || Boolean.TRUE.equals(item.getSuspectedDuplicate()))) {
// 调整账单后旧的疑似重复标记可能已失效,按当前内容重算,避免已一一匹配仍被拦截
syncSuspectedDuplicates(externals);
if (!oneToOneMatched(internals, externals)) {
throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复");
}
return internals;
}
private void syncSuspectedDuplicates(List<TransportReconciliationExternal> externals) {
Map<String, List<TransportReconciliationExternal>> groups = externals.stream()
.collect(Collectors.groupingBy(this::duplicateKey));
Set<Long> duplicateIds = new HashSet<>();
for (List<TransportReconciliationExternal> group : groups.values()) {
if (group.size() > 1) {
group.stream().map(TransportReconciliationExternal::getId).forEach(duplicateIds::add);
}
}
for (TransportReconciliationExternal external : externals) {
boolean duplicate = duplicateIds.contains(external.getId());
String matchStatus = external.getMatchStatus();
if (!duplicate && DUPLICATE.equals(matchStatus) && external.getMatchedInternalId() != null) {
matchStatus = MATCHED;
}
if (Boolean.TRUE.equals(external.getSuspectedDuplicate()) == duplicate
&& Objects.equals(matchStatus, external.getMatchStatus())) {
continue;
}
external.setSuspectedDuplicate(duplicate);
external.setMatchStatus(matchStatus);
externalMapper.update(null, Wrappers.<TransportReconciliationExternal>lambdaUpdate()
.eq(TransportReconciliationExternal::getId, external.getId())
.set(TransportReconciliationExternal::getSuspectedDuplicate, duplicate)
.set(TransportReconciliationExternal::getMatchStatus, matchStatus));
}
}
private boolean oneToOneMatched(List<TransportReconciliationInternal> internals,
List<TransportReconciliationExternal> externals) {
if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size()) {
return false;
}
if (externals.stream().anyMatch(item -> Boolean.TRUE.equals(item.getSuspectedDuplicate()))) {
return false;
}
Map<Long, TransportReconciliationExternal> externalMap = externals.stream()
.collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity(), (left, right) -> left));
Set<Long> usedExternalIds = new HashSet<>();
for (TransportReconciliationInternal internal : internals) {
if (!MATCHED.equals(internal.getMatchResult()) || internal.getMatchedExternalId() == null
|| !usedExternalIds.add(internal.getMatchedExternalId())) {
return false;
}
TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId());
if (external == null || !MATCHED.equals(external.getMatchStatus())
|| !Objects.equals(external.getMatchedInternalId(), internal.getId())) {
return false;
}
}
return usedExternalIds.size() == externals.size();
}
private void refreshStats(Long id) {
TransportReconciliation bill = existing(id);
List<TransportReconciliationInternal> internals = internalRows(id);