This commit is contained in:
2026-08-20 06:30:50 +08:00
parent df6d417e40
commit 8beb542c98
10 changed files with 558 additions and 32 deletions
@@ -55,5 +55,20 @@ public class ReceivablePayableAdjustFeeRequest implements Serializable {
@Schema(description = "动态费用项目") @Schema(description = "动态费用项目")
private Map<String, BigDecimal> feeItems; private Map<String, BigDecimal> feeItems;
@Schema(description = "是否手工费用行")
private Boolean manualFee;
@Schema(description = "手工费用项目名称")
private String feeItemName;
@Schema(description = "手工费用类型:charge/deduct")
private String feeType;
@Schema(description = "手工费用金额")
private BigDecimal amount;
@Schema(description = "备注")
private String remark;
} }
} }
@@ -24,12 +24,12 @@ package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import cn.idev.excel.FastExcel;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query; import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.secure.annotation.PreAuth;
@@ -43,6 +43,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO;
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.system.cache.DictCache;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -51,7 +52,13 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/** /**
* 应收应付明细控制器 * 应收应付明细控制器
@@ -160,7 +167,56 @@ public class ReceivablePayableDetailController extends BladeController {
@ApiOperationSupport(order = 10) @ApiOperationSupport(order = 10)
@Operation(summary = "导出应收应付明细") @Operation(summary = "导出应收应付明细")
public void exportReceivablePayableDetail(ReceivablePayableDetailVO query, HttpServletResponse response) { public void exportReceivablePayableDetail(ReceivablePayableDetailVO query, HttpServletResponse response) {
IPage<ReceivablePayableDetailVO> page = detailService.selectPage(Condition.getPage(new Query()), query); List<ReceivablePayableDetailVO> records = detailService.selectList(query);
ExcelUtil.export(response, "应收应付明细" + DateUtil.time(), "应收应付明细", page.getRecords(), ReceivablePayableDetailVO.class); Set<String> feeItemNames = new LinkedHashSet<>();
records.forEach(row -> {
if (row.getFeeItems() != null) feeItemNames.addAll(row.getFeeItems().keySet());
});
List<List<String>> head = new ArrayList<>();
String[] baseHeaders = {"单据号", "项目名称", "所属组织", "费用日期", "客商名称", "合同编号", "合同名称", "来源",
"预结算单号", "正式结算单号", "运单号", "车号", "运输类型", "货物名称", "货物类型", "运输总量", "里程(KM",
"批次号", "运输单价"};
for (String header : baseHeaders) head.add(List.of(header));
feeItemNames.forEach(name -> head.add(List.of(name)));
for (String header : new String[] {"费用合计", "状态", "创建人", "创建时间"}) head.add(List.of(header));
List<List<Object>> rows = records.stream().map(row -> {
List<Object> values = new ArrayList<>();
values.add(row.getDocumentNo()); values.add(row.getProjectName()); values.add(row.getDeptName()); values.add(row.getFeeDate());
values.add(row.getCustomerName()); values.add(row.getContractNo()); values.add(row.getContractName()); values.add(row.getSourceType());
values.add(row.getPreSettlementNo()); values.add(row.getFormalSettlementNo()); values.add(row.getWaybillNo()); values.add(row.getVehicleNo());
values.add(transportTypeName(row.getTransportType())); values.add(row.getCargoName()); values.add(row.getCargoType());
values.add(row.getTransportQuantity()); values.add(row.getMileage() != null && row.getMileage().compareTo(BigDecimal.valueOf(-1)) == 0 ? null : row.getMileage());
values.add(row.getBatchNo()); values.add(money(row.getUnitPrice(), row.getCurrency()));
feeItemNames.forEach(name -> values.add(money(decimal(row.getFeeItems() == null ? null : row.getFeeItems().get(name)), row.getCurrency())));
values.add(money(row.getTotalAmount(), row.getCurrency())); values.add(row.getSettlementStatusName()); values.add(row.getCreateUserName()); values.add(row.getCreateTime());
return values;
}).toList();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode("应收应付明细" + DateUtil.time(), StandardCharsets.UTF_8) + ".xlsx");
try {
FastExcel.write(response.getOutputStream()).head(head).sheet("应收应付明细").doWrite(rows);
} catch (Exception exception) {
throw new IllegalStateException("导出应收应付明细失败", exception);
}
}
private String transportTypeName(String value) {
if (value == null || value.isBlank()) return value;
String name = DictCache.getValue("transport_type", value);
return name == null || name.isBlank() ? value : name;
}
private String money(BigDecimal value, String currency) {
if (value == null) return "-";
return value.setScale(2, java.math.RoundingMode.HALF_UP).toPlainString() + " " + (currency == null || currency.isBlank() ? "RMB" : currency);
}
private BigDecimal decimal(Object value) {
if (value == null || String.valueOf(value).isBlank()) return null;
try { return new BigDecimal(String.valueOf(value)); } catch (NumberFormatException ignored) { return null; }
} }
} }
@@ -42,6 +42,9 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
boolean complete(Long id); boolean complete(Long id);
/** 运单完成后检查关联运单状态,全部完成时自动完成配载单。 */
boolean completeIfAllWaybillsCompleted(String loadingNo);
BusinessRemoveResultVO batchComplete(String ids); BusinessRemoveResultVO batchComplete(String ids);
} }
@@ -28,6 +28,7 @@ import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO;
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
@@ -45,6 +46,8 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
IPage<ReceivablePayableDetailVO> selectPage(IPage<ReceivablePayableDetail> page, ReceivablePayableDetailVO query); IPage<ReceivablePayableDetailVO> selectPage(IPage<ReceivablePayableDetail> page, ReceivablePayableDetailVO query);
List<ReceivablePayableDetailVO> selectList(ReceivablePayableDetailVO query);
ReceivablePayableFeeDetailVO feeDetail(Long id); ReceivablePayableFeeDetailVO feeDetail(Long id);
IPage<ReceivablePayableChangeRecordVO> changeRecords(IPage<?> page, Long detailId); IPage<ReceivablePayableChangeRecordVO> changeRecords(IPage<?> page, Long detailId);
@@ -69,4 +72,7 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */ /** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
void generateForCompletedWaybills(List<Long> waybillIds); void generateForCompletedWaybills(List<Long> waybillIds);
/** 关闭总单调度后按合同系统计费模式自动生成总单应收、应付明细。 */
void generateForClosedMasterOrder(MasterOrder masterOrder);
} }
@@ -263,8 +263,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
LoadingManage loadingManage = loadEditable(id, true); LoadingManage loadingManage = loadEditable(id, true);
if (!Objects.equals(loadingManage.getBusinessStatus(), STATUS_RUNNING) if (!Objects.equals(loadingManage.getBusinessStatus(), STATUS_RUNNING)
&& !(Objects.equals(loadingManage.getBusinessStatus(), STATUS_PENDING) && !(Objects.equals(loadingManage.getBusinessStatus(), STATUS_PENDING)
&& allAssociatedWaybillsRunning(loadingManage))) { && allAssociatedWaybillsCompletable(loadingManage))) {
throw new ServiceException("仅进行中状态允许完成"); throw new ServiceException("仅进行中,或关联运单全部进行中/已完成的待执行配载单允许完成");
} }
loadingManage.setBusinessStatus(STATUS_COMPLETED); loadingManage.setBusinessStatus(STATUS_COMPLETED);
boolean result = updateById(loadingManage); boolean result = updateById(loadingManage);
@@ -272,6 +272,38 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return result; return result;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public boolean completeIfAllWaybillsCompleted(String loadingNo) {
if (Func.isEmpty(loadingNo)) {
return false;
}
LoadingManage loadingManage = getOne(Wrappers.<LoadingManage>lambdaQuery()
.eq(LoadingManage::getLoadingNo, loadingNo)
.eq(LoadingManage::getIsDeleted, 0)
.last("FOR UPDATE"), false);
if (loadingManage == null
|| Objects.equals(loadingManage.getBusinessStatus(), STATUS_COMPLETED)
|| Objects.equals(loadingManage.getBusinessStatus(), STATUS_CANCELLED)
|| Objects.equals(loadingManage.getBusinessStatus(), STATUS_DRAFT)) {
return false;
}
List<Long> waybillIdList = waybillIds(loadingManage.getWaybillIdsJson());
if (Func.isEmpty(waybillIdList)) {
return false;
}
List<Waybill> waybillList = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIdList));
boolean allCompleted = waybillList.size() == waybillIdList.size()
&& waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_COMPLETED));
if (!allCompleted) {
return false;
}
loadingManage.setBusinessStatus(STATUS_COMPLETED);
return updateById(loadingManage);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public BusinessRemoveResultVO batchComplete(String ids) { public BusinessRemoveResultVO batchComplete(String ids) {
@@ -292,7 +324,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return result; return result;
} }
private boolean allAssociatedWaybillsRunning(LoadingManage loadingManage) { private boolean allAssociatedWaybillsCompletable(LoadingManage loadingManage) {
List<Long> waybillIdList = waybillIds(loadingManage.getWaybillIdsJson()); List<Long> waybillIdList = waybillIds(loadingManage.getWaybillIdsJson());
if (Func.isEmpty(waybillIdList)) { if (Func.isEmpty(waybillIdList)) {
return false; return false;
@@ -300,8 +332,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
List<Waybill> waybillList = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery() List<Waybill> waybillList = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0) .eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIdList)); .in(Waybill::getId, waybillIdList));
return waybillList.size() == waybillIdList.size() if (waybillList.size() != waybillIdList.size()) {
&& waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_RUNNING)); return false;
}
return waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_RUNNING))
|| waybillList.stream().allMatch(waybill -> Objects.equals(waybill.getBusinessStatus(), STATUS_COMPLETED));
} }
private LambdaQueryWrapper<LoadingManage> buildQuery(LoadingManageVO loadingManage) { private LambdaQueryWrapper<LoadingManage> buildQuery(LoadingManageVO loadingManage) {
@@ -24,6 +24,7 @@ import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IMasterOrderService; import org.springblade.transport.service.IMasterOrderService;
import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.TransportBusinessSupport;
@@ -53,13 +54,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
private final ITransportPlanService transportPlanService; private final ITransportPlanService transportPlanService;
private final IProjectApplyService projectApplyService; private final IProjectApplyService projectApplyService;
private final IContractManageService contractManageService; private final IContractManageService contractManageService;
private final IReceivablePayableDetailService receivablePayableDetailService;
public MasterOrderServiceImpl(IWaybillService waybillService, ITransportPlanService transportPlanService, IProjectApplyService projectApplyService, public MasterOrderServiceImpl(IWaybillService waybillService, ITransportPlanService transportPlanService, IProjectApplyService projectApplyService,
IContractManageService contractManageService) { IContractManageService contractManageService, IReceivablePayableDetailService receivablePayableDetailService) {
this.waybillService = waybillService; this.waybillService = waybillService;
this.transportPlanService = transportPlanService; this.transportPlanService = transportPlanService;
this.projectApplyService = projectApplyService; this.projectApplyService = projectApplyService;
this.contractManageService = contractManageService; this.contractManageService = contractManageService;
this.receivablePayableDetailService = receivablePayableDetailService;
} }
@Override @Override
@@ -124,7 +127,11 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
throw new ServiceException("当前状态不允许关闭调度"); throw new ServiceException("当前状态不允许关闭调度");
} }
masterOrder.setBusinessStatus("closed"); masterOrder.setBusinessStatus("closed");
return updateById(masterOrder); boolean updated = updateById(masterOrder);
if (updated) {
receivablePayableDetailService.generateForClosedMasterOrder(masterOrder);
}
return updated;
} }
@Override @Override
@@ -653,6 +653,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
Set<Long> existingSourceIds = existingDetails.stream().map(PreSettlementDetail::getSourceDetailId) Set<Long> existingSourceIds = existingDetails.stream().map(PreSettlementDetail::getSourceDetailId)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
List<Long> addedIds = distinctIds.stream().filter(id -> !existingSourceIds.contains(id)).toList(); List<Long> addedIds = distinctIds.stream().filter(id -> !existingSourceIds.contains(id)).toList();
List<ReceivablePayableDetail> addedSources = new ArrayList<>();
if (!addedIds.isEmpty()) { if (!addedIds.isEmpty()) {
List<ReceivablePayableDetail> sources = sourceDetailMapper.selectBatchIds(addedIds); List<ReceivablePayableDetail> sources = sourceDetailMapper.selectBatchIds(addedIds);
if (sources.size() != addedIds.size()) { if (sources.size() != addedIds.size()) {
@@ -683,12 +684,20 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
if (affected != 1) { if (affected != 1) {
throw new ServiceException("单据" + source.getDocumentNo() + "已被其他预结算单选择"); throw new ServiceException("单据" + source.getDocumentNo() + "已被其他预结算单选择");
} }
saveChange(settlement.getId(), "结算明细项", null, "新增", addedSources.add(source);
"新增单据号" + source.getDocumentNo(), "");
} }
updateById(settlement); updateById(settlement);
} }
renumberDetails(settlement.getId()); renumberDetails(settlement.getId());
if (!addedSources.isEmpty()) {
Map<Long, Integer> detailLineMap = listDetails(settlement.getId()).stream()
.collect(Collectors.toMap(PreSettlementDetail::getSourceDetailId, PreSettlementDetail::getLineNo,
(left, right) -> left));
for (ReceivablePayableDetail source : addedSources) {
saveChange(settlement.getId(), "结算明细项", detailLineMap.get(source.getId()), "新增",
"新增单据号" + source.getDocumentNo(), "");
}
}
} }
private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source, private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source,
@@ -883,7 +892,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
row.setManualFlag(1); row.setManualFlag(1);
if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row); if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row);
retainedManualIds.add(row.getId()); retainedManualIds.add(row.getId());
saveChange(settlementId, "合计费用项", row.getLineNo(), requestRow.getId() == null ? "新增" : "调整", saveChange(settlementId, "合计费用项", null, requestRow.getId() == null ? "新增" : "调整",
row.getFeeItem() + "金额" + row.getSettlementAmount(), ""); row.getFeeItem() + "金额" + row.getSettlementAmount(), "");
continue; continue;
} }
@@ -907,14 +916,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
row.setRemark(limitRemark(requestRow.getRemark(), 50)); row.setRemark(limitRemark(requestRow.getRemark(), 50));
summaryFeeMapper.updateById(row); summaryFeeMapper.updateById(row);
if (before.compareTo(row.getAdjustAmount()) != 0) { if (before.compareTo(row.getAdjustAmount()) != 0) {
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", saveChange(settlementId, "合计费用项", null, "调整",
"【调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "", ""); "【调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "", "");
} }
} }
for (PreSettlementSummaryFee manualRow : manualRows) { for (PreSettlementSummaryFee manualRow : manualRows) {
if (!retainedManualIds.contains(manualRow.getId())) { if (!retainedManualIds.contains(manualRow.getId())) {
summaryFeeMapper.deleteById(manualRow.getId()); summaryFeeMapper.deleteById(manualRow.getId());
saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除", saveChange(settlementId, "合计费用项", null, "删除",
"删除" + manualRow.getFeeItem() + "费用" + manualRow.getSettlementAmount(), ""); "删除" + manualRow.getFeeItem() + "费用" + manualRow.getSettlementAmount(), "");
} }
} }
@@ -36,6 +36,7 @@ import org.springblade.system.cache.UserCache;
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper; import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper;
import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
import org.springblade.transport.mapper.MasterOrderMapper;
import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest;
import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest;
@@ -44,6 +45,7 @@ import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.CommonAddress; import org.springblade.transport.pojo.entity.CommonAddress;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
import org.springblade.transport.pojo.entity.ReceivablePayableChangeRecord; import org.springblade.transport.pojo.entity.ReceivablePayableChangeRecord;
import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
@@ -91,8 +93,11 @@ public class ReceivablePayableDetailServiceImpl
extends BaseServiceImpl<ReceivablePayableDetailMapper, ReceivablePayableDetail> extends BaseServiceImpl<ReceivablePayableDetailMapper, ReceivablePayableDetail>
implements IReceivablePayableDetailService { implements IReceivablePayableDetailService {
private static final String SOURCE_MASTER_ORDER = "总单系统生成";
private final ReceivablePayableCargoFeeMapper cargoFeeMapper; private final ReceivablePayableCargoFeeMapper cargoFeeMapper;
private final ReceivablePayableChangeRecordMapper changeRecordMapper; private final ReceivablePayableChangeRecordMapper changeRecordMapper;
private final MasterOrderMapper masterOrderMapper;
private final IWaybillService waybillService; private final IWaybillService waybillService;
private final IContractManageService contractManageService; private final IContractManageService contractManageService;
private final ICommonAddressService commonAddressService; private final ICommonAddressService commonAddressService;
@@ -101,6 +106,7 @@ public class ReceivablePayableDetailServiceImpl
public ReceivablePayableDetailServiceImpl(ReceivablePayableCargoFeeMapper cargoFeeMapper, public ReceivablePayableDetailServiceImpl(ReceivablePayableCargoFeeMapper cargoFeeMapper,
ReceivablePayableChangeRecordMapper changeRecordMapper, ReceivablePayableChangeRecordMapper changeRecordMapper,
MasterOrderMapper masterOrderMapper,
IWaybillService waybillService, IWaybillService waybillService,
IContractManageService contractManageService, IContractManageService contractManageService,
ICommonAddressService commonAddressService, ICommonAddressService commonAddressService,
@@ -108,6 +114,7 @@ public class ReceivablePayableDetailServiceImpl
@Lazy IFormalSettlementService formalSettlementService) { @Lazy IFormalSettlementService formalSettlementService) {
this.cargoFeeMapper = cargoFeeMapper; this.cargoFeeMapper = cargoFeeMapper;
this.changeRecordMapper = changeRecordMapper; this.changeRecordMapper = changeRecordMapper;
this.masterOrderMapper = masterOrderMapper;
this.waybillService = waybillService; this.waybillService = waybillService;
this.contractManageService = contractManageService; this.contractManageService = contractManageService;
this.commonAddressService = commonAddressService; this.commonAddressService = commonAddressService;
@@ -120,6 +127,11 @@ public class ReceivablePayableDetailServiceImpl
return ReceivablePayableDetailWrapper.build().pageVO(page(page, buildQuery(query))); return ReceivablePayableDetailWrapper.build().pageVO(page(page, buildQuery(query)));
} }
@Override
public List<ReceivablePayableDetailVO> selectList(ReceivablePayableDetailVO query) {
return ReceivablePayableDetailWrapper.build().listVO(list(buildQuery(query)));
}
@Override @Override
public ReceivablePayableFeeDetailVO feeDetail(Long id) { public ReceivablePayableFeeDetailVO feeDetail(Long id) {
ReceivablePayableDetail detail = getExisting(id); ReceivablePayableDetail detail = getExisting(id);
@@ -127,6 +139,10 @@ public class ReceivablePayableDetailServiceImpl
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) .eq(ReceivablePayableCargoFee::getDetailId, detail.getId())
.eq(ReceivablePayableCargoFee::getIsDeleted, 0) .eq(ReceivablePayableCargoFee::getIsDeleted, 0)
.orderByAsc(ReceivablePayableCargoFee::getCreateTime)); .orderByAsc(ReceivablePayableCargoFee::getCreateTime));
// 运输量的单位取明细对应运单单位兼容历史费用行误存计费单位的情况
if (Func.isNotEmpty(detail.getQuantityUnit())) {
rows.forEach(row -> row.setQuantityUnit(detail.getQuantityUnit()));
}
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
feeItemNames.addAll(result.getFeeItemNames()); feeItemNames.addAll(result.getFeeItemNames());
@@ -157,6 +173,9 @@ public class ReceivablePayableDetailServiceImpl
if (Func.isEmpty(request.getContractId())) { if (Func.isEmpty(request.getContractId())) {
throw new ServiceException("请选择需要更新费用的合同"); throw new ServiceException("请选择需要更新费用的合同");
} }
if (Func.isEmpty(request.getBillingPlanId())) {
throw new ServiceException("请选择需要更新的合同计费方案");
}
List<ReceivablePayableDetail> details = list(buildUpdateQuery(request)); List<ReceivablePayableDetail> details = list(buildUpdateQuery(request));
if (Func.isEmpty(details)) { if (Func.isEmpty(details)) {
throw new ServiceException("没有可更新的待结算明细"); throw new ServiceException("没有可更新的待结算明细");
@@ -217,14 +236,65 @@ public class ReceivablePayableDetailServiceImpl
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)); .eq(ReceivablePayableCargoFee::getIsDeleted, 0));
Map<Long, ReceivablePayableCargoFee> existingMap = existingRows.stream() Map<Long, ReceivablePayableCargoFee> existingMap = existingRows.stream()
.collect(java.util.stream.Collectors.toMap(ReceivablePayableCargoFee::getId, row -> row)); .collect(java.util.stream.Collectors.toMap(ReceivablePayableCargoFee::getId, row -> row));
if (request.getRows().size() != existingRows.size()) { Set<Long> submittedExistingIds = request.getRows().stream()
.map(ReceivablePayableAdjustFeeRequest.AdjustRow::getId)
.filter(Objects::nonNull)
.collect(java.util.stream.Collectors.toSet());
if (!submittedExistingIds.equals(existingMap.keySet())) {
throw new ServiceException("费用调整行数据不完整"); throw new ServiceException("费用调整行数据不完整");
} }
Set<String> allowedFeeItems = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); Set<String> allowedFeeItems = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
existingRows.forEach(row -> allowedFeeItems.addAll(parseMap(row.getFeeItemsJson()).keySet())); existingRows.forEach(row -> allowedFeeItems.addAll(parseMap(row.getFeeItemsJson()).keySet()));
List<String> changes = new ArrayList<>(); List<String> changes = new ArrayList<>();
List<ReceivablePayableCargoFee> allRows = new ArrayList<>(existingRows);
for (ReceivablePayableAdjustFeeRequest.AdjustRow adjusted : request.getRows()) { for (ReceivablePayableAdjustFeeRequest.AdjustRow adjusted : request.getRows()) {
ReceivablePayableCargoFee existing = existingMap.get(adjusted.getId()); ReceivablePayableCargoFee existing = existingMap.get(adjusted.getId());
if (adjusted.getRemark() != null && adjusted.getRemark().length() > 200) {
throw new ServiceException("备注不能超过200个字");
}
if (Boolean.TRUE.equals(adjusted.getManualFee())) {
if (Func.isEmpty(adjusted.getFeeItemName())) {
throw new ServiceException("请填写手工费用项目");
}
if (!List.of("charge", "deduct").contains(adjusted.getFeeType())) {
throw new ServiceException("手工费用类型不正确");
}
validateNonNegative(adjusted.getAmount(), "手工费用金额");
BigDecimal signedAmount = money(adjusted.getAmount())
.multiply("deduct".equals(adjusted.getFeeType()) ? BigDecimal.valueOf(-1) : BigDecimal.ONE);
Map<String, BigDecimal> manualItems = new LinkedHashMap<>();
manualItems.put(adjusted.getFeeItemName().trim(), signedAmount);
boolean newManualRow = existing == null;
if (newManualRow) {
existing = new ReceivablePayableCargoFee();
existing.setDetailId(detail.getId());
existing.setLineNo("ADJ-" + System.currentTimeMillis());
existing.setOriginalAmount(BigDecimal.ZERO);
}
String oldName = existing.getCargoName();
BigDecimal oldAmount = money(existing.getAfterAmount());
// cargoName 表示运单货物名称手工收费项名称仅保存到费用项目 JSON
existing.setCargoName(detail.getCargoName());
existing.setBillingFactor("手工调整");
existing.setBillingType("deduct".equals(adjusted.getFeeType()) ? "手工扣费" : "手工收费");
existing.setTransportQuantity(detail.getTransportQuantity());
existing.setQuantityUnit(detail.getQuantityUnit());
existing.setMileage(detail.getMileage());
existing.setFreightAmount(BigDecimal.ZERO);
existing.setFeeItemsJson(JsonUtil.toJson(manualItems));
existing.setAdjustAmount(signedAmount.subtract(money(existing.getOriginalAmount())));
existing.setAfterAmount(signedAmount);
existing.setRemark(adjusted.getRemark());
if (newManualRow) {
cargoFeeMapper.insert(existing);
allRows.add(existing);
} else {
cargoFeeMapper.updateById(existing);
}
changes.add("【手工费用】从[" + oldName + " " + formatValue(oldAmount) + "]调整为["
+ existing.getBillingType() + " " + existing.getCargoName() + " " + formatValue(signedAmount) + "]");
continue;
}
if (existing == null) { if (existing == null) {
throw new ServiceException("存在无效的费用调整行"); throw new ServiceException("存在无效的费用调整行");
} }
@@ -244,6 +314,10 @@ public class ReceivablePayableDetailServiceImpl
appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity()); appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity());
appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage()); appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage());
appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount()); appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount());
if (!Objects.equals(existing.getRemark(), adjusted.getRemark())) {
changes.add("【备注】从[" + Objects.toString(existing.getRemark(), "") + "]调整为["
+ Objects.toString(adjusted.getRemark(), "") + "]");
}
Map<String, Object> oldFeeItems = parseMap(existing.getFeeItemsJson()); Map<String, Object> oldFeeItems = parseMap(existing.getFeeItemsJson());
for (String name : allowedFeeItems) { for (String name : allowedFeeItems) {
appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name))); appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name)));
@@ -254,6 +328,7 @@ public class ReceivablePayableDetailServiceImpl
existing.setMileage(money(adjusted.getMileage())); existing.setMileage(money(adjusted.getMileage()));
existing.setFreightAmount(freightAmount); existing.setFreightAmount(freightAmount);
existing.setFeeItemsJson(JsonUtil.toJson(feeItems)); existing.setFeeItemsJson(JsonUtil.toJson(feeItems));
existing.setRemark(adjusted.getRemark());
existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount()))); existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount())));
existing.setAfterAmount(afterAmount); existing.setAfterAmount(afterAmount);
cargoFeeMapper.updateById(existing); cargoFeeMapper.updateById(existing);
@@ -264,7 +339,7 @@ public class ReceivablePayableDetailServiceImpl
for (int i = 0; i < changes.size(); i++) { for (int i = 0; i < changes.size(); i++) {
saveChangeRecord(detail, changes.get(i), request.getAdjustReason(), String.format("%04d", i + 1)); saveChangeRecord(detail, changes.get(i), request.getAdjustReason(), String.format("%04d", i + 1));
} }
refreshAdjustedDetail(detail, existingRows); refreshAdjustedDetail(detail, allRows);
} }
@Override @Override
@@ -420,6 +495,50 @@ public class ReceivablePayableDetailServiceImpl
} }
} }
@Override
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
public void generateForClosedMasterOrder(MasterOrder masterOrder) {
if (masterOrder == null || Func.isEmpty(masterOrder.getId()) || Func.isEmpty(masterOrder.getContractId())) {
return;
}
ContractManage contract = contractManageService.getById(masterOrder.getContractId());
if (contract == null || !isSystemGeneration(contract)) return;
try {
List<Waybill> masterGoods = masterOrderGoods(masterOrder);
if (masterGoods.isEmpty()) return;
List<ReceivablePayableCargoFee> matchedFees = new ArrayList<>();
for (Waybill masterGoodsItem : masterGoods) {
String planId = matchedPlanId(masterGoodsItem, contract);
if (Func.isEmpty(planId)) continue;
matchedFees.addAll(calculatedFees(masterGoodsItem, contract, planId, true));
}
if (matchedFees.isEmpty()) return;
normalizeMasterFeeLines(matchedFees);
BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees);
for (String settlementType : List.of("payable", "receivable")) {
if (existsByMasterOrder(masterOrder.getMasterNo(), settlementType)) continue;
ReceivablePayableDetail detail = buildMasterOrderDetail(masterOrder, contract, masterGoods,
settlementType, matchedFees, contractUnitPrice);
save(detail);
for (ReceivablePayableCargoFee fee : matchedFees) {
ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class);
copy.setId(null);
copy.setDetailId(detail.getId());
copy.setWaybillId(null);
cargoFeeMapper.insert(copy);
}
}
} catch (Exception exception) {
log.error("自动生成总单费用明细失败,masterOrderId:{}, masterNo:{}, contractId:{}, failureReason:{}",
masterOrder.getId(), masterOrder.getMasterNo(), masterOrder.getContractId(), exception.getMessage(), exception);
if (exception instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(exception);
}
}
private boolean isSystemGeneration(ContractManage contract) { private boolean isSystemGeneration(ContractManage contract) {
if (Func.isNotEmpty(contract.getFeeGenerationMode())) { if (Func.isNotEmpty(contract.getFeeGenerationMode())) {
return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode()); return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode());
@@ -525,9 +644,12 @@ public class ReceivablePayableDetailServiceImpl
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery() LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0) .eq(Waybill::getIsDeleted, 0)
.eq(Waybill::getContractId, request.getContractId()) .eq(Waybill::getContractId, request.getContractId())
.eq(Waybill::getBusinessStatus, "completed") .eq(Waybill::getBusinessStatus, "completed");
.notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail where is_deleted = 0" String targetSettlementType = settlementType(request.getSettlementType());
+ (Func.isNotEmpty(request.getSettlementType()) ? " and settlement_type = '" + settlementType(request.getSettlementType()) + "'" : "")); wrapper.notInSql(Waybill::getId,
"select waybill_id from blade_receivable_payable_detail"
+ " where is_deleted = 0 and waybill_id is not null and settlement_type = '"
+ targetSettlementType + "'");
if (Func.isNotEmpty(request.getBatchNo())) { if (Func.isNotEmpty(request.getBatchNo())) {
wrapper.like(Waybill::getBatchNo, request.getBatchNo()); wrapper.like(Waybill::getBatchNo, request.getBatchNo());
} }
@@ -580,7 +702,7 @@ public class ReceivablePayableDetailServiceImpl
detail.setCargoType(waybill.getCargoType()); detail.setCargoType(waybill.getCargoType());
detail.setTransportQuantity(waybill.getQuantity()); detail.setTransportQuantity(waybill.getQuantity());
detail.setQuantityUnit(waybill.getQuantityUnit()); detail.setQuantityUnit(waybill.getQuantityUnit());
detail.setMileage(waybill.getMileage()); detail.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
detail.setBatchNo(waybill.getBatchNo()); detail.setBatchNo(waybill.getBatchNo());
detail.setUnitPrice(unitPrice); detail.setUnitPrice(unitPrice);
detail.setCurrency("RMB"); detail.setCurrency("RMB");
@@ -593,6 +715,80 @@ public class ReceivablePayableDetailServiceImpl
return detail; return detail;
} }
private ReceivablePayableDetail buildMasterOrderDetail(MasterOrder masterOrder, ContractManage contract,
List<Waybill> masterGoods, String settlementType,
List<ReceivablePayableCargoFee> fees, BigDecimal unitPrice) {
Waybill masterWaybill = masterGoods.get(0);
ReceivablePayableDetail detail = buildDetail(masterWaybill, contract, settlementType, fees, unitPrice);
detail.setSourceType(SOURCE_MASTER_ORDER);
detail.setWaybillId(null);
detail.setWaybillNo(masterOrder.getMasterNo());
detail.setVehicleNo(null);
detail.setTransportType(masterOrder.getTransportOrganizationType());
detail.setCargoName(joinMasterGoodsField(masterGoods, Waybill::getCargoName));
detail.setCargoType(joinMasterGoodsField(masterGoods, Waybill::getCargoType));
detail.setTransportQuantity(masterGoods.stream().map(Waybill::getQuantity)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
detail.setQuantityUnit(commonMasterGoodsValue(masterGoods, Waybill::getQuantityUnit));
detail.setMileage(null);
detail.setBatchNo(null);
detail.setRemark(masterOrder.getRemark());
return detail;
}
private List<Waybill> masterOrderGoods(MasterOrder masterOrder) {
List<Waybill> result = new ArrayList<>();
for (Map<String, Object> goods : parseList(masterOrder.getGoodsJson())) {
BigDecimal quantity = decimal(goods.get("quantity"));
if (quantity.compareTo(BigDecimal.ZERO) <= 0) continue;
Waybill masterGoodsItem = new Waybill();
masterGoodsItem.setProjectId(masterOrder.getProjectId());
masterGoodsItem.setProjectName(masterOrder.getProjectName());
masterGoodsItem.setContractId(masterOrder.getContractId());
masterGoodsItem.setContractName(masterOrder.getContractName());
masterGoodsItem.setCustomerName(masterOrder.getCustomerName());
masterGoodsItem.setTransportType(masterOrder.getTransportOrganizationType());
masterGoodsItem.setCargoName(stringValue(goods, "cargoName"));
masterGoodsItem.setCargoType(stringValue(goods, "cargoType"));
masterGoodsItem.setSpecification(stringValue(goods, "specification"));
masterGoodsItem.setModel(stringValue(goods, "model"));
masterGoodsItem.setQuantity(quantity);
masterGoodsItem.setQuantityUnit(stringValue(goods, "quantityUnit"));
masterGoodsItem.setDepartureName(masterOrder.getDepartureName());
masterGoodsItem.setDepartureAddress(masterOrder.getDepartureAddress());
masterGoodsItem.setArrivalName(masterOrder.getArrivalName());
masterGoodsItem.setArrivalAddress(masterOrder.getArrivalAddress());
masterGoodsItem.setEndDate(masterOrder.getPlanEndTime() == null ? LocalDate.now()
: masterOrder.getPlanEndTime().toLocalDate());
masterGoodsItem.setMasterNo(masterOrder.getMasterNo());
masterGoodsItem.setWaybillNo(masterOrder.getMasterNo());
masterGoodsItem.setGoodsJson(JsonUtil.toJson(goods));
masterGoodsItem.setRemark(masterOrder.getRemark());
result.add(masterGoodsItem);
}
return result;
}
private void normalizeMasterFeeLines(List<ReceivablePayableCargoFee> fees) {
for (int index = 0; index < fees.size(); index++) {
ReceivablePayableCargoFee fee = fees.get(index);
fee.setWaybillId(null);
fee.setLineNo(String.format("%04d", index + 1));
}
}
private String joinMasterGoodsField(List<Waybill> masterGoods,
java.util.function.Function<Waybill, String> getter) {
return masterGoods.stream().map(getter).filter(Func::isNotEmpty).distinct()
.collect(java.util.stream.Collectors.joining(","));
}
private String commonMasterGoodsValue(List<Waybill> masterGoods,
java.util.function.Function<Waybill, String> getter) {
List<String> values = masterGoods.stream().map(getter).filter(Func::isNotEmpty).distinct().toList();
return values.size() == 1 ? values.get(0) : "";
}
private BigDecimal resolveContractUnitPrice(List<ReceivablePayableCargoFee> fees) { private BigDecimal resolveContractUnitPrice(List<ReceivablePayableCargoFee> fees) {
return fees.stream() return fees.stream()
.filter(this::isFreight) .filter(this::isFreight)
@@ -631,7 +827,7 @@ public class ReceivablePayableDetailServiceImpl
cargoFee.setQuantityUnit(waybill.getQuantityUnit()); cargoFee.setQuantityUnit(waybill.getQuantityUnit());
cargoFee.setPriceUnit(waybill.getPriceUnit()); cargoFee.setPriceUnit(waybill.getPriceUnit());
cargoFee.setUnitPrice(unitPrice); cargoFee.setUnitPrice(unitPrice);
cargoFee.setMileage(waybill.getMileage()); cargoFee.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
cargoFee.setFreightAmount(freightAmount); cargoFee.setFreightAmount(freightAmount);
cargoFee.setFeeItemsJson(JsonUtil.toJson(feeItems)); cargoFee.setFeeItemsJson(JsonUtil.toJson(feeItems));
cargoFee.setOriginalAmount(total); cargoFee.setOriginalAmount(total);
@@ -647,9 +843,7 @@ public class ReceivablePayableDetailServiceImpl
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) { private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) {
List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson());
Map<String, Object> plan = "__matched__".equals(planId) ? plans.stream().filter(this::isDefaultPlan).findFirst().orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1)) : plans.stream().filter(item -> Objects.equals(stringValue(item, "id"), planId) Map<String, Object> plan = resolveBillingPlan(plans, planId);
|| Objects.equals(stringValue(item, "planId"), planId)).findFirst()
.orElseGet(() -> plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null));
if (plan == null || !(plan.get("rules") instanceof List<?>)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill)); if (plan == null || !(plan.get("rules") instanceof List<?>)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill));
List<ReceivablePayableCargoFee> result = new ArrayList<>(); List<ReceivablePayableCargoFee> result = new ArrayList<>();
int line = 1; int line = 1;
@@ -664,9 +858,9 @@ public class ReceivablePayableDetailServiceImpl
fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++));
fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType()); fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType());
fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", ""));
fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(stringValue(rule, "billingUnit", waybill.getQuantityUnit())); fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(waybill.getQuantityUnit());
fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice")));
fee.setMileage(waybill.getMileage()); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO);
fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount))); fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount)));
fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark()));
result.add(fee); result.add(fee);
@@ -674,6 +868,24 @@ public class ReceivablePayableDetailServiceImpl
return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result; return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result;
} }
private Map<String, Object> resolveBillingPlan(List<Map<String, Object>> plans, String planId) {
if ("__matched__".equals(planId)) {
return plans.stream().filter(this::isDefaultPlan).findFirst()
.orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1));
}
if (Func.isEmpty(planId)) {
return plans.stream().filter(this::isDefaultPlan).findFirst()
.orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1));
}
return plans.stream().filter(plan -> Objects.equals(stringValue(plan, "id"), planId)
|| Objects.equals(stringValue(plan, "planId"), planId)
|| Objects.equals(stringValue(plan, "name"), planId)
|| Objects.equals(stringValue(plan, "planName"), planId)
|| Objects.equals(stringValue(plan, "billingPlanName"), planId))
.findFirst()
.orElseThrow(() -> new ServiceException("合同计费方案不存在或已变更,请重新选择"));
}
private boolean isDefaultPlan(Map<String, Object> plan) { private boolean isDefaultPlan(Map<String, Object> plan) {
Object value = plan.get("defaultPlan"); Object value = plan.get("defaultPlan");
return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value)); return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value));
@@ -851,6 +1063,10 @@ public class ReceivablePayableDetailServiceImpl
} }
private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) {
if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) {
rebuildMasterOrderDetailFee(detail, billingPlanId);
return;
}
Waybill waybill = waybillService.getById(detail.getWaybillId()); Waybill waybill = waybillService.getById(detail.getWaybillId());
if (waybill == null) { if (waybill == null) {
throw new ServiceException("关联运单不存在"); throw new ServiceException("关联运单不存在");
@@ -869,6 +1085,38 @@ public class ReceivablePayableDetailServiceImpl
updateById(detail); updateById(detail);
} }
private void rebuildMasterOrderDetailFee(ReceivablePayableDetail detail, String billingPlanId) {
MasterOrder masterOrder = masterOrderMapper.selectOne(Wrappers.<MasterOrder>lambdaQuery()
.eq(MasterOrder::getMasterNo, detail.getWaybillNo())
.eq(MasterOrder::getIsDeleted, 0));
if (masterOrder == null) {
throw new ServiceException("关联总单不存在");
}
ContractManage contract = contractManageService.getById(detail.getContractId());
List<Waybill> masterGoods = masterOrderGoods(masterOrder);
List<ReceivablePayableCargoFee> fees = masterGoods.stream()
.flatMap(goods -> calculatedFees(goods, contract, billingPlanId).stream()).toList();
normalizeMasterFeeLines(fees);
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
fees.forEach(fee -> {
fee.setDetailId(detail.getId());
fee.setWaybillId(null);
cargoFeeMapper.insert(fee);
});
BigDecimal freight = fees.stream().filter(this::isFreight)
.map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
detail.setFreightAmount(freight);
detail.setOtherFeeAmount(total.subtract(freight));
detail.setTotalAmount(total);
detail.setUnitPrice(resolveContractUnitPrice(fees));
detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap<String, BigDecimal>::new,
(map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll)));
updateById(detail);
}
private void closeDetails(List<Long> ids, String settlementType) { private void closeDetails(List<Long> ids, String settlementType) {
if (Func.isEmpty(ids)) { if (Func.isEmpty(ids)) {
throw new ServiceException("请选择需要关闭的明细"); throw new ServiceException("请选择需要关闭的明细");
@@ -927,6 +1175,14 @@ public class ReceivablePayableDetailServiceImpl
.eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0; .eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0;
} }
private boolean existsByMasterOrder(String masterNo, String settlementType) {
return count(Wrappers.<ReceivablePayableDetail>lambdaQuery()
.eq(ReceivablePayableDetail::getSourceType, SOURCE_MASTER_ORDER)
.eq(ReceivablePayableDetail::getWaybillNo, masterNo)
.eq(ReceivablePayableDetail::getSettlementType, settlementType)
.eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0;
}
private String settlementType(String value) { private String settlementType(String value) {
if (Func.isEmpty(value)) return "receivable"; if (Func.isEmpty(value)) return "receivable";
if (!List.of("receivable", "payable").contains(value)) { if (!List.of("receivable", "payable").contains(value)) {
@@ -960,9 +1216,96 @@ public class ReceivablePayableDetailServiceImpl
map.put("transportType", waybill.getTransportType()); map.put("transportType", waybill.getTransportType());
map.put("carrierType", waybill.getCarrierType()); map.put("carrierType", waybill.getCarrierType());
map.put("cargoInfo", waybill.getCargoName()); map.put("cargoInfo", waybill.getCargoName());
map.put("departureAddress", waybill.getDepartureAddress());
map.put("departureContact", waybill.getDepartureContact());
map.put("arrivalAddress", waybill.getArrivalAddress());
map.put("arrivalContact", waybill.getArrivalContact());
map.put("unitPrice", waybill.getUnitPrice());
map.put("freight", waybillFreight(waybill));
map.put("otherFeeTotal", waybillOtherFee(waybill));
map.put("freightTotal", waybillFreightTotal(waybill));
map.put("contractName", waybill.getContractName());
map.put("planName", waybill.getPlanName());
map.put("batchNo", waybill.getBatchNo());
map.put("originalNo", waybill.getOriginalNo());
map.put("masterNo", waybill.getMasterNo());
map.put("remark", Func.isNotEmpty(waybill.getRemark()) ? waybill.getRemark() : waybill.getTaskRemark());
map.put("createTime", waybill.getCreateTime());
map.put("updateTime", waybill.getUpdateTime());
map.put("businessStatusName", waybillStatusName(waybill.getBusinessStatus()));
return map; return map;
} }
private BigDecimal waybillFreight(Waybill waybill) {
Map<String, Object> freight = waybillFreightMap(waybill);
Object value = firstValue(freight, "freight", "freightAmount", "transportFee");
if (value != null) return decimal(value);
if (freight.get("freightItems") instanceof List<?> items) {
BigDecimal total = BigDecimal.ZERO;
boolean hasAmount = false;
for (Object item : items) {
if (!(item instanceof Map<?, ?> source)) continue;
Object amount = firstValue(stringMap(source), "freightAmount", "amount", "totalAmount");
if (amount == null) continue;
total = total.add(decimal(amount));
hasAmount = true;
}
if (hasAmount) return total;
}
return null;
}
private BigDecimal waybillOtherFee(Waybill waybill) {
Map<String, Object> freight = waybillFreightMap(waybill);
Object value = firstValue(freight, "otherFeeTotal", "otherFreightAmount", "otherAmount");
return value == null ? waybill.getOtherFeeTotal() : decimal(value);
}
private BigDecimal waybillFreightTotal(Waybill waybill) {
Map<String, Object> freight = waybillFreightMap(waybill);
Object total = firstValue(freight, "freightTotal", "totalFreight", "totalFreightAmount", "totalAmount");
if (total != null) return decimal(total);
BigDecimal freightAmount = waybillFreight(waybill);
BigDecimal otherFeeAmount = waybillOtherFee(waybill);
if (freightAmount == null && otherFeeAmount == null) return null;
return money(freightAmount).add(money(otherFeeAmount));
}
private Map<String, Object> waybillFreightMap(Waybill waybill) {
if (Func.isEmpty(waybill.getFreightJson())) return new LinkedHashMap<>();
try {
Object parsed = JsonUtil.parse(waybill.getFreightJson(), Object.class);
if (parsed instanceof Map<?, ?> source) return stringMap(source);
if (parsed instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof Map<?, ?> source) {
return stringMap(source);
}
} catch (Exception ignored) {
// 兼容历史费用 JSON 异常数据
}
return new LinkedHashMap<>();
}
private Map<String, Object> stringMap(Map<?, ?> source) {
Map<String, Object> result = new LinkedHashMap<>();
source.forEach((key, value) -> result.put(String.valueOf(key), value));
return result;
}
private Object firstValue(Map<String, Object> map, String... keys) {
for (String key : keys) if (map.get(key) != null && !String.valueOf(map.get(key)).isBlank()) return map.get(key);
return null;
}
private String waybillStatusName(String status) {
return switch (status == null ? "" : status) {
case "completed" -> "已完成";
case "processing", "running", "in_progress", "inProgress" -> "进行中";
case "pending", "created" -> "待执行";
case "cancelled", "canceled" -> "已取消";
default -> status;
};
}
private Map<String, Object> candidateMap(ReceivablePayableDetailVO detail) { private Map<String, Object> candidateMap(ReceivablePayableDetailVO detail) {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("id", detail.getId()); map.put("id", detail.getId());
@@ -1038,6 +1381,10 @@ public class ReceivablePayableDetailServiceImpl
return value == null ? BigDecimal.ZERO : value.setScale(2, RoundingMode.HALF_UP); return value == null ? BigDecimal.ZERO : value.setScale(2, RoundingMode.HALF_UP);
} }
private BigDecimal normalizeGeneratedMileage(BigDecimal mileage) {
return mileage != null && mileage.compareTo(BigDecimal.valueOf(-1)) == 0 ? null : mileage;
}
private String formatMoney(BigDecimal value) { private String formatMoney(BigDecimal value) {
return money(value).toPlainString(); return money(value).toPlainString();
} }
@@ -425,7 +425,7 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl<ShippingTemplat
} }
private synchronized String nextCode() { private synchronized String nextCode() {
String prefix = "MBJH-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String prefix = "MB-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
List<ShippingTemplate> latestList = list(Wrappers.<ShippingTemplate>lambdaQuery() List<ShippingTemplate> latestList = list(Wrappers.<ShippingTemplate>lambdaQuery()
.select(ShippingTemplate::getTemplateCode) .select(ShippingTemplate::getTemplateCode)
.likeRight(ShippingTemplate::getTemplateCode, prefix) .likeRight(ShippingTemplate::getTemplateCode, prefix)
@@ -37,11 +37,13 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.mapper.WaybillMapper;
import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.ProcessConfig;
import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingManageVO; import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.pojo.vo.WaybillVO; import org.springblade.transport.pojo.vo.WaybillVO;
import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.IWaybillService; import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.TransportBusinessSupport;
@@ -72,6 +74,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@jakarta.annotation.Resource @jakarta.annotation.Resource
private ILoadingManageService loadingManageService; private ILoadingManageService loadingManageService;
@jakarta.annotation.Resource
private IProcessConfigService processConfigService;
@jakarta.annotation.Resource @jakarta.annotation.Resource
@org.springframework.context.annotation.Lazy @org.springframework.context.annotation.Lazy
private IReceivablePayableDetailService receivablePayableDetailService; private IReceivablePayableDetailService receivablePayableDetailService;
@@ -93,9 +98,14 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
boolean created = Func.isEmpty(waybill.getId()); boolean created = Func.isEmpty(waybill.getId());
if (!created) { if (!created) {
Waybill oldRecord = loadEditable(waybill.getId(), true); Waybill oldRecord = loadEditable(waybill.getId(), true);
assertNotLoaded(oldRecord);
waybill.setWaybillNo(oldRecord.getWaybillNo()); waybill.setWaybillNo(oldRecord.getWaybillNo());
waybill.setLoadingNo(oldRecord.getLoadingNo());
waybill.setDeptId(oldRecord.getDeptId()); waybill.setDeptId(oldRecord.getDeptId());
waybill.setDeptName(oldRecord.getDeptName()); waybill.setDeptName(oldRecord.getDeptName());
} else {
waybill.setLoadingNo(null);
fillProjectProcessConfig(waybill);
} }
prepare(waybill); prepare(waybill);
if (created && Func.isEmpty(waybill.getWaybillNo())) { if (created && Func.isEmpty(waybill.getWaybillNo())) {
@@ -105,6 +115,33 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
return saveOrUpdate(waybill); return saveOrUpdate(waybill);
} }
private void fillProjectProcessConfig(Waybill waybill) {
if (Func.isNotEmpty(waybill.getProcessJson()) || Func.isEmpty(waybill.getProjectId())) {
return;
}
String projectId = String.valueOf(waybill.getProjectId());
processConfigService.list(Wrappers.<ProcessConfig>lambdaQuery()
.eq(ProcessConfig::getStatus, 1)
.eq(ProcessConfig::getIsDeleted, 0)
.like(ProcessConfig::getProjectIds, projectId)
.orderByDesc(ProcessConfig::getCreateTime))
.stream()
.filter(processConfig -> containsProjectId(processConfig.getProjectIds(), projectId))
.map(ProcessConfig::getNodeConfigJson)
.filter(Func::isNotEmpty)
.findFirst()
.ifPresent(waybill::setProcessJson);
}
private boolean containsProjectId(String projectIds, String projectId) {
if (Func.isEmpty(projectIds)) {
return false;
}
return List.of(projectIds.split(",")).stream()
.map(String::trim)
.anyMatch(projectId::equals);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public BusinessRemoveResultVO removeWaybill(String ids) { public BusinessRemoveResultVO removeWaybill(String ids) {
@@ -223,20 +260,21 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
target.setPlanId(source.getPlanId()); target.setPlanId(source.getPlanId());
target.setPlanName(source.getPlanName()); target.setPlanName(source.getPlanName());
target.setMasterNo(source.getMasterNo()); target.setMasterNo(source.getMasterNo());
target.setLoadingNo(source.getLoadingNo()); target.setLoadingNo(null);
target.setBatchNo(source.getBatchNo()); target.setBatchNo(source.getBatchNo());
target.setRelationNo(source.getRelationNo()); target.setRelationNo(source.getRelationNo());
target.setCurrentProcessNode(source.getCurrentProcessNode()); target.setCurrentProcessNode(source.getCurrentProcessNode());
target.setGoodsJson(source.getGoodsJson()); target.setGoodsJson(source.getGoodsJson());
target.setCarrierJson(source.getCarrierJson()); target.setCarrierJson(source.getCarrierJson());
target.setTaskInfoJson(source.getTaskInfoJson()); target.setTaskInfoJson(source.getTaskInfoJson());
target.setProcessJson(source.getProcessJson()); target.setProcessJson(null);
target.setRouteJson(source.getRouteJson()); target.setRouteJson(source.getRouteJson());
target.setFreightJson(source.getFreightJson()); target.setFreightJson(source.getFreightJson());
target.setAttachmentsJson(source.getAttachmentsJson()); target.setAttachmentsJson(source.getAttachmentsJson());
target.setRemark(source.getRemark()); target.setRemark(source.getRemark());
target.setBusinessStatus("pending"); target.setBusinessStatus("pending");
target.setWaybillNo(nextCode()); target.setWaybillNo(nextCode());
fillProjectProcessConfig(target);
prepare(target); prepare(target);
validate(target); validate(target);
save(target); save(target);
@@ -247,6 +285,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean changeRoute(Waybill waybill) { public boolean changeRoute(Waybill waybill) {
Waybill oldRecord = loadEditable(waybill.getId(), true); Waybill oldRecord = loadEditable(waybill.getId(), true);
assertNotLoaded(oldRecord);
if ("completed".equals(oldRecord.getBusinessStatus()) || "cancelled".equals(oldRecord.getBusinessStatus())) { if ("completed".equals(oldRecord.getBusinessStatus()) || "cancelled".equals(oldRecord.getBusinessStatus())) {
throw new ServiceException("当前运单状态不允许变更运输路线"); throw new ServiceException("当前运单状态不允许变更运输路线");
} }
@@ -265,6 +304,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean cancel(Long id) { public boolean cancel(Long id) {
Waybill waybill = loadEditable(id, true); Waybill waybill = loadEditable(id, true);
assertNotLoaded(waybill);
if ("completed".equals(waybill.getBusinessStatus()) || "cancelled".equals(waybill.getBusinessStatus())) { if ("completed".equals(waybill.getBusinessStatus()) || "cancelled".equals(waybill.getBusinessStatus())) {
throw new ServiceException("当前状态不允许取消"); throw new ServiceException("当前状态不允许取消");
} }
@@ -276,6 +316,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean reassign(Long id) { public boolean reassign(Long id) {
Waybill waybill = loadEditable(id, true); Waybill waybill = loadEditable(id, true);
assertNotLoaded(waybill);
if (!"pending".equals(waybill.getBusinessStatus())) { if (!"pending".equals(waybill.getBusinessStatus())) {
throw new ServiceException("仅待执行运单允许重新派单"); throw new ServiceException("仅待执行运单允许重新派单");
} }
@@ -294,6 +335,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
boolean updated = updateById(waybill); boolean updated = updateById(waybill);
if (updated) { if (updated) {
receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId())); receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId()));
loadingManageService.completeIfAllWaybillsCompleted(waybill.getLoadingNo());
} }
return updated; return updated;
} }
@@ -636,7 +678,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
} }
private boolean shouldSkipDelete(Waybill waybill) { private boolean shouldSkipDelete(Waybill waybill) {
return false; return Func.isNotEmpty(waybill.getLoadingNo());
}
private void assertNotLoaded(Waybill waybill) {
if (Func.isNotEmpty(waybill.getLoadingNo())) {
throw new ServiceException("运单已关联配载单,请在配载单中修改");
}
} }
private void validateRoadTaskInfo(Waybill waybill) { private void validateRoadTaskInfo(Waybill waybill) {