✨ 新增导入结算单,并修复运单超管组织过滤与计量单位软删恢复
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+20
@@ -34,6 +34,26 @@ import java.util.List;
|
||||
*/
|
||||
public interface MeasurementUnitMapper extends BaseMapper<MeasurementUnit> {
|
||||
|
||||
/**
|
||||
* 按编码查询计量单位(包含逻辑删除记录,用于新增恢复)。
|
||||
*/
|
||||
MeasurementUnit selectByUnitCodeIncludingDeleted(@Param("unitCode") String unitCode);
|
||||
|
||||
/**
|
||||
* 按名称查询计量单位(包含逻辑删除记录,用于新增恢复)。
|
||||
*/
|
||||
MeasurementUnit selectByUnitNameIncludingDeleted(@Param("unitName") String unitName);
|
||||
|
||||
/**
|
||||
* 恢复逻辑删除计量单位。
|
||||
*/
|
||||
int restoreById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 释放已软删除记录占用的单位名称,避免唯一索引冲突。
|
||||
*/
|
||||
int releaseUnitName(@Param("id") Long id, @Param("unitName") String unitName);
|
||||
|
||||
/**
|
||||
* 自定义分页
|
||||
*
|
||||
|
||||
+28
@@ -19,6 +19,34 @@
|
||||
<result column="remark" property="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByUnitCodeIncludingDeleted" resultType="org.springblade.system.pojo.entity.MeasurementUnit">
|
||||
SELECT *
|
||||
FROM blade_measurement_unit
|
||||
WHERE unit_code = #{unitCode}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectByUnitNameIncludingDeleted" resultType="org.springblade.system.pojo.entity.MeasurementUnit">
|
||||
SELECT *
|
||||
FROM blade_measurement_unit
|
||||
WHERE unit_name = #{unitName}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<update id="restoreById">
|
||||
UPDATE blade_measurement_unit
|
||||
SET is_deleted = 0
|
||||
WHERE id = #{id}
|
||||
AND is_deleted = 1
|
||||
</update>
|
||||
|
||||
<update id="releaseUnitName">
|
||||
UPDATE blade_measurement_unit
|
||||
SET unit_name = #{unitName}
|
||||
WHERE id = #{id}
|
||||
AND is_deleted = 1
|
||||
</update>
|
||||
|
||||
<select id="selectMeasurementUnitPage" resultMap="measurementUnitResultMap">
|
||||
SELECT
|
||||
mmu.id,
|
||||
|
||||
+32
@@ -62,10 +62,42 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(MeasurementUnit measurementUnit) {
|
||||
prepare(measurementUnit);
|
||||
prepareSubmitTarget(measurementUnit);
|
||||
validate(measurementUnit);
|
||||
return saveOrUpdate(measurementUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码或名称已存在且为软删除时:取消软删除并复用原主键,后续按更新落库;
|
||||
* 存在且未删除时仍由唯一校验提示「已存在」。
|
||||
*/
|
||||
private void prepareSubmitTarget(MeasurementUnit measurementUnit) {
|
||||
if (Func.isNotEmpty(measurementUnit.getId())) {
|
||||
return;
|
||||
}
|
||||
MeasurementUnit existingByCode = baseMapper.selectByUnitCodeIncludingDeleted(measurementUnit.getUnitCode());
|
||||
MeasurementUnit existingByName = baseMapper.selectByUnitNameIncludingDeleted(measurementUnit.getUnitName());
|
||||
MeasurementUnit toRestore = null;
|
||||
if (existingByCode != null && Objects.equals(existingByCode.getIsDeleted(), 1)) {
|
||||
toRestore = existingByCode;
|
||||
} else if (existingByName != null && Objects.equals(existingByName.getIsDeleted(), 1)
|
||||
&& (existingByCode == null || Objects.equals(existingByCode.getId(), existingByName.getId()))) {
|
||||
toRestore = existingByName;
|
||||
}
|
||||
if (toRestore == null) {
|
||||
return;
|
||||
}
|
||||
// 复用编码恢复时,若另一条软删除记录占用了目标名称,先释放名称唯一索引
|
||||
if (existingByName != null && Objects.equals(existingByName.getIsDeleted(), 1)
|
||||
&& !Objects.equals(existingByName.getId(), toRestore.getId())) {
|
||||
String releasedName = existingByName.getUnitName() + "_DEL_" + existingByName.getId();
|
||||
baseMapper.releaseUnitName(existingByName.getId(), releasedName);
|
||||
}
|
||||
baseMapper.restoreById(toRestore.getId());
|
||||
measurementUnit.setId(toRestore.getId());
|
||||
measurementUnit.setIsDeleted(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean changeStatus(Long id, Integer status) {
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportBatchRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportVoidRequest;
|
||||
import org.springblade.transport.pojo.vo.SettlementImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.service.ISettlementImportBatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "settlement_import")
|
||||
@RequestMapping("settlement-import")
|
||||
@Tag(name = "导入结算单", description = "导入结算单管理")
|
||||
public class SettlementImportController extends BladeController {
|
||||
|
||||
private final ISettlementImportBatchService settlementImportBatchService;
|
||||
|
||||
@GetMapping("/options")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "筛选项与表单选项")
|
||||
public R<Map<String, List<Map<String, Object>>>> options() {
|
||||
return R.data(settlementImportBatchService.options());
|
||||
}
|
||||
|
||||
@GetMapping("/next-code")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "下一个批次号")
|
||||
public R<String> nextCode() {
|
||||
return R.data(settlementImportBatchService.nextBatchNo());
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "导入结算单批次分页")
|
||||
public R<IPage<SettlementImportBatchVO>> list(SettlementImportBatchRequest request, Query query) {
|
||||
return R.data(settlementImportBatchService.page(Condition.getPage(query), request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "导入结算单批次详情")
|
||||
public R<SettlementImportBatchVO> detail(@RequestParam Long id) {
|
||||
return R.data(settlementImportBatchService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导出导入模板")
|
||||
public void exportTemplate(HttpServletResponse response) {
|
||||
settlementImportBatchService.exportTemplate(response);
|
||||
}
|
||||
|
||||
@PostMapping("/parse")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "解析导入 Excel")
|
||||
public R<Map<String, Object>> parse(@RequestPart("file") MultipartFile file) {
|
||||
return R.data(settlementImportBatchService.parse(file));
|
||||
}
|
||||
|
||||
@PostMapping("/validate")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "校验导入数据")
|
||||
public R<WaybillImportValidateResultVO> validate(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.validate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/export-failures")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "导出失败明细")
|
||||
public void exportFailures(@RequestBody SettlementImportBatchRequest request, HttpServletResponse response) {
|
||||
settlementImportBatchService.exportFailures(request, null, response);
|
||||
}
|
||||
|
||||
@PostMapping("/draft")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "保存草稿")
|
||||
public R<SettlementImportBatchVO> draft(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.saveDraft(request));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "确认导入结算单")
|
||||
public R<SettlementImportBatchVO> confirm(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.confirm(request));
|
||||
}
|
||||
|
||||
@PostMapping("/void")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "作废导入结算单批次")
|
||||
public R voidBatch(@RequestBody SettlementImportVoidRequest request) {
|
||||
settlementImportBatchService.voidBatch(request);
|
||||
return R.success("作废成功");
|
||||
}
|
||||
}
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.springblade.common.excel.ImportCellReadException;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 读取器(Apache POI)。
|
||||
* <p>
|
||||
* 定位「*运输费」或「运输费」列,其后直到「结算金额」为动态费用列。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class SettlementImportFeeReader {
|
||||
|
||||
private static final DataFormatter DATA_FORMATTER = new DataFormatter(Locale.CHINA);
|
||||
|
||||
private SettlementImportFeeReader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
*/
|
||||
public static final class ParseResult {
|
||||
private final List<Map<String, Object>> rows;
|
||||
private final List<String> feeItemNames;
|
||||
private final List<String> warnings;
|
||||
|
||||
public ParseResult(List<Map<String, Object>> rows, List<String> feeItemNames, List<String> warnings) {
|
||||
this.rows = rows;
|
||||
this.feeItemNames = feeItemNames;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public List<String> getFeeItemNames() {
|
||||
return feeItemNames;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
|
||||
public static ParseResult read(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
try (InputStream inputStream = file.getInputStream(); Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||
Sheet sheet = resolveDataSheet(workbook);
|
||||
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||
if (headerRow == null) {
|
||||
throw new ServiceException("未识别到表头,请使用标准导入结算单模板");
|
||||
}
|
||||
Map<Integer, String> headers = readHeaderMap(headerRow);
|
||||
int transportFeeColumn = findTransportFeeColumn(headers);
|
||||
if (transportFeeColumn < 0) {
|
||||
throw new ServiceException("未找到「*运输费」或「运输费」列");
|
||||
}
|
||||
int settlementAmountColumn = findColumnExact(headers, "结算金额");
|
||||
List<Integer> feeColumns = new ArrayList<>();
|
||||
List<String> feeItemNames = new ArrayList<>();
|
||||
int endColumn = settlementAmountColumn > transportFeeColumn
|
||||
? settlementAmountColumn
|
||||
: headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1;
|
||||
for (int column = transportFeeColumn + 1; column < endColumn; column++) {
|
||||
String name = headers.get(column);
|
||||
if (name == null || name.isBlank() || "结算金额".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
feeColumns.add(column);
|
||||
feeItemNames.add(name);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
int firstDataRow = sheet.getFirstRowNum() + 1;
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int rowIndex = firstDataRow; rowIndex <= lastRow; rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> mapped = mapRow(headers, feeColumns, row, rowIndex);
|
||||
if (mapped != null) {
|
||||
rows.add(mapped);
|
||||
}
|
||||
}
|
||||
return new ParseResult(rows, feeItemNames, List.of());
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("读取导入结算单 Excel 失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 优先取含「*运输费/运输费」表头的工作表,否则取第一个非空表。 */
|
||||
private static Sheet resolveDataSheet(Workbook workbook) {
|
||||
Sheet fallback = null;
|
||||
for (int index = 0; index < workbook.getNumberOfSheets(); index++) {
|
||||
Sheet sheet = workbook.getSheetAt(index);
|
||||
if (sheet == null) {
|
||||
continue;
|
||||
}
|
||||
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||
if (headerRow == null) {
|
||||
continue;
|
||||
}
|
||||
Map<Integer, String> headers = readHeaderMap(headerRow);
|
||||
if (findTransportFeeColumn(headers) >= 0) {
|
||||
return sheet;
|
||||
}
|
||||
if (fallback == null && !headers.isEmpty()) {
|
||||
fallback = sheet;
|
||||
}
|
||||
}
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
throw new ServiceException("Excel 中未找到有效工作表");
|
||||
}
|
||||
|
||||
private static Map<Integer, String> readHeaderMap(Row headerRow) {
|
||||
Map<Integer, String> headers = new LinkedHashMap<>();
|
||||
short lastCell = headerRow.getLastCellNum();
|
||||
for (int column = 0; column < lastCell; column++) {
|
||||
String value = normalizeHeader(cellString(headerRow.getCell(column)));
|
||||
if (value != null && !value.isBlank()) {
|
||||
headers.put(column, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** 去掉 BOM/不可见空白,全角星号转半角,便于匹配「*运输费」。 */
|
||||
private static String normalizeHeader(String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
String text = raw.replace("\uFEFF", "")
|
||||
.replace('\u00A0', ' ')
|
||||
.replace('*', '*')
|
||||
.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static Map<String, Object> mapRow(Map<Integer, String> headers, List<Integer> feeColumns,
|
||||
Row row, int excelRowIndex) {
|
||||
Map<String, Object> mapped = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
for (Map.Entry<Integer, String> entry : headers.entrySet()) {
|
||||
int column = entry.getKey();
|
||||
String trimmedHeader = entry.getValue();
|
||||
String cell = cellString(row.getCell(column));
|
||||
if (feeColumns.contains(column)) {
|
||||
feeItems.put(trimmedHeader, decimal(cell, trimmedHeader, excelRowIndex));
|
||||
continue;
|
||||
}
|
||||
String field = FIXED_HEADER_MAP.get(trimmedHeader);
|
||||
if (field == null) {
|
||||
continue;
|
||||
}
|
||||
if ("transportFee".equals(field) || "unitPrice".equals(field)
|
||||
|| "quantity".equals(field) || "mileage".equals(field)
|
||||
|| "settlementAmount".equals(field)) {
|
||||
mapped.put(field, decimal(cell, trimmedHeader, excelRowIndex));
|
||||
} else if ("serialNumber".equals(field)) {
|
||||
mapped.put(field, intValue(cell));
|
||||
} else {
|
||||
mapped.put(field, blankToNull(cell));
|
||||
}
|
||||
mapped.put(trimmedHeader, mapped.get(field));
|
||||
}
|
||||
|
||||
boolean hasBusinessValue = mapped.values().stream().anyMatch(value -> {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof String text) {
|
||||
return !text.isBlank();
|
||||
}
|
||||
return true;
|
||||
}) || feeItems.values().stream().anyMatch(amount -> amount != null && amount.compareTo(BigDecimal.ZERO) != 0);
|
||||
if (!hasBusinessValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigDecimal transportFee = toDecimal(mapped.get("transportFee"));
|
||||
BigDecimal feeItemTotal = feeItems.values().stream()
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
mapped.put("settlementAmount", settlementAmount);
|
||||
mapped.put("结算金额", settlementAmount);
|
||||
mapped.put("feeItems", feeItems);
|
||||
mapped.put("freight", transportFee);
|
||||
mapped.put("运费", transportFee);
|
||||
mapped.put("*运输费", transportFee);
|
||||
mapped.put("运输费", transportFee);
|
||||
mapped.put("otherFeeTotal", feeItemTotal);
|
||||
mapped.put("其他费用合计", feeItemTotal);
|
||||
mapped.put("freightTotal", settlementAmount);
|
||||
mapped.put("运费合计", settlementAmount);
|
||||
if (mapped.get("unitPrice") != null) {
|
||||
mapped.put("单价", mapped.get("unitPrice"));
|
||||
mapped.put("运输单价", mapped.get("unitPrice"));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static int findTransportFeeColumn(Map<Integer, String> headers) {
|
||||
int starred = findColumnExact(headers, "*运输费");
|
||||
if (starred >= 0) {
|
||||
return starred;
|
||||
}
|
||||
return findColumnExact(headers, "运输费");
|
||||
}
|
||||
|
||||
private static int findColumnExact(Map<Integer, String> headers, String header) {
|
||||
return headers.entrySet().stream()
|
||||
.filter(entry -> header.equals(entry.getValue()))
|
||||
.mapToInt(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElse(-1);
|
||||
}
|
||||
|
||||
private static String cellString(Cell cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
String text = DATA_FORMATTER.formatCellValue(cell);
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
text = text.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(String cell, String columnName, Integer excelRowIndex) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
String normalized = cell.trim().replace(",", "");
|
||||
try {
|
||||
return new BigDecimal(normalized).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new ImportCellReadException(excelRowIndex, columnName, cell.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static Integer intValue(String cell) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(cell.trim().replace(",", "")).intValue();
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankToNull(String cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
String text = cell.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static BigDecimal toDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
if (value instanceof BigDecimal bigDecimal) {
|
||||
return bigDecimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value).trim()).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException exception) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<String, String> FIXED_HEADER_MAP = buildFixedHeaderMap();
|
||||
|
||||
private static Map<String, String> buildFixedHeaderMap() {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
map.put("序号", "serialNumber");
|
||||
map.put("原始单号", "originalNo");
|
||||
map.put("配载标识号", "loadingIdentifier");
|
||||
map.put("*车牌号/船班号/船次号/班次号", "vehicleNo");
|
||||
map.put("车牌号/船班号/船次号/班次号", "vehicleNo");
|
||||
map.put("*车牌号/航班号/船号/班列号", "vehicleNo");
|
||||
map.put("车牌号/航班号/船号/班列号", "vehicleNo");
|
||||
map.put("*运输方式", "transportType");
|
||||
map.put("运输方式", "transportType");
|
||||
map.put("运输类型", "transportType");
|
||||
map.put("*运输类型", "transportType");
|
||||
map.put("司机/船长姓名", "driverName");
|
||||
map.put("司机/船长手机号", "driverPhone");
|
||||
map.put("*发货地址", "departureAddress");
|
||||
map.put("发货地址", "departureAddress");
|
||||
map.put("发货联系人", "departureContact");
|
||||
map.put("发货联系人电话", "departurePhone");
|
||||
map.put("*到货地址", "arrivalAddress");
|
||||
map.put("到货地址", "arrivalAddress");
|
||||
map.put("收货联系人", "arrivalContact");
|
||||
map.put("收货联系人电话", "arrivalPhone");
|
||||
map.put("*货物名称", "cargoName");
|
||||
map.put("货物名称", "cargoName");
|
||||
map.put("*货物类型", "cargoType");
|
||||
map.put("货物类型", "cargoType");
|
||||
map.put("包装", "packageType");
|
||||
map.put("*数量", "quantity");
|
||||
map.put("数量", "quantity");
|
||||
map.put("*数量单位", "quantityUnit");
|
||||
map.put("数量单位", "quantityUnit");
|
||||
map.put("规格", "specification");
|
||||
map.put("型号", "model");
|
||||
map.put("里程(km)", "mileage");
|
||||
map.put("里程", "mileage");
|
||||
map.put("运输单价", "unitPrice");
|
||||
map.put("单价", "unitPrice");
|
||||
map.put("*运输费", "transportFee");
|
||||
map.put("运输费", "transportFee");
|
||||
map.put("结算金额", "settlementAmount");
|
||||
map.put("*实际发货时间", "actualStartDate");
|
||||
map.put("实际发货时间", "actualStartDate");
|
||||
map.put("*实际完成时间", "actualEndDate");
|
||||
map.put("实际完成时间", "actualEndDate");
|
||||
map.put("预计发货时间", "planStartDate");
|
||||
map.put("预计完成时间", "planEndDate");
|
||||
map.put("备注", "remark");
|
||||
map.put("同一运单标识号", "waybillIdentifier");
|
||||
return map;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 模板(固定列 + 运输单价 + *运输费 + 示例费用项)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(28)
|
||||
@ContentRowHeight(18)
|
||||
public class SettlementImportTemplateExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("序号")
|
||||
private Integer serialNumber;
|
||||
@ExcelProperty("原始单号")
|
||||
private String originalNo;
|
||||
@ExcelProperty("配载标识号")
|
||||
private String loadingIdentifier;
|
||||
@ExcelProperty("*车牌号/航班号/船号/班列号")
|
||||
private String vehicleNo;
|
||||
@ExcelProperty("*运输方式")
|
||||
private String transportType;
|
||||
@ExcelProperty("司机/船长姓名")
|
||||
private String driverName;
|
||||
@ExcelProperty("司机/船长手机号")
|
||||
private String driverPhone;
|
||||
@ExcelProperty("*发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系人电话")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("*到货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系人电话")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("*货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("*货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
@ExcelProperty("*数量")
|
||||
private BigDecimal quantity;
|
||||
@ExcelProperty("*数量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("运输单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("*运输费")
|
||||
private BigDecimal transportFee;
|
||||
@ExcelProperty("装卸费")
|
||||
private BigDecimal loadingFee;
|
||||
@ExcelProperty("过路费")
|
||||
private BigDecimal tollFee;
|
||||
@ExcelProperty("结算金额")
|
||||
private BigDecimal settlementAmount;
|
||||
@ExcelProperty("*实际发货时间")
|
||||
private String actualStartDate;
|
||||
@ExcelProperty("*实际完成时间")
|
||||
private String actualEndDate;
|
||||
@ExcelProperty("预计发货时间")
|
||||
private String planStartDate;
|
||||
@ExcelProperty("预计完成时间")
|
||||
private String planEndDate;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("同一运单标识号")
|
||||
private String waybillIdentifier;
|
||||
|
||||
/** 导入失败原因(不导出到模板,仅用于失败明细) */
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
}
|
||||
+1
-2
@@ -101,8 +101,7 @@
|
||||
approved_time,
|
||||
kingdee_status,
|
||||
kingdee_fail_reason,
|
||||
kingdee_sync_time
|
||||
approved_time,
|
||||
kingdee_sync_time,
|
||||
(
|
||||
SELECT s.review_status
|
||||
FROM blade_customer_credit_score s
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
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.springblade.transport.pojo.entity.SettlementImportBatch;
|
||||
|
||||
/**
|
||||
* 导入结算单批次 Mapper
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Mapper
|
||||
public interface SettlementImportBatchMapper extends BaseMapper<SettlementImportBatch> {
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大批次流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(batch_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_settlement_import_batch WHERE tenant_id = #{tenantId} "
|
||||
+ "AND batch_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
+15
@@ -101,4 +101,19 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
|
||||
/** 关闭总单调度后生成总单客户合同应收,并按所属运单记录的承运商合同生成应付。 */
|
||||
void generateForClosedMasterOrder(MasterOrder masterOrder);
|
||||
|
||||
/**
|
||||
* 结算导入:按导入单据构建/刷新「结算导入」来源的应收应付明细(运输费+费用项金额以导入数据为准)。
|
||||
* <p>
|
||||
* 历史运单若已有系统生成明细,不覆盖原明细,另建结算导入明细;运单已进入结算则拒绝重复导入。
|
||||
* feeRows 每项含 waybillId、transportFee、unitPrice、feeItems(Map)、remark。
|
||||
* </p>
|
||||
*
|
||||
* @param settlementType 结算类型 receivable/payable
|
||||
* @param contractId 结算合同ID
|
||||
* @param feeRows 费用行
|
||||
* @return 明细主键列表(新建或刷新后的结算导入明细)
|
||||
*/
|
||||
List<Long> createDetailsFromSettlementImport(String settlementType, Long contractId,
|
||||
List<Map<String, Object>> feeRows);
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportBatchRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportVoidRequest;
|
||||
import org.springblade.transport.pojo.entity.SettlementImportBatch;
|
||||
import org.springblade.transport.pojo.vo.SettlementImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单批次服务
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface ISettlementImportBatchService extends BaseService<SettlementImportBatch> {
|
||||
|
||||
Map<String, List<Map<String, Object>>> options();
|
||||
|
||||
String nextBatchNo();
|
||||
|
||||
IPage<SettlementImportBatchVO> page(IPage<SettlementImportBatch> page, SettlementImportBatchRequest request);
|
||||
|
||||
SettlementImportBatchVO detail(Long id);
|
||||
|
||||
void exportTemplate(HttpServletResponse response);
|
||||
|
||||
Map<String, Object> parse(MultipartFile file);
|
||||
|
||||
WaybillImportValidateResultVO validate(SettlementImportBatchRequest request);
|
||||
|
||||
void exportFailures(SettlementImportBatchRequest request, MultipartFile file, HttpServletResponse response);
|
||||
|
||||
SettlementImportBatchVO saveDraft(SettlementImportBatchRequest request);
|
||||
|
||||
SettlementImportBatchVO confirm(SettlementImportBatchRequest request);
|
||||
|
||||
void voidBatch(SettlementImportVoidRequest request);
|
||||
}
|
||||
+4
-2
@@ -437,9 +437,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
} else if (Func.isNotEmpty(contractManage.getOrganizationId())) {
|
||||
queryWrapper.eq(ContractManage::getOrganizationId, contractManage.getOrganizationId());
|
||||
}
|
||||
queryWrapper.like(Func.isNotEmpty(contractManage.getContractNo()), ContractManage::getContractNo, contractManage.getContractNo())
|
||||
queryWrapper.eq(Func.isNotEmpty(contractManage.getProjectId()), ContractManage::getProjectId, contractManage.getProjectId())
|
||||
.like(Func.isNotEmpty(contractManage.getContractNo()), ContractManage::getContractNo, contractManage.getContractNo())
|
||||
.like(Func.isNotEmpty(contractManage.getContractName()), ContractManage::getContractName, contractManage.getContractName())
|
||||
.like(Func.isNotEmpty(contractManage.getProjectName()), ContractManage::getProjectName, contractManage.getProjectName())
|
||||
.like(Func.isNotEmpty(contractManage.getProjectName()) && Func.isEmpty(contractManage.getProjectId()),
|
||||
ContractManage::getProjectName, contractManage.getProjectName())
|
||||
.like(Func.isNotEmpty(contractManage.getOrganizationName()), ContractManage::getOrganizationName, contractManage.getOrganizationName())
|
||||
.eq(Func.isNotEmpty(contractManage.getContractCategory()), ContractManage::getContractCategory, contractManage.getContractCategory())
|
||||
.eq(Func.isNotEmpty(contractManage.getSignType()), ContractManage::getSignType, contractManage.getSignType())
|
||||
|
||||
+180
@@ -847,6 +847,186 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
private static final String SOURCE_SETTLEMENT_IMPORT = "结算导入";
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Long> createDetailsFromSettlementImport(String settlementType, Long contractId,
|
||||
List<Map<String, Object>> feeRows) {
|
||||
if (Func.isEmpty(settlementType) || Func.isEmpty(contractId)) {
|
||||
throw new ServiceException("结算类型与合同不能为空");
|
||||
}
|
||||
if (Func.isEmpty(feeRows)) {
|
||||
return List.of();
|
||||
}
|
||||
ContractManage contract = contractManageService.getById(contractId);
|
||||
if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("合同不存在");
|
||||
}
|
||||
String targetSettlementType = settlementType(settlementType);
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
Date generateTime = new Date();
|
||||
List<Long> createdIds = new ArrayList<>();
|
||||
for (Map<String, Object> feeRow : feeRows) {
|
||||
if (feeRow == null) {
|
||||
continue;
|
||||
}
|
||||
Long waybillId = toLong(feeRow.get("waybillId"));
|
||||
if (waybillId == null) {
|
||||
continue;
|
||||
}
|
||||
Waybill waybill = waybillService.getById(waybillId);
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("运单不存在:" + waybillId);
|
||||
}
|
||||
assertWaybillNotSettledForImport(waybill, targetSettlementType);
|
||||
|
||||
BigDecimal transportFee = money(decimal(feeRow.get("transportFee")));
|
||||
BigDecimal unitPrice = money(decimal(feeRow.get("unitPrice")));
|
||||
Map<String, BigDecimal> feeItems = normalizeFeeItems(toFeeItemMap(feeRow.get("feeItems")));
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal afterAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
ReceivablePayableCargoFee cargoFee = buildSettlementImportCargoFee(
|
||||
waybill, transportFee, unitPrice, feeItems, afterAmount, stringValue(feeRow.get("remark")));
|
||||
|
||||
ReceivablePayableDetail importDetail = getOne(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getWaybillId, waybillId)
|
||||
.eq(ReceivablePayableDetail::getSettlementType, targetSettlementType)
|
||||
.eq(ReceivablePayableDetail::getSourceType, SOURCE_SETTLEMENT_IMPORT)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
.orderByDesc(ReceivablePayableDetail::getCreateTime)
|
||||
.last("limit 1"), false);
|
||||
if (importDetail != null) {
|
||||
refreshSettlementImportDetail(importDetail, waybill, contract, targetSettlementType,
|
||||
cargoFee, unitPrice, stringValue(feeRow.get("remark")), currentUserId, generateTime);
|
||||
createdIds.add(importDetail.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
ReceivablePayableDetail detail = buildDetail(waybill, contract, targetSettlementType,
|
||||
List.of(cargoFee), unitPrice);
|
||||
detail.setSourceType(SOURCE_SETTLEMENT_IMPORT);
|
||||
detail.setSettlementStatus("pending");
|
||||
if (Func.isNotEmpty(stringValue(feeRow.get("remark")))) {
|
||||
detail.setRemark(stringValue(feeRow.get("remark")));
|
||||
}
|
||||
saveAutomaticDetail(detail, List.of(cargoFee), false, currentUserId, generateTime);
|
||||
createdIds.add(detail.getId());
|
||||
}
|
||||
return createdIds;
|
||||
}
|
||||
|
||||
/** 运单在该结算类型下已进入预/正式结算时,禁止再导入。 */
|
||||
private void assertWaybillNotSettledForImport(Waybill waybill, String settlementType) {
|
||||
List<ReceivablePayableDetail> details = list(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getWaybillId, waybill.getId())
|
||||
.eq(ReceivablePayableDetail::getSettlementType, settlementType)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0));
|
||||
for (ReceivablePayableDetail detail : details) {
|
||||
String settlementStatus = Func.toStr(detail.getSettlementStatus(), "pending");
|
||||
if (Func.isNotEmpty(detail.getFormalSettlementNo())
|
||||
|| Func.isNotEmpty(detail.getPreSettlementNo())
|
||||
|| List.of("pre_settled", "formal_settled", "closed").contains(settlementStatus)) {
|
||||
throw new ServiceException("运单" + Func.toStr(waybill.getWaybillNo(), String.valueOf(waybill.getId()))
|
||||
+ "已进入结算,无法重复导入");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ReceivablePayableCargoFee buildSettlementImportCargoFee(Waybill waybill, BigDecimal transportFee,
|
||||
BigDecimal unitPrice, Map<String, BigDecimal> feeItems,
|
||||
BigDecimal afterAmount, String remark) {
|
||||
ReceivablePayableCargoFee cargoFee = new ReceivablePayableCargoFee();
|
||||
cargoFee.setWaybillId(waybill.getId());
|
||||
cargoFee.setLineNo("0001");
|
||||
cargoFee.setDataSource(FEE_SOURCE_MANUAL);
|
||||
cargoFee.setCargoName(waybill.getCargoName());
|
||||
cargoFee.setCargoType(waybill.getCargoType());
|
||||
cargoFee.setSpecification(waybill.getSpecification());
|
||||
cargoFee.setModel(waybill.getModel());
|
||||
cargoFee.setBillingFactor("按重量");
|
||||
cargoFee.setBillingType("固定单价");
|
||||
cargoFee.setTransportQuantity(money(waybill.getQuantity()));
|
||||
cargoFee.setQuantityUnit(waybill.getQuantityUnit());
|
||||
cargoFee.setPriceUnit(waybill.getPriceUnit());
|
||||
cargoFee.setUnitPrice(unitPrice);
|
||||
cargoFee.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
|
||||
cargoFee.setFreightAmount(transportFee);
|
||||
cargoFee.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
cargoFee.setOriginalAmount(afterAmount);
|
||||
cargoFee.setAdjustAmount(BigDecimal.ZERO.setScale(2));
|
||||
cargoFee.setAfterAmount(afterAmount);
|
||||
cargoFee.setRemark(remark);
|
||||
return cargoFee;
|
||||
}
|
||||
|
||||
/** 刷新已有结算导入明细:费用项目与金额始终覆盖为本次导入数据。 */
|
||||
private void refreshSettlementImportDetail(ReceivablePayableDetail detail, Waybill waybill, ContractManage contract,
|
||||
String settlementType, ReceivablePayableCargoFee cargoFee,
|
||||
BigDecimal unitPrice, String remark, Long currentUserId, Date generateTime) {
|
||||
ReceivablePayableDetail rebuilt = buildDetail(waybill, contract, settlementType, List.of(cargoFee), unitPrice);
|
||||
detail.setFreightAmount(rebuilt.getFreightAmount());
|
||||
detail.setOtherFeeAmount(rebuilt.getOtherFeeAmount());
|
||||
detail.setTotalAmount(rebuilt.getTotalAmount());
|
||||
detail.setUnitPrice(rebuilt.getUnitPrice());
|
||||
detail.setCurrency(rebuilt.getCurrency());
|
||||
detail.setFeeItemsJson(rebuilt.getFeeItemsJson());
|
||||
detail.setContractId(rebuilt.getContractId());
|
||||
detail.setContractNo(rebuilt.getContractNo());
|
||||
detail.setContractName(rebuilt.getContractName());
|
||||
detail.setCustomerName(rebuilt.getCustomerName());
|
||||
detail.setSourceType(SOURCE_SETTLEMENT_IMPORT);
|
||||
detail.setSettlementStatus("pending");
|
||||
if (Func.isNotEmpty(remark)) {
|
||||
detail.setRemark(remark);
|
||||
}
|
||||
updateById(detail);
|
||||
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
|
||||
ReceivablePayableCargoFee copy = BeanUtil.copyProperties(cargoFee, ReceivablePayableCargoFee.class);
|
||||
copy.setId(null);
|
||||
copy.setDetailId(detail.getId());
|
||||
fillGeneratedAuditFields(copy, currentUserId, generateTime);
|
||||
cargoFeeMapper.insert(copy);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, BigDecimal> toFeeItemMap(Object value) {
|
||||
Map<String, BigDecimal> result = new LinkedHashMap<>();
|
||||
if (!(value instanceof Map<?, ?> raw)) {
|
||||
return result;
|
||||
}
|
||||
raw.forEach((key, amount) -> {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
result.put(String.valueOf(key), decimal(amount));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null || "".equals(value)) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
|
||||
+1144
File diff suppressed because it is too large
Load Diff
+9
-10
@@ -1211,15 +1211,14 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
|
||||
@Override
|
||||
public List<WaybillExcel> exportWaybill(WaybillVO waybill, String ids) {
|
||||
TransportBusinessSupport.validateAllDept(waybill.getAllDept(), "运单管理");
|
||||
List<Long> idList = Func.toLongList(ids);
|
||||
LambdaQueryWrapper<Waybill> queryWrapper;
|
||||
if (Func.isNotEmpty(idList)) {
|
||||
queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
if (!Objects.equals(waybill.getAllDept(), 1)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, TransportBusinessSupport.currentDeptId("运单管理"));
|
||||
} else if (Func.isNotEmpty(waybill.getDeptId())) {
|
||||
queryWrapper.eq(Waybill::getDeptId, waybill.getDeptId());
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(
|
||||
waybill.getAllDept(), waybill.getDeptId(), "运单管理");
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
}
|
||||
queryWrapper.in(Waybill::getId, idList).orderByDesc(Waybill::getCreateTime);
|
||||
} else {
|
||||
@@ -1604,12 +1603,12 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> buildQuery(WaybillVO waybill) {
|
||||
TransportBusinessSupport.validateAllDept(waybill.getAllDept(), "运单管理");
|
||||
LambdaQueryWrapper<Waybill> queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
if (!Objects.equals(waybill.getAllDept(), 1)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, TransportBusinessSupport.currentDeptId("运单管理"));
|
||||
} else if (Func.isNotEmpty(waybill.getDeptId())) {
|
||||
queryWrapper.eq(Waybill::getDeptId, waybill.getDeptId());
|
||||
// 与常用线路等模块一致:超管默认不按组织过滤;普通用户 allDept!=1 时强制当前组织
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(
|
||||
waybill.getAllDept(), waybill.getDeptId(), "运单管理");
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
}
|
||||
if (Func.isNotEmpty(waybill.getWaybillNo())) {
|
||||
queryWrapper.eq(Waybill::getWaybillNo, waybill.getWaybillNo());
|
||||
|
||||
Reference in New Issue
Block a user