1、调整凭证

2、调整对账
3、解决正式结算冲突bug
This commit is contained in:
2026-09-09 10:20:52 +08:00
parent bdafb83583
commit 0ce701ab90
7 changed files with 193 additions and 23 deletions
@@ -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)
@@ -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<FormalSettlement> {
/**
* 按正式结算单号查询,包含逻辑删除记录,用于新增时复用软删除单据。
*/
@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);
}
@@ -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<TransportReconciliation> {
/**
* 按对账单号查询,包含逻辑删除记录,用于新增时复用软删除单据。
*/
@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);
}
@@ -36,6 +36,6 @@ public interface ITransportReconciliationService extends BaseService<TransportRe
void manualMatch(TransportReconciliationManualMatchRequest request);
void unmatch(Long internalId);
void adjustInternal(TransportReconciliationInternal row);
void updateByMatch(Long id);
void updateByMatch(TransportReconciliationVO request);
void complete(Long id);
}
@@ -309,7 +309,27 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
if (Func.isEmpty(request.getSourcePreSettlementIds()) && Func.isEmpty(request.getSourceDetailIds())) {
throw new ServiceException("请至少选择一张预结算单或一条应收应付明细");
}
FormalSettlement settlement = request.getId() == null ? new FormalSettlement() : editable(request.getId());
FormalSettlement settlement;
boolean restoring = false;
if (request.getId() != null) {
settlement = editable(request.getId());
} else {
String requestedNo = Func.isEmpty(request.getFormalSettlementNo()) ? null : request.getFormalSettlementNo().trim();
FormalSettlement existing = requestedNo == null ? null
: baseMapper.selectByFormalSettlementNoIncludingDeleted(AuthUtil.getTenantId(), requestedNo);
if (existing != null && !Objects.equals(existing.getIsDeleted(), 1)) {
throw new ServiceException("正式结算单号" + requestedNo + "已存在");
}
if (existing != null) {
settlement = existing;
baseMapper.restoreByIdIncludingDeleted(AuthUtil.getTenantId(), existing.getId());
settlement.setIsDeleted(0);
restoring = true;
} else {
settlement = new FormalSettlement();
}
if (requestedNo != null) settlement.setFormalSettlementNo(requestedNo);
}
boolean creating = settlement.getId() == null;
if (settlement.getId() != null) releaseSources(settlement);
List<PreSettlement> sources = Func.isEmpty(request.getSourcePreSettlementIds()) ? List.of()
@@ -332,8 +352,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|| "terminated".equals(contract.getContractStage())) {
throw new ServiceException("合同未审核完成,不可转正式结算单");
}
if (settlement.getId() == null) {
settlement.setFormalSettlementNo(nextNo(settlementType));
if (creating || restoring) {
if (Func.isEmpty(settlement.getFormalSettlementNo())) settlement.setFormalSettlementNo(nextNo(settlementType));
settlement.setApprovalStatus(DRAFT);
settlement.setCurrentNode("草稿");
settlement.setSourceType(sources.isEmpty() ? "应收应付" : directDetails.isEmpty() ? "预结算合并" : "混合来源");
@@ -364,7 +384,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
applySummaryRequest(settlement.getId(), request.getSummaryFees());
refreshSettlementAmount(settlement);
rebuildInvoices(settlement, request.getInvoices());
if (!creating) {
if (!creating && !restoring) {
saveChange(settlement.getId(), "结算单基本信息", null, "调整",
"保存正式结算单" + settlement.getFormalSettlementNo(), request.getRemark());
}
@@ -161,15 +161,40 @@ public class TransportReconciliationServiceImpl
.eq(TransportReconciliation::getFormalSettlementId, formal.getId())
.ne(request.getId() != null, TransportReconciliation::getId, request.getId()));
if (occupied > 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<TransportReconciliationInternal> internals = assertAllMatched(bill);
if (money(bill.getExternalAmount()).compareTo(money(bill.getPaidAmount())) < 0) {
throw new ServiceException("导入账单匹配金额小于已付金额,不能更新内部账单");
}
Map<Long, TransportReconciliationExternal> externalMap = externalRows(id).stream()
public void updateByMatch(TransportReconciliationVO request) {
if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在");
TransportReconciliation bill = editable(request.getId());
List<TransportReconciliationInternal> incomingInternals = request.getInternalDetails();
List<TransportReconciliationExternal> incomingExternals = request.getExternalDetails();
if (incomingInternals == null) incomingInternals = internalRows(request.getId());
if (incomingExternals == null) incomingExternals = externalRows(request.getId());
applySnapshots(bill, incomingInternals, incomingExternals);
List<TransportReconciliationInternal> internals = rematch(request.getId());
Map<Long, TransportReconciliationExternal> 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<TransportReconciliationInternal> rematch(Long reconciliationId) {
List<TransportReconciliationInternal> internals = internalRows(reconciliationId);
List<TransportReconciliationExternal> externals = externalRows(reconciliationId);
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
resetMatches(internals, externals);
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream()
.collect(Collectors.groupingBy(this::matchKey));
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream()
.collect(Collectors.groupingBy(this::matchKey));
for (Map.Entry<String, List<TransportReconciliationExternal>> entry : externalGroups.entrySet()) {
List<TransportReconciliationExternal> externalGroup = entry.getValue();
List<TransportReconciliationInternal> 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.<Waybill>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