🐛 修复应付承运商、汇票占用、一单多货结算与运输对账完成校验
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
@@ -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;
|
||||
|
||||
+13
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+112
@@ -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) {
|
||||
|
||||
+168
-20
@@ -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);
|
||||
|
||||
+113
-16
@@ -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 只落了首行。
|
||||
*/
|
||||
|
||||
+56
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user