diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index 03210df..73ba999 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -20,6 +20,7 @@ import java.util.List; public class FormalSettlementSaveRequest implements Serializable { @Serial private static final long serialVersionUID = 1L; private Long id; + private String formalSettlementNo; private Long contractId; private String settlementType; private List sourcePreSettlementIds; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java index a963aef..d192c02 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java @@ -180,7 +180,15 @@ public class TransportReconciliationController extends BladeController { @PostMapping("/update-by-match") @ApiOperationSupport(order = 13) @Operation(summary = "按匹配结果更新账单") - public R updateByMatch(@RequestParam Long id) { reconciliationService.updateByMatch(id); return R.success("账单更新完成"); } + public R updateByMatch(@RequestParam Long id, + @RequestBody(required = false) TransportReconciliationVO request) { + if (request == null) { + request = new TransportReconciliationVO(); + } + request.setId(id); + reconciliationService.updateByMatch(request); + return R.success("账单更新完成"); + } @PostMapping("/complete") @ApiOperationSupport(order = 14) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java index 117950b..36f2248 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java @@ -10,9 +10,23 @@ package org.springblade.transport.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import org.springblade.transport.pojo.entity.FormalSettlement; /** 正式结算单 Mapper。 @author Chill */ @Mapper public interface FormalSettlementMapper extends BaseMapper { + + /** + * 按正式结算单号查询,包含逻辑删除记录,用于新增时复用软删除单据。 + */ + @Select("SELECT * FROM blade_formal_settlement WHERE tenant_id = #{tenantId} AND formal_settlement_no = #{formalSettlementNo} ORDER BY is_deleted ASC, id DESC LIMIT 1") + FormalSettlement selectByFormalSettlementNoIncludingDeleted(@Param("tenantId") String tenantId, + @Param("formalSettlementNo") String formalSettlementNo); + + /** 恢复逻辑删除的正式结算单主记录。 */ + @Update("UPDATE blade_formal_settlement SET is_deleted = 0 WHERE tenant_id = #{tenantId} AND id = #{id} AND is_deleted = 1") + int restoreByIdIncludingDeleted(@Param("tenantId") String tenantId, @Param("id") Long id); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java index 1a664e5..e808e68 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java @@ -3,9 +3,23 @@ package org.springblade.transport.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import org.springblade.transport.pojo.entity.TransportReconciliation; /** 运输对账单 Mapper。 @author Chill */ @Mapper public interface TransportReconciliationMapper extends BaseMapper { + + /** + * 按对账单号查询,包含逻辑删除记录,用于新增时复用软删除单据。 + */ + @Select("SELECT * FROM blade_transport_reconciliation WHERE tenant_id = #{tenantId} AND reconciliation_no = #{reconciliationNo} ORDER BY is_deleted ASC, id DESC LIMIT 1") + TransportReconciliation selectByReconciliationNoIncludingDeleted(@Param("tenantId") String tenantId, + @Param("reconciliationNo") String reconciliationNo); + + /** 恢复逻辑删除的运输对账单主记录。 */ + @Update("UPDATE blade_transport_reconciliation SET is_deleted = 0 WHERE tenant_id = #{tenantId} AND id = #{id} AND is_deleted = 1") + int restoreByIdIncludingDeleted(@Param("tenantId") String tenantId, @Param("id") Long id); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java index 7c0a608..1dc23ff 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java @@ -36,6 +36,6 @@ public interface ITransportReconciliationService extends BaseService sources = Func.isEmpty(request.getSourcePreSettlementIds()) ? List.of() @@ -332,8 +352,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl 0) throw new ServiceException("该正式结算单已归属其他对账单"); - TransportReconciliation bill = request.getId() == null ? new TransportReconciliation() : editable(request.getId()); - boolean rebuild = bill.getId() == null || !Objects.equals(bill.getFormalSettlementId(), formal.getId()) + TransportReconciliation bill; + boolean restoring = false; + if (request.getId() != null) { + bill = editable(request.getId()); + } else { + String reconciliationNo = nextNo(); + TransportReconciliation deletedBill = baseMapper.selectByReconciliationNoIncludingDeleted( + AuthUtil.getTenantId(), reconciliationNo); + if (deletedBill != null && !Objects.equals(deletedBill.getIsDeleted(), 1)) { + throw new ServiceException("对账单号" + reconciliationNo + "已存在"); + } + if (deletedBill == null) { + bill = new TransportReconciliation(); + } else { + baseMapper.restoreByIdIncludingDeleted(AuthUtil.getTenantId(), deletedBill.getId()); + deletedBill.setIsDeleted(0); + bill = deletedBill; + restoring = true; + } + bill.setReconciliationNo(reconciliationNo); + } + boolean rebuild = restoring || bill.getId() == null || !Objects.equals(bill.getFormalSettlementId(), formal.getId()) || !Objects.equals(bill.getReconciliationMode(), request.getReconciliationMode()); if (bill.getId() == null) { - bill.setReconciliationNo(nextNo()); bill.setReconciliationStatus(UNFINISHED); bill.setMatchStatus(UNMATCHED); bill.setBillUpdated(false); } + if (restoring) { + bill.setReconciliationStatus(UNFINISHED); + bill.setMatchStatus(UNMATCHED); + bill.setBillUpdated(false); + bill.setCompletedTime(null); + } copyHeader(formal, bill); bill.setReconciliationMode(request.getReconciliationMode()); bill.setReconcilerId(AuthUtil.getUserId()); @@ -473,6 +498,11 @@ public class TransportReconciliationServiceImpl if (source.getReconciliationId() != null && !Objects.equals(source.getReconciliationId(), target.getReconciliationId())) { throw new ServiceException("内部账单明细不属于当前对账单"); } + target.setVehicleNo(source.getVehicleNo()); + target.setDepartureAddress(source.getDepartureAddress()); + target.setArrivalAddress(source.getArrivalAddress()); + target.setCargoName(source.getCargoName()); + target.setCargoType(source.getCargoType()); target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量")); target.setUnitPrice(nonNegative(source.getUnitPrice(), "运输单价")); target.setMileage(source.getMileage()); @@ -584,28 +614,111 @@ public class TransportReconciliationServiceImpl @Override @Transactional(rollbackFor = Exception.class) - public void updateByMatch(Long id) { - TransportReconciliation bill = editable(id); - List internals = assertAllMatched(bill); - if (money(bill.getExternalAmount()).compareTo(money(bill.getPaidAmount())) < 0) { - throw new ServiceException("导入账单匹配金额小于已付金额,不能更新内部账单"); - } - Map externalMap = externalRows(id).stream() + public void updateByMatch(TransportReconciliationVO request) { + if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在"); + TransportReconciliation bill = editable(request.getId()); + List incomingInternals = request.getInternalDetails(); + List incomingExternals = request.getExternalDetails(); + if (incomingInternals == null) incomingInternals = internalRows(request.getId()); + if (incomingExternals == null) incomingExternals = externalRows(request.getId()); + applySnapshots(bill, incomingInternals, incomingExternals); + List internals = rematch(request.getId()); + Map externalMap = externalRows(request.getId()).stream() .collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity())); for (TransportReconciliationInternal internal : internals) { TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId()); - if (VEHICLE.equals(bill.getReconciliationMode()) && hasMultipleCargo(internal.getFormalSettlementDetailId())) { - internal.setUpdateResult("skipped_multi_cargo"); - internal.setUpdateMessage("一车多货,已跳过自动更新,请人工调整货物费用"); - internalMapper.updateById(internal); - continue; - } applyAmount(bill, internal, external); + copyExternalToInternal(internal, external); + internalMapper.updateById(internal); + updateWaybill(internal, external); } recalculateSettlement(bill); bill.setBillUpdated(true); updateById(bill); - refreshStats(id); + refreshStats(request.getId()); + } + + private List rematch(Long reconciliationId) { + List internals = internalRows(reconciliationId); + List externals = externalRows(reconciliationId); + if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); + resetMatches(internals, externals); + Map> externalGroups = externals.stream() + .collect(Collectors.groupingBy(this::matchKey)); + Map> internalGroups = internals.stream() + .collect(Collectors.groupingBy(this::matchKey)); + for (Map.Entry> entry : externalGroups.entrySet()) { + List externalGroup = entry.getValue(); + List internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); + if (externalGroup.size() == 1 && internalGroup.size() == 1) { + link(internalGroup.get(0), externalGroup.get(0)); + } else if (externalGroup.size() > 1) { + for (TransportReconciliationExternal external : externalGroup) { + external.setSuspectedDuplicate(true); + external.setMatchStatus(DUPLICATE); + externalMapper.updateById(external); + } + } + } + return assertAllMatched(existing(reconciliationId)); + } + + private void copyExternalToInternal(TransportReconciliationInternal internal, + TransportReconciliationExternal external) { + internal.setVehicleNo(external.getVehicleNo()); + internal.setDepartureAddress(external.getDepartureAddress()); + internal.setArrivalAddress(external.getArrivalAddress()); + internal.setActualDepartureTime(external.getActualDepartureTime()); + internal.setActualCompletionTime(external.getActualCompletionTime()); + internal.setTransportType(external.getTransportType()); + internal.setCargoName(external.getCargoName()); + internal.setCargoType(external.getCargoType()); + internal.setSpecification(external.getSpecification()); + internal.setModel(external.getModel()); + internal.setTransportQuantity(external.getTransportQuantity()); + internal.setQuantityUnit(external.getQuantityUnit()); + internal.setMileage(external.getMileage()); + internal.setBatchNo(external.getBatchNo()); + internal.setUnitPrice(external.getUnitPrice()); + internal.setFreightAmount(external.getFreightAmount()); + internal.setFeeItemsJson(external.getFeeItemsJson()); + internal.setSettlementAmount(external.getSettlementAmount()); + } + + private void updateWaybill(TransportReconciliationInternal internal, + TransportReconciliationExternal external) { + Long waybillId = null; + FormalSettlementDetail detail = internal.getFormalSettlementDetailId() == null ? null + : formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); + if (detail != null) waybillId = detail.getWaybillId(); + if (waybillId == null && internal.getSourceDetailId() != null) { + ReceivablePayableDetail source = receivablePayableMapper.selectById(internal.getSourceDetailId()); + if (source != null) waybillId = source.getWaybillId(); + } + Waybill waybill = waybillId == null ? null : waybillMapper.selectById(waybillId); + if (waybill == null && Func.isNotEmpty(internal.getWaybillNo())) { + waybill = waybillMapper.selectOne(Wrappers.lambdaQuery() + .eq(Waybill::getWaybillNo, internal.getWaybillNo())); + } + if (waybill == null) return; + waybill.setVehicleNo(external.getVehicleNo()); + waybill.setDepartureAddress(external.getDepartureAddress()); + waybill.setArrivalAddress(external.getArrivalAddress()); + waybill.setStartDate(external.getActualDepartureTime() == null ? null : external.getActualDepartureTime().toLocalDate()); + waybill.setEndDate(external.getActualCompletionTime() == null ? null : external.getActualCompletionTime().toLocalDate()); + waybill.setTransportType(external.getTransportType()); + waybill.setCargoName(external.getCargoName()); + waybill.setCargoType(external.getCargoType()); + waybill.setSpecification(external.getSpecification()); + waybill.setModel(external.getModel()); + waybill.setQuantity(external.getTransportQuantity()); + waybill.setQuantityUnit(external.getQuantityUnit()); + waybill.setMileage(external.getMileage()); + waybill.setBatchNo(external.getBatchNo()); + waybill.setUnitPrice(external.getUnitPrice()); + waybill.setFreightJson(external.getFeeItemsJson()); + waybill.setOtherFeeTotal(money(external.getSettlementAmount()).subtract(money(external.getFreightAmount())).max(BigDecimal.ZERO)); + waybillMapper.updateById(waybill); } @Override