diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java index a042452..fb18df7 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java @@ -71,6 +71,7 @@ public interface ISysClient { String PARAM_VALUE = API_PREFIX + "/param-value"; String REGION = API_PREFIX + "/region"; String FEE_ITEMS = API_PREFIX + "/fee-items"; + String CARGO_TYPES = API_PREFIX + "/cargo-types"; /** * 获取菜单 @@ -301,4 +302,7 @@ public interface ISysClient { @GetMapping(FEE_ITEMS) R> getFeeItems(); + @GetMapping(CARGO_TYPES) + R> getCargoTypes(); + } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java index fe7d1d0..f31965c 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java @@ -164,5 +164,9 @@ public class ISysClientFallback implements ISysClient { return R.fail("获取数据失败"); } + @Override + public R> getCargoTypes() { + return R.fail("获取数据失败"); + } } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java index d498905..5945ff5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java @@ -8,6 +8,7 @@ import org.springblade.transport.pojo.entity.TransportReconciliation; import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; import org.springblade.transport.pojo.entity.TransportReconciliationExternal; import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import java.io.Serial; import java.util.List; @@ -25,5 +26,6 @@ public class TransportReconciliationVO extends TransportReconciliation { @TableField(exist = false) private String matchStatusName; @TableField(exist = false) private List internalDetails; @TableField(exist = false) private List externalDetails; + @TableField(exist = false) private List feeSummary; @TableField(exist = false) private List changeRecords; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java index 82d63be..789db31 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java @@ -65,6 +65,7 @@ public class SysClient implements ISysClient { private final IRegionService regionService; private final IFeeItemService feeItemService; + private final ICargoTypeService cargoTypeService; @Override @GetMapping(MENU) @@ -212,5 +213,12 @@ public class SysClient implements ISysClient { .orderByAsc(FeeItem::getFeeCategory, FeeItem::getName))); } + @Override + @GetMapping(CARGO_TYPES) + public R> getCargoTypes() { + return R.data(cargoTypeService.list(Wrappers.lambdaQuery() + .eq(CargoType::getIsDeleted, 0) + .orderByAsc(CargoType::getCargoCode))); + } } 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 d192c02..b1f5b6b 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 @@ -21,8 +21,10 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFeeReader; import org.springblade.transport.excel.CargoReconciliationFailureExcel; import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFeeReader; import org.springblade.transport.excel.VehicleReconciliationFailureExcel; import org.springblade.transport.excel.TransportReconciliationExportExcel; import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; @@ -40,10 +42,11 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import jakarta.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.List; import java.math.BigDecimal; +import java.util.List; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Map; /** 运输对账单控制器。 @author Chill */ @RestController @@ -122,7 +125,10 @@ public class TransportReconciliationController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "导入整车总额外部账单") public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { - List failures = reconciliationService.importVehicles(id, ExcelUtil.read(file, VehicleReconciliationExcel.class)); + List rows = ExcelUtil.read(file, VehicleReconciliationExcel.class); + List> feeItems = VehicleReconciliationFeeReader.read(file); + for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index)); + List failures = reconciliationService.importVehicles(id, rows); if (!failures.isEmpty()) { ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class); return null; @@ -134,7 +140,10 @@ public class TransportReconciliationController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入货物明细外部账单") public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { - List failures = reconciliationService.importCargoes(id, ExcelUtil.read(file, CargoReconciliationExcel.class)); + List rows = ExcelUtil.read(file, CargoReconciliationExcel.class); + List> feeItems = CargoReconciliationFeeReader.read(file); + for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index)); + List failures = reconciliationService.importCargoes(id, rows); if (!failures.isEmpty()) { ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class); return null; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java index 0c03d69..25a6e20 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java @@ -15,6 +15,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; import java.math.BigDecimal; +import java.util.Map; /** 货物明细对账导入模型。 @author Chill */ @Data @@ -34,8 +35,9 @@ public class CargoReconciliationExcel implements Serializable { @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; @ExcelProperty("里程(KM)") @NumberFormat("0.00") private BigDecimal mileage; @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; - @ExcelProperty("费用项目名称1") @NumberFormat("0.00") private BigDecimal feeItemOne; - @ExcelProperty("费用项目名称2") @NumberFormat("0.00") private BigDecimal feeItemTwo; + @ExcelProperty("水费") @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelProperty("罚款") @NumberFormat("0.00") private BigDecimal feeItemTwo; @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; + @ExcelIgnore private Map feeItems; @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java new file mode 100644 index 0000000..e280210 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java @@ -0,0 +1,106 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import cn.idev.excel.FastExcel; +import cn.idev.excel.context.AnalysisContext; +import cn.idev.excel.event.AnalysisEventListener; +import cn.idev.excel.metadata.data.ReadCellData; +import org.springblade.core.log.exception.ServiceException; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** 货物明细对账动态费用读取器。 @author Chill */ +public final class CargoReconciliationFeeReader { + + private CargoReconciliationFeeReader() { + } + + public static List> read(MultipartFile file) { + try (InputStream inputStream = file.getInputStream()) { + FeeListener listener = new FeeListener(); + FastExcel.read(inputStream) + .useDefaultListener(false) + .registerReadListener(listener) + .sheet() + .doRead(); + return listener.getFeeItems(); + } catch (IOException exception) { + throw new ServiceException("读取货物明细对账费用列失败"); + } + } + + private static final class FeeListener extends AnalysisEventListener>> { + private final List> feeItems = new ArrayList<>(); + private Map headers = Map.of(); + private int freightColumn = -1; + private int settlementColumn = -1; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + headers = headMap; + freightColumn = findColumn("运输费"); + settlementColumn = findColumn("结算费用合计"); + } + + @Override + public void invoke(Map> row, AnalysisContext context) { + Map values = new LinkedHashMap<>(); + if (freightColumn >= 0 && settlementColumn > freightColumn) { + for (int column = freightColumn + 1; column < settlementColumn; column++) { + String name = headers.get(column); + if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column))); + } + } + feeItems.add(values); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + } + + private int findColumn(String header) { + return headers.entrySet().stream() + .filter(entry -> header.equals(entry.getValue())) + .mapToInt(Map.Entry::getKey) + .findFirst().orElse(-1); + } + + private BigDecimal decimal(ReadCellData cellData) { + if (cellData == null) return BigDecimal.ZERO.setScale(2); + Object value = cellData.getData(); + if (value == null) { + value = switch (cellData.getType()) { + case NUMBER -> cellData.getNumberValue(); + case STRING, DIRECT_STRING, ERROR -> cellData.getStringValue(); + case BOOLEAN -> cellData.getBooleanValue(); + default -> null; + }; + } + if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO.setScale(2); + try { + return new BigDecimal(String.valueOf(value).trim()).setScale(2); + } catch (NumberFormatException exception) { + return BigDecimal.ZERO.setScale(2); + } + } + + private List> getFeeItems() { + return feeItems; + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java index 8a36075..f2d3266 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java @@ -15,6 +15,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; import java.math.BigDecimal; +import java.util.Map; /** 整车总额对账导入模型。 @author Chill */ @Data @@ -36,5 +37,6 @@ public class VehicleReconciliationExcel implements Serializable { @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; @ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne; @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; + @ExcelIgnore private Map feeItems; @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java new file mode 100644 index 0000000..e6d889a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java @@ -0,0 +1,106 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import cn.idev.excel.FastExcel; +import cn.idev.excel.context.AnalysisContext; +import cn.idev.excel.event.AnalysisEventListener; +import cn.idev.excel.metadata.data.ReadCellData; +import org.springblade.core.log.exception.ServiceException; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** 整车总额对账动态费用读取器。 @author Chill */ +public final class VehicleReconciliationFeeReader { + + private VehicleReconciliationFeeReader() { + } + + public static List> read(MultipartFile file) { + try (InputStream inputStream = file.getInputStream()) { + FeeListener listener = new FeeListener(); + FastExcel.read(inputStream) + .useDefaultListener(false) + .registerReadListener(listener) + .sheet() + .doRead(); + return listener.getFeeItems(); + } catch (IOException exception) { + throw new ServiceException("读取整车总额对账费用列失败"); + } + } + + private static final class FeeListener extends AnalysisEventListener>> { + private final List> feeItems = new ArrayList<>(); + private Map headers = Map.of(); + private int freightColumn = -1; + private int settlementColumn = -1; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + headers = headMap; + freightColumn = findColumn("运输费"); + settlementColumn = findColumn("结算费用合计"); + } + + @Override + public void invoke(Map> row, AnalysisContext context) { + Map values = new LinkedHashMap<>(); + if (freightColumn >= 0 && settlementColumn > freightColumn) { + for (int column = freightColumn + 1; column < settlementColumn; column++) { + String name = headers.get(column); + if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column))); + } + } + feeItems.add(values); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + } + + private int findColumn(String header) { + return headers.entrySet().stream() + .filter(entry -> header.equals(entry.getValue())) + .mapToInt(Map.Entry::getKey) + .findFirst().orElse(-1); + } + + private BigDecimal decimal(ReadCellData cellData) { + if (cellData == null) return BigDecimal.ZERO.setScale(2); + Object value = cellData.getData(); + if (value == null) { + value = switch (cellData.getType()) { + case NUMBER -> cellData.getNumberValue(); + case STRING, DIRECT_STRING, ERROR -> cellData.getStringValue(); + case BOOLEAN -> cellData.getBooleanValue(); + default -> null; + }; + } + if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO.setScale(2); + try { + return new BigDecimal(String.valueOf(value).trim()).setScale(2); + } catch (NumberFormatException exception) { + return BigDecimal.ZERO.setScale(2); + } + } + + private List> getFeeItems() { + return feeItems; + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 8fd9239..1b72c3b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.LoadingManageExcel; import org.springblade.transport.pojo.entity.LoadingManage; +import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; import org.springblade.transport.pojo.vo.LoadingManageVO; @@ -31,6 +32,14 @@ public interface ILoadingManageService extends BaseService { boolean submit(LoadingManage loadingManage); + /** + * 根据导入运单创建配载单并建立运单关联,保留导入运单的业务状态。 + * + * @param loadingNo 配载标识号 + * @param waybills 已导入的运单 + */ + void createFromImportedWaybills(String loadingNo, List waybills); + BusinessRemoveResultVO removeLoadingManage(String ids); List exportLoadingManage(LoadingManageVO loadingManage, String ids); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 270cc5c..71c9694 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -42,6 +42,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * 配载管理 服务实现类 @@ -149,6 +150,68 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybills) { + String normalizedLoadingNo = TransportBusinessSupport.trimToNull(loadingNo); + if (Func.isEmpty(normalizedLoadingNo) || Func.isEmpty(waybills)) { + return; + } + LoadingManage existing = getOne(Wrappers.lambdaQuery() + .eq(LoadingManage::getLoadingNo, normalizedLoadingNo) + .eq(LoadingManage::getIsDeleted, 0), false); + if (existing != null) { + throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo); + } + Waybill first = waybills.get(0); + LoadingManage loadingManage = new LoadingManage(); + loadingManage.setLoadingNo(normalizedLoadingNo); + loadingManage.setLoadingSubNos(waybills.stream() + .map(Waybill::getWaybillNo) + .filter(Func::isNotEmpty) + .collect(Collectors.joining(","))); + loadingManage.setWaybillIdsJson(JsonUtil.toJson(waybills.stream().map(Waybill::getId).toList())); + loadingManage.setProjectId(first.getProjectId()); + loadingManage.setProjectName(first.getProjectName()); + loadingManage.setCustomerName(first.getCustomerName()); + loadingManage.setTransportType(first.getTransportType()); + loadingManage.setCargoType(first.getCargoType()); + loadingManage.setCargoName(first.getCargoName()); + loadingManage.setVehicleNo(first.getVehicleNo()); + loadingManage.setTrailerVehicleNo(first.getTrailerVehicleNo()); + loadingManage.setDriverName(first.getDriverName()); + loadingManage.setDriverPhone(first.getDriverPhone()); + loadingManage.setEscortName(first.getEscortName()); + loadingManage.setEscortPhone(first.getEscortPhone()); + loadingManage.setCarrierType(first.getCarrierType()); + loadingManage.setCarrierName(first.getCarrierName()); + loadingManage.setCarrierContractId(first.getCarrierContractId()); + loadingManage.setDepartureAddress(first.getDepartureAddress()); + loadingManage.setArrivalAddress(first.getArrivalAddress()); + loadingManage.setOriginalNo(first.getOriginalNo()); + loadingManage.setDataSource("批量导入"); + loadingManage.setStartDate(first.getStartDate()); + loadingManage.setEndDate(first.getEndDate()); + loadingManage.setPlanName(first.getPlanName()); + loadingManage.setBatchNo(first.getBatchNo()); + loadingManage.setCurrentProcessNode(first.getCurrentProcessNode()); + loadingManage.setMileage(first.getMileage()); + loadingManage.setEstimatedStartDate(first.getEstimatedStartTime()); + loadingManage.setEstimatedEndDate(first.getEstimatedEndTime()); + loadingManage.setTaskRemark(first.getTaskRemark()); + loadingManage.setGoodsJson(first.getGoodsJson()); + loadingManage.setRouteJson(first.getRouteJson()); + loadingManage.setTaskInfoJson(first.getTaskInfoJson()); + loadingManage.setDeptId(first.getDeptId()); + loadingManage.setDeptName(first.getDeptName()); + loadingManage.setBusinessStatus(STATUS_COMPLETED); + save(loadingManage); + + waybillMapper.update(null, Wrappers.lambdaUpdate() + .in(Waybill::getId, waybills.stream().map(Waybill::getId).toList()) + .set(Waybill::getLoadingNo, normalizedLoadingNo)); + } + @Override @Transactional(rollbackFor = Exception.class) public BusinessRemoveResultVO removeLoadingManage(String ids) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java index 71afae3..3a34240 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java @@ -22,6 +22,7 @@ import org.springblade.transport.excel.VehicleReconciliationExcel; import org.springblade.transport.excel.VehicleReconciliationFailureExcel; import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper; import org.springblade.transport.mapper.LoadingManageMapper; @@ -38,6 +39,7 @@ import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.MasterOrder; @@ -61,9 +63,13 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; import java.util.function.Function; import java.util.stream.Collectors; @@ -91,6 +97,7 @@ public class TransportReconciliationServiceImpl private final FormalSettlementSourceMapper formalSourceMapper; private final FormalSettlementDetailMapper formalDetailMapper; private final FormalSettlementDetailFeeMapper formalDetailFeeMapper; + private final FormalSettlementSummaryFeeMapper formalSummaryFeeMapper; private final ReceivablePayableDetailMapper receivablePayableMapper; private final ReceivablePayableCargoFeeMapper cargoFeeMapper; private final WaybillMapper waybillMapper; @@ -141,8 +148,11 @@ public class TransportReconciliationServiceImpl if (Func.isEmpty(vo.getCustomerName())) { vo.setCustomerName("receivable".equals(vo.getSettlementType()) ? vo.getPayerName() : vo.getPayeeName()); } - vo.setInternalDetails(internalRows(id)); - vo.setExternalDetails(externalRows(id)); + List internals = internalRows(id); + List externals = externalRows(id); + vo.setInternalDetails(internals); + vo.setExternalDetails(externals); + vo.setFeeSummary(feeSummary(vo.getFormalSettlementId(), externals)); vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() .eq(TransportReconciliationChangeRecord::getReconciliationId, id) .orderByDesc(TransportReconciliationChangeRecord::getChangeTime))); @@ -264,7 +274,12 @@ public class TransportReconciliationServiceImpl external.setExternalLineNo(index + 1); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); - external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目1", money(row.getFeeItemOne())))); + Map feeItems = row.getFeeItems(); + if (feeItems == null || feeItems.isEmpty()) { + feeItems = new HashMap<>(); + feeItems.put("费用项目1", money(row.getFeeItemOne())); + } + external.setFeeItemsJson(JsonUtil.toJson(feeItems)); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); external.setRawDataJson(JsonUtil.toJson(row)); @@ -274,9 +289,10 @@ public class TransportReconciliationServiceImpl private List validateImportVehicle(VehicleReconciliationExcel row) { List validationErrors = new ArrayList<>(); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, - Func.isEmpty(row.getVehicleNo()), "车牌号不能为空"); + row == null || Func.isEmpty(row.getVehicleNo()), "车牌号不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, - Func.isEmpty(row.getCargoName()), "货物名称不能为空"); + row == null || Func.isEmpty(row.getCargoName()), "货物名称不能为空"); + if (row == null) return validationErrors; org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(row.getActualDepartureTime()), "实际发货时间不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, @@ -296,7 +312,11 @@ public class TransportReconciliationServiceImpl addImportDecimalErrors(validationErrors, row.getMileage(), "里程(KM)", 2); addImportDecimalErrors(validationErrors, row.getUnitPrice(), "运输单价", 2); addImportDecimalErrors(validationErrors, row.getFreightAmount(), "运输费", 2); - addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "费用项目1", 2); + if (row.getFeeItems() != null && !row.getFeeItems().isEmpty()) { + row.getFeeItems().forEach((name, value) -> addImportDecimalErrors(validationErrors, value, name, 2)); + } else { + addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "费用项目1", 2); + } addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2); LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors); @@ -372,7 +392,13 @@ public class TransportReconciliationServiceImpl external.setExternalLineNo(index + 1); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); - external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目名称1", money(row.getFeeItemOne()), "费用项目名称2", money(row.getFeeItemTwo())))); + Map feeItems = row.getFeeItems(); + if (feeItems == null || feeItems.isEmpty()) { + feeItems = new HashMap<>(); + feeItems.put("水费", money(row.getFeeItemOne())); + feeItems.put("罚款", money(row.getFeeItemTwo())); + } + external.setFeeItemsJson(JsonUtil.toJson(feeItems)); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); external.setRawDataJson(JsonUtil.toJson(row)); @@ -408,8 +434,12 @@ public class TransportReconciliationServiceImpl 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.getFeeItemTwo(), "费用项目名称2", 2); + if (row.getFeeItems() != null) { + row.getFeeItems().forEach((name, value) -> addImportDecimalErrors(validationErrors, value, name, 2)); + } else { + addImportDecimalErrors(validationErrors, row.getFeeItemOne(), "水费", 2); + addImportDecimalErrors(validationErrors, row.getFeeItemTwo(), "罚款", 2); + } addImportDecimalErrors(validationErrors, row.getSettlementAmount(), "结算费用合计", 2); LocalDateTime departureTime = parseImportTime(row.getActualDepartureTime(), "实际发货时间", validationErrors); @@ -420,6 +450,7 @@ public class TransportReconciliationServiceImpl return validationErrors; } + @Override public TransportReconciliationVO matchPreview(TransportReconciliationVO request) { if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在"); @@ -430,6 +461,7 @@ public class TransportReconciliationServiceImpl if (externals == null) externals = externalRows(request.getId()); if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); resetPreviewMatches(internals, externals); + markSuspectedDuplicates(externals, false); Map> externalGroups = externals.stream() .collect(Collectors.groupingBy(this::matchKey)); Map> internalGroups = internals.stream() @@ -439,11 +471,6 @@ public class TransportReconciliationServiceImpl List internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); if (externalGroup.size() == 1 && internalGroup.size() == 1) { linkPreview(internalGroup.get(0), externalGroup.get(0)); - } else if (externalGroup.size() > 1) { - for (TransportReconciliationExternal external : externalGroup) { - external.setSuspectedDuplicate(true); - external.setMatchStatus(DUPLICATE); - } } } TransportReconciliationVO result = detail(request.getId()); @@ -459,6 +486,9 @@ public class TransportReconciliationServiceImpl if (request == null || request.getId() == null) throw new ServiceException("运输对账单不存在"); TransportReconciliation bill = editable(request.getId()); applySnapshots(bill, request.getInternalDetails(), request.getExternalDetails()); + if (request.getFeeSummary() != null) { + applyCompletionSummaryFees(bill.getFormalSettlementId(), request.getFeeSummary()); + } if (request.getReconciliationDate() != null) bill.setReconciliationDate(request.getReconciliationDate()); bill.setRemark(limit(request.getRemark(), 200)); updateById(bill); @@ -467,6 +497,93 @@ public class TransportReconciliationServiceImpl return detail(bill.getId()); } + /** + * 保存汇总对账弹窗中的费用行,并同步正式结算单金额。 + */ + private void applyCompletionSummaryFees(Long formalSettlementId, List incomingRows) { + if (formalSettlementId == null) throw new ServiceException("正式结算单不存在"); + List existingRows = formalSummaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0)); + Map existingById = existingRows.stream() + .filter(row -> row.getId() != null) + .collect(Collectors.toMap(FormalSettlementSummaryFee::getId, Function.identity())); + Set retainedManualIds = new HashSet<>(); + for (FormalSettlementSummaryFee incoming : incomingRows) { + if (incoming == null) continue; + boolean manual = Integer.valueOf(1).equals(incoming.getManualFlag()); + FormalSettlementSummaryFee row = incoming.getId() == null ? null : existingById.get(incoming.getId()); + if (manual) { + if (row != null && !Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("系统生成费用行不允许改为手工费用"); + } + if (Func.isEmpty(incoming.getFeeItem())) throw new ServiceException("手工费用项目不能为空"); + BigDecimal adjustAmount = money(incoming.getAdjustAmount()); + if ("补款".equals(incoming.getFeeItem()) && adjustAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("补款调整金额必须大于0"); + } + if ("扣款".equals(incoming.getFeeItem()) && adjustAmount.compareTo(BigDecimal.ZERO) >= 0) { + throw new ServiceException("扣款调整金额必须小于0"); + } + String feeType = Func.isEmpty(incoming.getFeeType()) ? "其他费用" : limit(incoming.getFeeType(), 50); + String feeItem = limit(incoming.getFeeItem(), 50); + if (row == null) { + row = new FormalSettlementSummaryFee(); + row.setFormalSettlementId(formalSettlementId); + row.setLineNo(existingRows.size() + 1); + row.setFeeType(feeType); + row.setFeeItem(feeItem); + row.setManualFlag(1); + row.setOriginalAmount(BigDecimal.ZERO); + } + row.setFeeType(feeType); + row.setFeeItem(feeItem); + row.setOriginalAmount(BigDecimal.ZERO); + row.setAdjustAmount(adjustAmount); + row.setSettlementAmount(adjustAmount); + row.setRemark(limit(incoming.getRemark(), 200)); + row.setManualFlag(1); + if (row.getId() == null) formalSummaryFeeMapper.insert(row); + else formalSummaryFeeMapper.updateById(row); + if (row.getId() != null) retainedManualIds.add(row.getId()); + continue; + } + if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("系统生成费用行不存在"); + } + BigDecimal originalAmount = money(row.getOriginalAmount()); + BigDecimal adjustAmount = money(incoming.getAdjustAmount()); + row.setOriginalAmount(originalAmount); + row.setAdjustAmount(adjustAmount); + row.setSettlementAmount(originalAmount.add(adjustAmount)); + row.setRemark(limit(incoming.getRemark(), 200)); + formalSummaryFeeMapper.updateById(row); + } + for (FormalSettlementSummaryFee row : existingRows) { + if (Integer.valueOf(1).equals(row.getManualFlag()) && !retainedManualIds.contains(row.getId())) { + formalSummaryFeeMapper.deleteById(row.getId()); + } + } + List finalRows = formalSummaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementSummaryFee::getLineNo)); + BigDecimal total = BigDecimal.ZERO; + int lineNo = 1; + for (FormalSettlementSummaryFee row : finalRows) { + row.setLineNo(lineNo++); + row.setSettlementAmount(money(row.getOriginalAmount()).add(money(row.getAdjustAmount()))); + total = total.add(money(row.getSettlementAmount())); + formalSummaryFeeMapper.updateById(row); + } + FormalSettlement formal = formalSettlementMapper.selectById(formalSettlementId); + if (formal == null) throw new ServiceException("正式结算单不存在"); + formal.setSettlementAmount(total); + formal.setLocalSettlementAmount(total.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); + formal.setRemainingPayableAmount(total.subtract(money(formal.getPaidAmount())).max(BigDecimal.ZERO)); + formalSettlementMapper.updateById(formal); + } + private void applySnapshots(TransportReconciliation bill, List incomingInternals, List incomingExternals) { @@ -516,7 +633,8 @@ public class TransportReconciliationServiceImpl target.setUpdateMessage(limit(source.getUpdateMessage(), 200)); } - private void applyExternalSnapshot(TransportReconciliationExternal target, TransportReconciliationExternal source) { + private void applyExternalSnapshot(TransportReconciliationExternal target, + TransportReconciliationExternal source) { if (source.getReconciliationId() != null && !Objects.equals(source.getReconciliationId(), target.getReconciliationId())) { throw new ServiceException("外部账单明细不属于当前对账单"); } @@ -551,6 +669,7 @@ public class TransportReconciliationServiceImpl List externals = externalRows(id); if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); resetMatches(internals, externals); + markSuspectedDuplicates(externals, true); Map> externalGroups = externals.stream() .collect(Collectors.groupingBy(this::matchKey)); Map> internalGroups = internals.stream() @@ -560,10 +679,6 @@ public class TransportReconciliationServiceImpl 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); - } } } refreshStats(id); @@ -632,6 +747,7 @@ public class TransportReconciliationServiceImpl internalMapper.updateById(internal); updateWaybill(internal, external); } + refreshFormalSummaryFees(bill.getFormalSettlementId()); recalculateSettlement(bill); bill.setBillUpdated(true); updateById(bill); @@ -643,6 +759,7 @@ public class TransportReconciliationServiceImpl List externals = externalRows(reconciliationId); if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); resetMatches(internals, externals); + markSuspectedDuplicates(externals, true); Map> externalGroups = externals.stream() .collect(Collectors.groupingBy(this::matchKey)); Map> internalGroups = internals.stream() @@ -652,12 +769,6 @@ public class TransportReconciliationServiceImpl 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)); @@ -771,6 +882,19 @@ public class TransportReconciliationServiceImpl ReceivablePayableCargoFee sourceFee = fee.getSourceFeeId() == null ? null : cargoFeeMapper.selectById(fee.getSourceFeeId()); if (sourceFee != null) { row.setSpecification(sourceFee.getSpecification()); row.setModel(sourceFee.getModel()); } } else { + List detailFees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()) + .eq(FormalSettlementDetailFee::getIsDeleted, 0)); + if (!detailFees.isEmpty()) { + Map feeItems = new LinkedHashMap<>(); + for (FormalSettlementDetailFee detailFee : detailFees) { + parseFeeItems(detailFee.getFeeItemsJson()).forEach((name, amount) -> + feeItems.merge(name, money(amount), BigDecimal::add)); + } + row.setFeeItemsJson(JsonUtil.toJson(feeItems)); + row.setFreightAmount(detailFees.stream().map(FormalSettlementDetailFee::getFreightAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + } row.setSettlementAmount(detail.getSettlementAmountTax()); } internalMapper.insert(row); @@ -820,14 +944,19 @@ public class TransportReconciliationServiceImpl private void applyAmount(TransportReconciliation bill, TransportReconciliationInternal internal, TransportReconciliationExternal external) { BigDecimal before = money(internal.getSettlementAmount()); BigDecimal after = money(external.getSettlementAmount()); + updateFormalSettlementHeader(internal, external, after); if (internal.getFormalSettlementDetailFeeId() != null) { FormalSettlementDetailFee fee = formalDetailFeeMapper.selectById(internal.getFormalSettlementDetailFeeId()); - fee.setSettlementAmountTax(after); fee.setAdjustAmount(after.subtract(money(fee.getOriginalAmount()))); formalDetailFeeMapper.updateById(fee); + if (fee != null) { + copyExternalToDetailFee(fee, external, after); + formalDetailFeeMapper.updateById(fee); + } if (internal.getSourceCargoFeeId() != null) { ReceivablePayableCargoFee sourceFee = cargoFeeMapper.selectById(internal.getSourceCargoFeeId()); if (sourceFee != null) { sourceFee.setAfterAmount(after); sourceFee.setAdjustAmount(after.subtract(money(sourceFee.getOriginalAmount()))); cargoFeeMapper.updateById(sourceFee); } } } else { + updateFormalSettlementDetailFees(internal, external, after); FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, internal.getFormalSettlementDetailId())); @@ -854,6 +983,154 @@ public class TransportReconciliationServiceImpl record.setChangeReason("运输对账按匹配结果更新"); changeRecordMapper.insert(record); } + private void updateFormalSettlementHeader(TransportReconciliationInternal internal, + TransportReconciliationExternal external, BigDecimal settlementAmount) { + if (internal.getFormalSettlementDetailId() == null) return; + FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); + if (detail == null) return; + detail.setVehicleNo(external.getVehicleNo()); + detail.setDepartureAddress(external.getDepartureAddress()); + detail.setArrivalAddress(external.getArrivalAddress()); + detail.setActualDepartureTime(external.getActualDepartureTime()); + detail.setActualCompletionTime(external.getActualCompletionTime()); + detail.setTransportType(external.getTransportType()); + detail.setCargoName(external.getCargoName()); + detail.setCargoType(external.getCargoType()); + detail.setTransportQuantity(external.getTransportQuantity()); + detail.setQuantityUnit(external.getQuantityUnit()); + detail.setMileage(external.getMileage()); + detail.setBatchNo(external.getBatchNo()); + detail.setUnitPrice(external.getUnitPrice()); + detail.setFreightAmount(external.getFreightAmount()); + detail.setFeeItemsJson(external.getFeeItemsJson()); + detail.setSettlementAmountTax(settlementAmount); + detail.setAdjustAmount(settlementAmount.subtract(money(detail.getOriginalAmount()))); + formalDetailMapper.updateById(detail); + } + + private void updateFormalSettlementDetailFees(TransportReconciliationInternal internal, + TransportReconciliationExternal external, BigDecimal settlementAmount) { + if (internal.getFormalSettlementDetailId() == null) return; + FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); + if (detail == null) return; + + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()) + .eq(FormalSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetailFee::getLineNo)); + if (fees.size() == 1) { + FormalSettlementDetailFee fee = fees.get(0); + copyExternalToDetailFee(fee, external, settlementAmount); + formalDetailFeeMapper.updateById(fee); + } else if (fees.size() > 1) { + Map externalFeeItems = parseFeeItems(external.getFeeItemsJson()); + List> matchedItems = new ArrayList<>(); + List amounts = new ArrayList<>(); + for (int index = 0; index < fees.size(); index++) { + FormalSettlementDetailFee fee = fees.get(index); + Map feeItems = parseFeeItems(fee.getFeeItemsJson()); + Map matchedFeeItems = new LinkedHashMap<>(); + feeItems.keySet().forEach(name -> { + BigDecimal amount = externalFeeItems.get(name); + if (amount != null) matchedFeeItems.put(name, money(amount)); + }); + matchedItems.add(matchedFeeItems); + amounts.add(matchedFeeItems.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add)); + } + BigDecimal allocated = amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal residual = settlementAmount.subtract(allocated); + if (!amounts.isEmpty()) amounts.set(0, amounts.get(0).add(residual)); + for (int index = 0; index < fees.size(); index++) { + FormalSettlementDetailFee fee = fees.get(index); + copyExternalToDetailFee(fee, external, amounts.get(index), matchedItems.get(index), false); + formalDetailFeeMapper.updateById(fee); + } + } + } + + private void copyExternalToDetailFee(FormalSettlementDetailFee fee, + TransportReconciliationExternal external, BigDecimal settlementAmount) { + copyExternalToDetailFee(fee, external, settlementAmount, parseFeeItems(external.getFeeItemsJson())); + } + + private void copyExternalToDetailFee(FormalSettlementDetailFee fee, + TransportReconciliationExternal external, BigDecimal settlementAmount, + Map feeItems) { + copyExternalToDetailFee(fee, external, settlementAmount, feeItems, true); + } + + private void copyExternalToDetailFee(FormalSettlementDetailFee fee, + TransportReconciliationExternal external, BigDecimal settlementAmount, + Map feeItems, boolean copyTransportFields) { + if (!copyTransportFields) { + fee.setFeeItemsJson(JsonUtil.toJson(feeItems)); + fee.setSettlementAmountTax(settlementAmount); + fee.setAdjustAmount(settlementAmount.subtract(money(fee.getOriginalAmount()))); + return; + } + fee.setTransportQuantity(external.getTransportQuantity()); + fee.setQuantityUnit(external.getQuantityUnit()); + fee.setMileage(external.getMileage()); + fee.setUnitPrice(external.getUnitPrice()); + fee.setFreightAmount(external.getFreightAmount()); + fee.setFeeItemsJson(JsonUtil.toJson(feeItems)); + fee.setSettlementAmountTax(settlementAmount); + fee.setAdjustAmount(settlementAmount.subtract(money(fee.getOriginalAmount()))); + } + + private List feeSummary(Long formalSettlementId, + List externals) { + if (formalSettlementId == null) return List.of(); + Map externalFees = new LinkedHashMap<>(); + for (TransportReconciliationExternal external : externals) { + externalFees.merge("运输费", money(external.getFreightAmount()), BigDecimal::add); + parseFeeItems(external.getFeeItemsJson()).forEach((name, amount) -> + externalFees.merge(name, money(amount), BigDecimal::add)); + } + return formalSummaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementSummaryFee::getLineNo)).stream() + .peek(summary -> { + BigDecimal after = externalFees.get(summary.getFeeItem()); + if (after != null) { + summary.setSettlementAmount(after); + summary.setAdjustAmount(after.subtract(money(summary.getOriginalAmount()))); + } + }).toList(); + } + + private void refreshFormalSummaryFees(Long formalSettlementId) { + if (formalSettlementId == null) return; + Map currentAmounts = new LinkedHashMap<>(); + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId)); + for (FormalSettlementDetail detail : details) { + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()) + .eq(FormalSettlementDetailFee::getIsDeleted, 0)); + for (FormalSettlementDetailFee fee : fees) { + Map items = parseFeeItems(fee.getFeeItemsJson()); + boolean containsFreight = items.keySet().stream().anyMatch(this::isFreightFeeItem); + if (!containsFreight) currentAmounts.merge("运输费", money(fee.getFreightAmount()), BigDecimal::add); + items.forEach((name, amount) -> currentAmounts.merge(name, money(amount), BigDecimal::add)); + } + } + for (FormalSettlementSummaryFee summary : formalSummaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, formalSettlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0))) { + if (Integer.valueOf(1).equals(summary.getManualFlag())) continue; + BigDecimal amount = money(currentAmounts.get(summary.getFeeItem())); + summary.setSettlementAmount(amount); + summary.setAdjustAmount(amount.subtract(money(summary.getOriginalAmount()))); + formalSummaryFeeMapper.updateById(summary); + } + } + + private boolean isFreightFeeItem(String name) { + return name != null && (name.contains("运费") || name.contains("运输费")); + } + private void recalculateSettlement(TransportReconciliation bill) { List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, bill.getFormalSettlementId())); @@ -1027,10 +1304,70 @@ public class TransportReconciliationServiceImpl private String matchKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getCargoType()); } + + private String duplicateKey(TransportReconciliationExternal row) { + return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), + row.getActualDepartureTime(), row.getActualCompletionTime(), row.getTransportType(), + row.getCargoName(), row.getCargoType(), row.getSpecification(), row.getModel(), + row.getTransportQuantity(), row.getQuantityUnit(), row.getMileage(), row.getBatchNo(), + row.getUnitPrice(), row.getFreightAmount(), duplicateFeeItemsKey(row.getFeeItemsJson()), row.getSettlementAmount()); + } + + private String duplicateFeeItemsKey(String feeItemsJson) { + if (Func.isEmpty(feeItemsJson)) return ""; + try { + Map feeItems = JsonUtil.parse(feeItemsJson, Map.class); + StringJoiner joiner = new StringJoiner(","); + feeItems.entrySet().stream().sorted(Map.Entry.comparingByKey()) + .forEach(entry -> joiner.add(normal(entry.getKey()) + "=" + normal(entry.getValue()))); + return joiner.toString(); + } catch (Exception exception) { + return normal(feeItemsJson); + } + } + + @SuppressWarnings("unchecked") + private Map parseFeeItems(String feeItemsJson) { + if (Func.isEmpty(feeItemsJson)) return Map.of(); + try { + Map source = JsonUtil.parse(feeItemsJson, Map.class); + Map result = new LinkedHashMap<>(); + source.forEach((name, value) -> { + if (Func.isEmpty(name) || value == null) return; + try { + result.put(name, new BigDecimal(String.valueOf(value))); + } catch (NumberFormatException ignored) { + // 忽略无法转换的费用金额 + } + }); + return result; + } catch (Exception exception) { + return Map.of(); + } + } + + private void markSuspectedDuplicates(List externals, boolean persist) { + Map> duplicateGroups = externals.stream() + .collect(Collectors.groupingBy(this::duplicateKey)); + for (List group : duplicateGroups.values()) { + if (group.size() <= 1) continue; + for (TransportReconciliationExternal external : group) { + external.setSuspectedDuplicate(true); + external.setMatchStatus(DUPLICATE); + if (persist) externalMapper.updateById(external); + } + } + } private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); } private String normal(Object value) { if (value == null) return ""; - if (value instanceof BigDecimal decimal) return decimal.stripTrailingZeros().toPlainString(); + if (value instanceof Number number) { + try { + return new BigDecimal(number.toString()).stripTrailingZeros().toPlainString(); + } catch (NumberFormatException ignored) { + return number.toString(); + } + } if (value instanceof List list) return JsonUtil.toJson(list); return value.toString().trim().replaceAll("\\s+", "").toLowerCase(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java index 829aaa4..4d3a270 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java @@ -21,7 +21,9 @@ import org.springblade.core.tool.utils.WebUtil; import org.springblade.system.cache.DictCache; import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.UserCache; +import org.springblade.system.feign.ISysClient; import org.springblade.system.pojo.entity.DictBiz; +import org.springblade.system.pojo.entity.CargoType; import org.springblade.transport.excel.WaybillImportBatchExcel; import org.springblade.transport.mapper.WaybillImportBatchMapper; import org.springblade.transport.pojo.dto.WaybillImportBatchRequest; @@ -35,6 +37,7 @@ 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.ILoadingManageService; import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.IWaybillImportBatchService; import org.springblade.transport.service.IWaybillService; @@ -50,9 +53,11 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -70,14 +75,20 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows(); List waybills = new ArrayList<>(); + Map> loadingWaybills = new TreeMap<>(); for (int index = 0; index < rows.size(); index++) { try { + String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号"); Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft); if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill); waybills.add(waybill); + if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) { + loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill); + } } catch (Exception exception) { throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage()); } } + loadingWaybills.forEach(loadingManageService::createFromImportedWaybills); batch.setWaybillCount(waybills.size()); updateById(batch); // 导入完成且导入类型为运单时,按合同费用生成模式(系统生成)同步生成应收应付明细。 @@ -262,7 +279,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl transportTypeOptions = loadTransportTypeOptions(); List quantityUnitOptions = loadQuantityUnitOptions(); + Set cargoTypeOptions = loadCargoTypeOptions(); // 构建配载标识号和同一运单标识号的映射 Map> loadingIdentifierMap = new HashMap<>(); Map> waybillIdentifierMap = new HashMap<>(); - Map loadingIdentifierCountMap = new HashMap<>(); for (int i = 0; i < rows.size(); i++) { Map row = rows.get(i); @@ -437,7 +454,6 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl new ArrayList<>()).add(i); - loadingIdentifierCountMap.merge(loadingIdentifier, 1, Integer::sum); } if (Func.isNotEmpty(waybillIdentifier)) { @@ -464,33 +480,41 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl 1 && sameIdentifierRows.indexOf(rowIndex) > 0) { - errors.add("配载标识号重复"); - } } private void validateVehicleNo(Map row, List errors) { - String vehicleNo = stringValue(row, "vehicleNo"); + String vehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"); String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"); - if (Func.isEmpty(vehicleNo) || Func.isEmpty(transportType)) { + if (Func.isEmpty(transportType)) { return; } - // 当运输类型为公路整车或公路配载/零担时,校验车牌号格式 - if (transportType.contains("公路整车") || transportType.contains("公路配载") || transportType.contains("公路零担")) { - if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) { - errors.add("车牌号格式不正确,应为首位汉字、次位大写字母、总长度7或8位"); + // 公路运输必须填写车牌号;其他运输方式的该字段可填写航班号、船号或班列号。 + if (!isRoadTransport(transportType)) { + return; + } + if (Func.isEmpty(vehicleNo)) { + errors.add("公路运输时车牌号/航班号/船号/班列号不能为空"); + return; + } + if (vehicleNo.length() < 7) { + errors.add("车牌号长度不能少于7位"); + } + if (vehicleNo.length() > 8) { + errors.add("车牌号长度不能超过8位"); + } + if (!VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()) { + errors.add("车牌号首位必须是省份简称,第二位必须是英文字母"); + } + if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) { + errors.add("车牌号格式不正确,应为首位省份简称、次位英文字母、总长度7或8位"); + } + } + + private boolean isRoadTransport(String transportType) { + String value = transportType == null ? "" : transportType.trim().toLowerCase(); + return value.contains("公路") || value.contains("道路") || value.contains("road") || "gl".equals(value); + } + + private void validateAddressDetail(Map row, String field, String fieldName, List errors) { + String address = stringValue(row, field, fieldName); + if (Func.isEmpty(address)) { + errors.add(fieldName + "不能为空"); + return; + } + String normalizedAddress = address.trim(); + String[] addressParts = normalizedAddress.split("[\\s//,,;;||>]+", -1); + boolean hasDetailPart = addressParts.length > 3; + if (hasDetailPart) { + for (int index = 3; index < addressParts.length; index++) { + if (Func.isNotEmpty(addressParts[index])) { + return; + } } } + if (!REGION_ONLY_ADDRESS_PATTERN.matcher(normalizedAddress.replaceAll("[\\s//,,;;||>]+", "")).matches()) { + return; + } + errors.add(fieldName + "必须包含省市区以外的详细地址"); + } + + private void validateRemark(Map row, List errors) { + String remark = stringValue(row, "remark", "备注"); + if (Func.isNotEmpty(remark) && remark.length() > 200) { + errors.add("备注不能超过200个字"); + } + } + + private void validateCargoFields(Map row, Set cargoTypeOptions, List errors) { + String cargoName = stringValue(row, "cargoName", "货物名称"); + if (Func.isEmpty(cargoName)) { + errors.add("货物名称不能为空"); + } + String cargoType = stringValue(row, "cargoType", "货物类型"); + if (Func.isEmpty(cargoType)) { + errors.add("货物类型不能为空"); + return; + } + boolean exists = cargoTypeOptions.contains(cargoType.trim()) + || cargoTypeOptions.stream().anyMatch(value -> value.equalsIgnoreCase(cargoType.trim())); + if (!exists) { + errors.add("货物类型必须在/base/cargo-type中存在"); + } } private void validateTransportType(Map row, Map transportTypeOptions, List errors) { @@ -721,12 +806,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl loadCargoTypeOptions() { + R> response = sysClient.getCargoTypes(); + if (response == null || !response.isSuccess() || response.getData() == null) { + throw new ServiceException("货物类型基础数据读取失败,请稍后重试"); + } + Set options = new HashSet<>(); + for (CargoType cargoType : response.getData()) { + if (cargoType == null) continue; + if (Func.isNotEmpty(cargoType.getCargoName())) options.add(cargoType.getCargoName().trim()); + if (Func.isNotEmpty(cargoType.getCargoCode())) options.add(cargoType.getCargoCode().trim()); + } + return options; + } + private void addTransportTypeOption(Map options, String dictKey, String dictValue) { if (Func.isNotEmpty(dictKey)) options.put(dictKey.trim(), dictKey.trim()); if (Func.isNotEmpty(dictValue)) options.put(dictValue.trim(), Func.isNotEmpty(dictKey) ? dictKey.trim() : dictValue.trim()); @@ -808,7 +907,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl