调整结算模块

This commit is contained in:
2026-09-07 19:37:08 +08:00
parent 2e7ab6f508
commit 4d34525b7b
8 changed files with 208 additions and 40 deletions
@@ -33,7 +33,7 @@ public class VehicleReconciliationExcel implements Serializable {
@ExcelProperty("里程(KM") @NumberFormat("0.00") private BigDecimal mileage;
@ExcelProperty("批次号") private String batchNo;
@ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice;
@ExcelProperty("运费") @NumberFormat("0.00") private BigDecimal freightAmount;
@ExcelProperty("") @NumberFormat("0.00") private BigDecimal freightAmount;
@ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne;
@ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount;
@ExcelIgnore private String errorMessage;
@@ -31,6 +31,7 @@ import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO;
import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO;
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
@@ -81,6 +82,12 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
void generateForCompletedWaybills(List<Long> waybillIds);
/**
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
*/
void generateForImportedWaybills(List<Waybill> waybills);
/** 完成配载单后按其记录的承运商合同汇总生成一条应付明细。 */
void generateForCompletedLoading(List<Long> waybillIds, Long carrierContractId, String loadingNo);
@@ -641,6 +641,14 @@ public class ReceivablePayableDetailServiceImpl
this::resolveWaybillCarrierContract, AuthUtil.getUserId(), new Date());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void generateForImportedWaybills(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) return;
generateAutomaticWaybillDetails(waybills, true,
this::resolveWaybillCarrierContract, AuthUtil.getUserId(), new Date());
}
@Override
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.vo.TransportReconciliationVO;
import org.springblade.transport.service.ITransportReconciliationService;
import org.springblade.transport.wrapper.TransportReconciliationWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@@ -139,6 +140,9 @@ public class TransportReconciliationServiceImpl
@Override
public TransportReconciliationVO detail(Long id) {
TransportReconciliationVO vo = TransportReconciliationWrapper.build().entityVO(existing(id));
if (Func.isEmpty(vo.getCustomerName())) {
vo.setCustomerName("receivable".equals(vo.getSettlementType()) ? vo.getPayerName() : vo.getPayeeName());
}
vo.setInternalDetails(internalRows(id));
vo.setExternalDetails(externalRows(id));
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<TransportReconciliationChangeRecord>lambdaQuery()
@@ -196,32 +200,115 @@ public class TransportReconciliationServiceImpl
@Override
@Transactional(rollbackFor = Exception.class)
public List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows) {
if (Func.isEmpty(rows)) throw new ServiceException("导入数据不能为空");
TransportReconciliation bill = editable(id);
if (!VEHICLE.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是整车总额对账");
resetExternal(id);
List<VehicleReconciliationFailureExcel> failures = new ArrayList<>();
List<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
VehicleReconciliationExcel row = rows.get(index);
List<String> validationErrors = validateImportVehicle(row);
if (Func.isNotEmpty(validationErrors)) {
VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel();
BeanUtil.copyProperties(row, failure);
failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
failures.add(failure);
continue;
}
try {
validateExternal(row.getVehicleNo(), row.getCargoName(), row.getTransportQuantity(), row.getSettlementAmount());
TransportReconciliationExternal external = new TransportReconciliationExternal();
BeanUtil.copyProperties(row, external);
external.setReconciliationId(id); external.setExternalLineNo(index + 2);
external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间"));
external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间"));
external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目1", money(row.getFeeItemOne()))));
external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false);
external.setRawDataJson(JsonUtil.toJson(row)); externalMapper.insert(external);
validExternals.add(buildVehicleExternal(id, index, row));
} catch (Exception exception) {
VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel();
BeanUtil.copyProperties(row, failure); failure.setErrorMessage(exception.getMessage());
BeanUtil.copyProperties(row, failure);
failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(
exception instanceof ServiceException ? exception.getMessage() : "导入失败")));
failures.add(failure);
}
}
if (Func.isNotEmpty(failures)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return failures;
}
for (TransportReconciliationExternal external : validExternals) {
if (externalMapper.insert(external) <= 0) throw new ServiceException("外部账单保存失败");
}
refreshStats(id);
return failures;
}
private TransportReconciliationExternal buildVehicleExternal(Long reconciliationId, int index,
VehicleReconciliationExcel row) {
TransportReconciliationExternal external = new TransportReconciliationExternal();
BeanUtil.copyProperties(row, external);
external.setReconciliationId(reconciliationId);
external.setExternalLineNo(index + 2);
external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间"));
external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间"));
external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目1", money(row.getFeeItemOne()))));
external.setMatchStatus(UNMATCHED);
external.setSuspectedDuplicate(false);
external.setRawDataJson(JsonUtil.toJson(row));
return external;
}
private List<String> validateImportVehicle(VehicleReconciliationExcel row) {
List<String> validationErrors = new ArrayList<>();
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getVehicleNo()), "车牌号不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getCargoName()), "货物名称不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getActualDepartureTime()), "实际发货时间不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getTransportQuantity()), "运输总量不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
Func.isEmpty(row.getSettlementAmount()), "结算费用合计不能为空");
addImportLengthError(validationErrors, row.getVehicleNo(), 30, "车牌号不能超过30字");
addImportLengthError(validationErrors, row.getDepartureAddress(), 500, "发货地址不能超过500字");
addImportLengthError(validationErrors, row.getArrivalAddress(), 500, "到货地址不能超过500字");
addImportLengthError(validationErrors, row.getTransportType(), 100, "运输类型不能超过100字");
addImportLengthError(validationErrors, row.getCargoName(), 500, "货物名称不能超过500字");
addImportLengthError(validationErrors, row.getCargoType(), 500, "货物类型不能超过500字");
addImportLengthError(validationErrors, row.getBatchNo(), 100, "批次号不能超过100字");
addImportDecimalErrors(validationErrors, row.getTransportQuantity(), "运输总量", 6);
addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM", 2);
addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2);
addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2);
addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "费用项目1", 2);
addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2);
LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors);
LocalDateTime completionTime = parseImportTime(row.getActualCompletionTime(), "实际完成时间", validationErrors);
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
departureTime != null && completionTime != null && completionTime.isBefore(departureTime),
"实际完成时间不能早于实际发货时间");
return validationErrors;
}
private LocalDateTime parseImportTime(String value, String field, List<String> validationErrors) {
if (Func.isEmpty(value)) return null;
try {
return parseTimeNullable(value, field);
} catch (ServiceException exception) {
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, true, exception.getMessage());
return null;
}
}
private void addImportLengthError(List<String> validationErrors, String value, int maxLength, String message) {
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, value, maxLength, message);
}
private void addImportDecimalErrors(List<String> validationErrors, BigDecimal value, String field, int scale) {
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
value != null && value.compareTo(BigDecimal.ZERO) < 0, field + "不能小于0");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors,
value != null && value.stripTrailingZeros().scale() > scale, field + "最多保留" + scale + "位小数");
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> rows) {
@@ -563,6 +650,7 @@ public class TransportReconciliationServiceImpl
bill.setFormalSettlementId(formal.getId()); bill.setFormalSettlementNo(formal.getFormalSettlementNo()); bill.setSettlementType(formal.getSettlementType());
bill.setProjectId(formal.getProjectId()); bill.setProjectName(formal.getProjectName()); bill.setDeptId(formal.getDeptId()); bill.setDeptName(formal.getDeptName());
bill.setContractId(formal.getContractId()); bill.setContractNo(formal.getContractNo()); bill.setContractName(formal.getContractName());
bill.setCustomerName("receivable".equals(formal.getSettlementType()) ? formal.getPayerName() : formal.getPayeeName());
bill.setPayerName(formal.getPayerName()); bill.setPayeeName(formal.getPayeeName()); bill.setCurrency(formal.getCurrency());
bill.setSettlementAmount(money(formal.getSettlementAmount())); bill.setPaidAmount(money(formal.getPaidAmount()));
List<String> preNos = formalSourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, formal.getId()))
@@ -637,5 +725,20 @@ public class TransportReconciliationServiceImpl
private BigDecimal sumExternalQuantity(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumInternalAmount(List<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumExternalAmount(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private String nextNo() { return "DZ" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); }
private synchronized String nextNo() {
String prefix = "DZD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
int sequence = list(Wrappers.<TransportReconciliation>lambdaQuery()
.select(TransportReconciliation::getReconciliationNo)
.likeRight(TransportReconciliation::getReconciliationNo, prefix))
.stream()
.map(TransportReconciliation::getReconciliationNo)
.filter(number -> number != null && number.length() == prefix.length() + 5)
.map(number -> number.substring(prefix.length()))
.filter(suffix -> suffix.chars().allMatch(Character::isDigit))
.mapToInt(Integer::parseInt)
.max()
.orElse(0) + 1;
if (sequence > 99999) throw new ServiceException("当日运输对账单号流水已用完");
return prefix + String.format("%05d", sequence);
}
}
@@ -25,6 +25,7 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillImportBatchService;
import org.springblade.transport.service.IWaybillService;
@@ -48,56 +49,75 @@ import java.util.stream.Collectors;
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/** 批量导入状态仅保留草稿与导入完成两种。 */
private static final String STATUS_DRAFT = "draft";
private static final String STATUS_COMPLETED = "completed";
private static final String IMPORT_TYPE_WAYBILL = "waybill";
private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
private final IWaybillService waybillService;
private final ICustomerArchiveService customerArchiveService;
private final IProjectApplyService projectApplyService;
private final ITransportPlanService transportPlanService;
private final IReceivablePayableDetailService receivablePayableDetailService;
@Override
@Transactional(rollbackFor = Exception.class)
public WaybillImportBatch saveDraft(WaybillImportBatchRequest request) {
WaybillImportBatch batch = buildBatch(request, "draft");
batch.setWaybillCount(0);
if (Func.isNotEmpty(request.getId())) {
WaybillImportBatch oldBatch = getById(request.getId());
if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) throw new ServiceException("运单批次不存在");
batch.setId(oldBatch.getId());
batch.setBatchNo(oldBatch.getBatchNo());
}
saveOrUpdate(batch);
return batch;
return persist(request, STATUS_DRAFT);
}
@Override
@Transactional(rollbackFor = Exception.class)
public WaybillImportBatch confirm(WaybillImportBatchRequest request) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
String importStatus = "processing".equals(request.getStatus()) ? "processing" : "completed";
return persist(request, STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED);
}
/** 草稿与确认导入共用落库流程,差异仅在于运单是否走校验以及是否生成应收应付明细。 */
private WaybillImportBatch persist(WaybillImportBatchRequest request, String importStatus) {
boolean draft = STATUS_DRAFT.equals(importStatus);
WaybillImportBatch batch = buildBatch(request, importStatus);
if (Func.isNotEmpty(request.getId())) {
WaybillImportBatch oldBatch = getById(request.getId());
if (oldBatch == null || Objects.equals(oldBatch.getIsDeleted(), 1)) throw new ServiceException("运单批次不存在");
if (!"draft".equals(oldBatch.getImportStatus())) throw new ServiceException("仅草稿状态的批次允许确认导入");
if (!STATUS_DRAFT.equals(oldBatch.getImportStatus())) throw new ServiceException("仅草稿状态的批次允许编辑");
batch.setId(oldBatch.getId());
batch.setBatchNo(oldBatch.getBatchNo());
}
batch.setWaybillCount(0);
saveOrUpdate(batch);
// 重新落库前清理批次已生成的运单避免草稿反复保存产生重复运单
clearBatchWaybills(batch.getId());
for (int index = 0; index < request.getRows().size(); index++) {
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
List<Waybill> waybills = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
try {
Waybill waybill = buildWaybill(request.getRows().get(index), batch, request.getCarrierContractId());
waybillService.submit(waybill);
Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
waybills.add(waybill);
} catch (Exception exception) {
throw new ServiceException("" + (index + 1) + "导入失败:" + exception.getMessage());
throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
}
}
batch.setWaybillCount(request.getRows().size());
batch.setImportStatus(importStatus);
batch.setWaybillCount(waybills.size());
updateById(batch);
// 导入完成且导入类型为运单时按合同费用生成模式系统生成同步生成应收应付明细
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType())) {
receivablePayableDetailService.generateForImportedWaybills(waybills);
}
return batch;
}
private void clearBatchWaybills(Long batchId) {
if (Func.isEmpty(batchId)) return;
List<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getImportBatchId, batchId).eq(Waybill::getIsDeleted, 0));
if (Func.isNotEmpty(waybills)) {
waybillService.deleteLogic(waybills.stream().map(Waybill::getId).toList());
}
}
@Override
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
@@ -143,7 +163,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
batch.setCarrierIds(joinIds(request.getCarrierIds()));
batch.setCarrierName(resolveCarrierNames(request.getCarrierIds(), request.getCarrierName()));
batch.setImportStatus(importStatus);
batch.setImportType("settlement".equals(request.getImportType()) ? "settlement" : "waybill");
batch.setImportType(IMPORT_TYPE_SETTLEMENT.equals(request.getImportType()) ? IMPORT_TYPE_SETTLEMENT : IMPORT_TYPE_WAYBILL);
ProjectApply project = projectApplyService.getById(request.getProjectId());
if (project == null || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) {
throw new ServiceException("仅允许选择审核通过或变更审核通过的项目");
@@ -155,7 +175,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return batch;
}
private Waybill buildWaybill(Map<String, Object> row, WaybillImportBatch batch, Long carrierContractId) {
private Waybill buildWaybill(Map<String, Object> row, WaybillImportBatch batch, Long carrierContractId, boolean draft) {
if (row == null) throw new ServiceException("运单数据不正确");
Waybill waybill = new Waybill();
waybill.setOriginalNo(stringValue(row, "originalNo"));
@@ -207,12 +227,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
waybill.setImportBatchId(batch.getId());
waybill.setBatchNo(batch.getBatchNo());
waybill.setDataSource("批量导入");
waybill.setBusinessStatus("processing".equals(batch.getImportStatus()) ? "pending" : batch.getImportStatus());
waybill.setBusinessStatus(batch.getImportStatus());
waybill.setQuantity(defaultQuantity(waybill.getQuantity()));
waybill.setQuantityUnit(Func.isEmpty(waybill.getQuantityUnit()) ? "" : waybill.getQuantityUnit());
waybill.setPriceUnit(Func.isEmpty(waybill.getPriceUnit()) ? "" : waybill.getPriceUnit());
waybill.setStartDate(parseDate(row.get("startDate"), "开始时间"));
waybill.setEndDate(parseDate(row.get("endDate"), "结束时间"));
// 草稿允许明细不完整时间为空时不阻断保存
waybill.setStartDate(parseDate(row.get("startDate"), "开始时间", !draft));
waybill.setEndDate(parseDate(row.get("endDate"), "结束时间", !draft));
return waybill;
}
@@ -254,8 +275,11 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return mileage != null && mileage.compareTo(BigDecimal.ONE.negate()) == 0 ? null : mileage;
}
private LocalDate parseDate(Object value, String fieldName) {
if (value == null || String.valueOf(value).isBlank()) throw new ServiceException(fieldName + "不能为空");
private LocalDate parseDate(Object value, String fieldName, boolean required) {
if (value == null || String.valueOf(value).isBlank()) {
if (!required) return null;
throw new ServiceException(fieldName + "不能为空");
}
String text = String.valueOf(value).trim();
try {
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
@@ -287,8 +311,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
private WaybillImportBatchVO toVO(WaybillImportBatch batch) {
WaybillImportBatchVO vo = BeanUtil.copyProperties(batch, WaybillImportBatchVO.class);
if (vo == null) throw new ServiceException("运单批次数据转换失败");
vo.setImportTypeName("settlement".equals(batch.getImportType()) ? "结算单" : "运单");
vo.setStatusName(switch (batch.getImportStatus()) { case "draft" -> "草稿"; case "processing" -> "进行中"; default -> "完成"; });
vo.setImportTypeName(IMPORT_TYPE_SETTLEMENT.equals(batch.getImportType()) ? "结算单" : "运单");
vo.setStatusName(STATUS_DRAFT.equals(batch.getImportStatus()) ? "草稿" : "导入完成");
vo.setCreateUserName(UserCache.getUserRealName(batch.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(batch.getUpdateUser()));
return vo;