🐛 修复应付承运商、汇票占用、一单多货结算与运输对账完成校验
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
@@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.transport.pojo.entity;
|
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 com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -41,6 +43,7 @@ public class TransportReconciliationExternal extends TenantEntity {
|
|||||||
private BigDecimal freightAmount;
|
private BigDecimal freightAmount;
|
||||||
private String feeItemsJson;
|
private String feeItemsJson;
|
||||||
private BigDecimal settlementAmount;
|
private BigDecimal settlementAmount;
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private Boolean suspectedDuplicate;
|
private Boolean suspectedDuplicate;
|
||||||
private String matchStatus;
|
private String matchStatus;
|
||||||
private Long matchedInternalId;
|
private Long matchedInternalId;
|
||||||
|
|||||||
+13
-4
@@ -403,10 +403,19 @@ public final class MkProcessBizHandlers {
|
|||||||
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||||
return entity != null;
|
return entity != null;
|
||||||
}
|
}
|
||||||
entity.setApprovalStatus("returned");
|
if ("reviewing".equals(entity.getApprovalStatus())) {
|
||||||
entity.setCurrentNode("审核不通过");
|
PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest();
|
||||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
request.setId(id);
|
||||||
return service.updateById(entity);
|
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()
|
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||||
.eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo));
|
.eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo));
|
||||||
fillDetailWaybillAddresses(details);
|
fillDetailWaybillAddresses(details);
|
||||||
|
fillDetailCargoNames(details);
|
||||||
vo.setDetails(details);
|
vo.setDetails(details);
|
||||||
vo.setSummaryFees(listSummaryFees(id));
|
vo.setSummaryFees(listSummaryFees(id));
|
||||||
vo.setPayments(paymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
vo.setPayments(paymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||||
@@ -1663,6 +1664,117 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
|||||||
.set(ReceivablePayableDetail::getSettlementStatus, "formal_settled"));
|
.set(ReceivablePayableDetail::getSettlementStatus, "formal_settled"));
|
||||||
if (affected != 1) throw new ServiceException("单据" + source.getDocumentNo() + "已被其他结算单选择");
|
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) {
|
private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) {
|
||||||
|
|||||||
+168
-20
@@ -383,6 +383,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
fillBillLedger(entity, request);
|
fillBillLedger(entity, request);
|
||||||
validateQuota(entity);
|
validateQuota(entity);
|
||||||
saveOrUpdate(entity);
|
saveOrUpdate(entity);
|
||||||
|
// 汇票付款在生成申请时即占用可用余额;编辑时按差额调整,避免重复扣减
|
||||||
|
syncBillOccupation(entity);
|
||||||
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
|
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
|
||||||
.eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId()));
|
.eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId()));
|
||||||
int lineNo = 1;
|
int lineNo = 1;
|
||||||
@@ -419,7 +421,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override @Transactional(rollbackFor = Exception.class)
|
@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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void submit(PaymentApplicationStatusRequest request) {
|
public void submit(PaymentApplicationStatusRequest request) {
|
||||||
@@ -434,13 +445,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
validateProjectAdvanceContract(entity);
|
validateProjectAdvanceContract(entity);
|
||||||
validateSelectedBill(entity, false);
|
validateSelectedBill(entity, false);
|
||||||
validateQuota(entity);
|
validateQuota(entity);
|
||||||
|
// 驳回后可用余额已退回,再次提交时重新占用
|
||||||
|
if (isBillPayment(entity.getPaymentMethod()) && activeBillUsage(entity.getId()) == null) {
|
||||||
|
syncBillOccupation(entity);
|
||||||
|
}
|
||||||
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
|
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
|
||||||
entity.setCurrentNode("财务审核");
|
entity.setCurrentNode("财务审核");
|
||||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
refreshFormalSettlement(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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void voidBill(PaymentApplicationStatusRequest request) {
|
public void voidBill(PaymentApplicationStatusRequest request) {
|
||||||
@@ -464,7 +495,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
public void approve(PaymentApplicationStatusRequest request) {
|
public void approve(PaymentApplicationStatusRequest request) {
|
||||||
PaymentApplication entity = lockedPayment(request.getId());
|
PaymentApplication entity = lockedPayment(request.getId());
|
||||||
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批中的付款申请允许审核");
|
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());
|
entity.setApprovalStatus(TransportBusiConstant.APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
refreshFormalSettlement(entity);
|
refreshFormalSettlement(entity);
|
||||||
@@ -2016,9 +2050,17 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
|
|
||||||
private BigDecimal sumApplied(LambdaQueryWrapper<PaymentApplication> wrapper) {
|
private BigDecimal sumApplied(LambdaQueryWrapper<PaymentApplication> wrapper) {
|
||||||
return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
|
return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
|
||||||
|
.filter(item -> !releasedBillPayment(item))
|
||||||
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
.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) {
|
private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) {
|
||||||
if ("progress_advance".equals(paymentType)) {
|
if ("progress_advance".equals(paymentType)) {
|
||||||
return cumulativePreSettlementApplied(referenceId, excludeId);
|
return cumulativePreSettlementApplied(referenceId, excludeId);
|
||||||
@@ -2053,7 +2095,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
coveredPaymentIds.add(payment.getId());
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
relationAmount = relationAmount.add(money(relation.getAppliedAmount()));
|
relationAmount = relationAmount.add(money(relation.getAppliedAmount()));
|
||||||
@@ -2064,6 +2107,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
.ne(excludeId != null, PaymentApplication::getId, excludeId)
|
.ne(excludeId != null, PaymentApplication::getId, excludeId)
|
||||||
.notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)
|
.notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)
|
||||||
.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
|
.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
|
||||||
|
.filter(item -> !releasedBillPayment(item))
|
||||||
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
return relationAmount.add(legacyAmount);
|
return relationAmount.add(legacyAmount);
|
||||||
}
|
}
|
||||||
@@ -2093,7 +2137,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId());
|
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.setBillLedgerId(ledger.getId());
|
||||||
entity.setBillNo(ledger.getBillNo());
|
entity.setBillNo(ledger.getBillNo());
|
||||||
}
|
}
|
||||||
@@ -2101,37 +2145,127 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
private void validateSelectedBill(PaymentApplication entity, boolean locked) {
|
private void validateSelectedBill(PaymentApplication entity, boolean locked) {
|
||||||
if (!isBillPayment(entity.getPaymentMethod())) return;
|
if (!isBillPayment(entity.getPaymentMethod())) return;
|
||||||
BillLedger ledger = locked ? lockedBill(entity.getBillLedgerId()) : billLedgerMapper.selectById(entity.getBillLedgerId());
|
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 (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
|
||||||
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
|
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
|
||||||
throw new ServiceException("所选汇票已到期");
|
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("当前使用部门不在汇票可用部门范围内");
|
if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void useBillBalance(PaymentApplication entity) {
|
private void useBillBalance(PaymentApplication entity) {
|
||||||
BillLedger ledger = lockedBill(entity.getBillLedgerId());
|
BillLedger ledger = lockedBill(entity.getBillLedgerId());
|
||||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
|
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null);
|
||||||
BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
BillLedgerUsage exists = lockedBillUsage(entity.getId());
|
||||||
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE"));
|
if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) {
|
||||||
if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录");
|
throw new ServiceException("该付款申请已生成汇票使用记录");
|
||||||
|
}
|
||||||
BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
|
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);
|
billLedgerMapper.updateById(ledger);
|
||||||
BillLedgerUsage usage = new BillLedgerUsage();
|
|
||||||
usage.setBillLedgerId(ledger.getId());
|
|
||||||
usage.setPaymentApplicationId(entity.getId());
|
|
||||||
usage.setApplicationNo(entity.getPaymentNo());
|
usage.setApplicationNo(entity.getPaymentNo());
|
||||||
usage.setUsedAmount(amount);
|
usage.setUsedAmount(amount);
|
||||||
usage.setUseDeptId(entity.getDeptId());
|
usage.setUseDeptId(entity.getDeptId());
|
||||||
usage.setUseDeptName(entity.getDeptName());
|
usage.setUseDeptName(entity.getDeptName());
|
||||||
usage.setUsageStatus(TransportBusiConstant.APPROVED);
|
billLedgerUsageMapper.updateById(usage);
|
||||||
usage.setStatus(1);
|
}
|
||||||
billLedgerUsageMapper.insert(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) {
|
private void releaseBillBalance(PaymentApplication entity) {
|
||||||
@@ -2139,9 +2273,23 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
|||||||
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
|
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
|
||||||
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE"));
|
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE"));
|
||||||
if (usage == null) throw new ServiceException("未找到对应的汇票使用记录,无法作废");
|
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());
|
BillLedger ledger = lockedBill(usage.getBillLedgerId());
|
||||||
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount()))
|
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);
|
billLedgerMapper.updateById(ledger);
|
||||||
usage.setUsageStatus("released");
|
usage.setUsageStatus("released");
|
||||||
billLedgerUsageMapper.updateById(usage);
|
billLedgerUsageMapper.updateById(usage);
|
||||||
|
|||||||
+113
-16
@@ -836,6 +836,12 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
if (existsByWaybill(waybill.getId(), targetSettlementType)) {
|
if (existsByWaybill(waybill.getId(), targetSettlementType)) {
|
||||||
continue;
|
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);
|
ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), targetSettlementType);
|
||||||
detail.setSourceType(SOURCE_MANUAL_GENERATION);
|
detail.setSourceType(SOURCE_MANUAL_GENERATION);
|
||||||
save(detail);
|
save(detail);
|
||||||
@@ -1064,11 +1070,20 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
List<Waybill> payableWaybills = waybills.stream()
|
List<Waybill> payableWaybills = waybills.stream()
|
||||||
.filter(waybill -> !isSelfTransport(waybill)).toList();
|
.filter(waybill -> !isSelfTransport(waybill)).toList();
|
||||||
if (payableWaybills.isEmpty()) return;
|
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",
|
generateAutomaticDetail(carrierContract, "payable",
|
||||||
() -> existsByLoading(loadingNo, "payable"), payableWaybills,
|
() -> existsByLoading(loadingNo, "payable"), carrierMatchedWaybills,
|
||||||
(fees, unitPrice) -> {
|
(fees, unitPrice) -> {
|
||||||
normalizeWaybillFeeLines(fees);
|
normalizeWaybillFeeLines(fees);
|
||||||
return buildLoadingDetail(loadingNo, carrierContract, payableWaybills, fees, unitPrice);
|
return buildLoadingDetail(loadingNo, carrierContract, carrierMatchedWaybills, fees, unitPrice);
|
||||||
},
|
},
|
||||||
false, currentUserId, generateTime);
|
false, currentUserId, generateTime);
|
||||||
}
|
}
|
||||||
@@ -1118,22 +1133,54 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
|
|
||||||
private ContractManage resolveWaybillCarrierContract(Waybill waybill) {
|
private ContractManage resolveWaybillCarrierContract(Waybill waybill) {
|
||||||
if (Func.isNotEmpty(waybill.getLoadingNo())) return null;
|
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())) {
|
if (Func.isNotEmpty(waybill.getCarrierContractId())) {
|
||||||
ContractManage contract = contractManageService.getById(waybill.getCarrierContractId());
|
ContractManage recorded = contractManageService.getById(waybill.getCarrierContractId());
|
||||||
if (contract == null || !"承运商合同".equals(contract.getContractCategory())) {
|
if (recorded == null || !"承运商合同".equals(recorded.getContractCategory())) {
|
||||||
throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确");
|
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) {
|
private ContractManage findCarrierContract(Waybill waybill) {
|
||||||
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId())
|
String carrierName = trimToNull(waybill.getCarrierName());
|
||||||
|| Func.isEmpty(waybill.getCarrierName())) {
|
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。
|
// 承运商合同甲方应与运单绑定的客户合同甲方一致,乙方必须是运单选择的承运商。
|
||||||
ContractManage customerContract = contractManageService.getById(waybill.getContractId());
|
ContractManage customerContract = contractManageService.getById(waybill.getContractId());
|
||||||
if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) {
|
if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1142,13 +1189,38 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
.eq(ContractManage::getIsDeleted, 0)
|
.eq(ContractManage::getIsDeleted, 0)
|
||||||
.eq(ContractManage::getProjectId, waybill.getProjectId())
|
.eq(ContractManage::getProjectId, waybill.getProjectId())
|
||||||
.eq(ContractManage::getPartyA, customerContract.getPartyA())
|
.eq(ContractManage::getPartyA, customerContract.getPartyA())
|
||||||
.eq(ContractManage::getPartyB, waybill.getCarrierName())
|
.eq(ContractManage::getPartyB, carrierName)
|
||||||
.eq(ContractManage::getContractCategory, "承运商合同")
|
.eq(ContractManage::getContractCategory, "承运商合同")
|
||||||
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
|
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
|
||||||
.or().ne(ContractManage::getContractStage, "terminated"))
|
.or().ne(ContractManage::getContractStage, "terminated"))
|
||||||
.orderByDesc(ContractManage::getCreateTime), false);
|
.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
|
@Override
|
||||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||||
rollbackFor = Exception.class)
|
rollbackFor = Exception.class)
|
||||||
@@ -1201,12 +1273,24 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
.collect(Collectors.toMap(ContractManage::getId, contract -> contract));
|
.collect(Collectors.toMap(ContractManage::getId, contract -> contract));
|
||||||
Map<Long, List<Waybill>> carrierWaybills = waybills.stream().collect(Collectors.groupingBy(
|
Map<Long, List<Waybill>> carrierWaybills = waybills.stream().collect(Collectors.groupingBy(
|
||||||
Waybill::getCarrierContractId, LinkedHashMap::new, Collectors.toList()));
|
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()) {
|
for (Map.Entry<Long, List<Waybill>> entry : carrierWaybills.entrySet()) {
|
||||||
ContractManage carrierContract = carrierContracts.get(entry.getKey());
|
ContractManage carrierContract = carrierContracts.get(entry.getKey());
|
||||||
if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) {
|
if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) {
|
||||||
throw new ServiceException("总单【" + masterOrder.getMasterNo() + "】记录的承运商合同不存在或合同类别不正确");
|
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",
|
generateAutomaticDetail(carrierContract, "payable",
|
||||||
() -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()),
|
() -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()),
|
||||||
currentCarrierWaybills,
|
currentCarrierWaybills,
|
||||||
@@ -1635,9 +1719,7 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId());
|
detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId());
|
||||||
detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName());
|
detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName());
|
||||||
detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate());
|
detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate());
|
||||||
detail.setCustomerName(contract == null
|
detail.setCustomerName(resolveCounterpartyName(waybill, contract, settlementType));
|
||||||
? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName())
|
|
||||||
: ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA()));
|
|
||||||
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
|
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
|
||||||
detail.setContractNo(contract == null ? null : contract.getContractNo());
|
detail.setContractNo(contract == null ? null : contract.getContractNo());
|
||||||
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
|
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
|
||||||
@@ -1652,8 +1734,8 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
detail.setArrivalContact(waybill.getArrivalContact());
|
detail.setArrivalContact(waybill.getArrivalContact());
|
||||||
detail.setArrivalPhone(waybill.getArrivalPhone());
|
detail.setArrivalPhone(waybill.getArrivalPhone());
|
||||||
detail.setTransportType(waybill.getTransportType());
|
detail.setTransportType(waybill.getTransportType());
|
||||||
detail.setCargoName(waybill.getCargoName());
|
detail.setCargoName(joinDistinctGoods(waybill, "cargoName", waybill.getCargoName()));
|
||||||
detail.setCargoType(waybill.getCargoType());
|
detail.setCargoType(joinDistinctGoods(waybill, "cargoType", waybill.getCargoType()));
|
||||||
detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill));
|
detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill));
|
||||||
detail.setQuantityUnit(waybill.getQuantityUnit());
|
detail.setQuantityUnit(waybill.getQuantityUnit());
|
||||||
detail.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
|
detail.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
|
||||||
@@ -1668,6 +1750,21 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
return detail;
|
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 只落了首行。
|
* 多货物运单优先按 goodsJson 数量合计作为运输总量,避免主表 quantity 只落了首行。
|
||||||
*/
|
*/
|
||||||
|
|||||||
+56
-3
@@ -1233,14 +1233,67 @@ public class TransportReconciliationServiceImpl
|
|||||||
private List<TransportReconciliationInternal> assertAllMatched(TransportReconciliation bill) {
|
private List<TransportReconciliationInternal> assertAllMatched(TransportReconciliation bill) {
|
||||||
List<TransportReconciliationInternal> internals = internalRows(bill.getId());
|
List<TransportReconciliationInternal> internals = internalRows(bill.getId());
|
||||||
List<TransportReconciliationExternal> externals = externalRows(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()))
|
syncSuspectedDuplicates(externals);
|
||||||
|| externals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchStatus()) || Boolean.TRUE.equals(item.getSuspectedDuplicate()))) {
|
if (!oneToOneMatched(internals, externals)) {
|
||||||
throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复");
|
throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复");
|
||||||
}
|
}
|
||||||
return internals;
|
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) {
|
private void refreshStats(Long id) {
|
||||||
TransportReconciliation bill = existing(id);
|
TransportReconciliation bill = existing(id);
|
||||||
List<TransportReconciliationInternal> internals = internalRows(id);
|
List<TransportReconciliationInternal> internals = internalRows(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user