package com.ruoyi.outsourcing.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.basic.mapper.ProductMapper;
|
import com.ruoyi.basic.mapper.ProductModelMapper;
|
import com.ruoyi.basic.pojo.Product;
|
import com.ruoyi.basic.pojo.ProductModel;
|
import com.ruoyi.common.enums.ReviewStatusEnum;
|
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.utils.OrderUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
import com.ruoyi.outsourcing.dto.OutsourcingOrderDto;
|
import com.ruoyi.outsourcing.enums.OutsourcingOrderStatusEnum;
|
import com.ruoyi.outsourcing.excel.OutsourcingOrderExcelDto;
|
import com.ruoyi.outsourcing.mapper.OutsourcingOrderMapper;
|
import com.ruoyi.outsourcing.mapper.OutsourcingOrderProductMapper;
|
import com.ruoyi.outsourcing.mapper.OutsourcingPartnerMapper;
|
import com.ruoyi.outsourcing.pojo.OutsourcingOrder;
|
import com.ruoyi.outsourcing.pojo.OutsourcingOrderProduct;
|
import com.ruoyi.outsourcing.pojo.OutsourcingPartner;
|
import com.ruoyi.outsourcing.service.IOutsourcingOrderService;
|
import com.ruoyi.stock.dto.StockInventoryDto;
|
import com.ruoyi.stock.pojo.StockOutRecord;
|
import com.ruoyi.stock.service.StockInventoryService;
|
import com.ruoyi.stock.service.StockOutRecordService;
|
import jakarta.servlet.http.HttpServletResponse;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import java.util.stream.Collectors;
|
|
/**
|
* 委外订单 Service 实现
|
*/
|
@Service
|
@RequiredArgsConstructor
|
public class OutsourcingOrderServiceImpl extends ServiceImpl<OutsourcingOrderMapper, OutsourcingOrder>
|
implements IOutsourcingOrderService {
|
|
private static final String ORDER_NO_PREFIX = "WWDD";
|
|
private final OutsourcingOrderMapper outsourcingOrderMapper;
|
private final OutsourcingOrderProductMapper outsourcingOrderProductMapper;
|
private final OutsourcingPartnerMapper outsourcingPartnerMapper;
|
private final ProductMapper productMapper;
|
private final ProductModelMapper productModelMapper;
|
private final StockInventoryService stockInventoryService;
|
private final StockOutRecordService stockOutRecordService;
|
|
@Override
|
public List<OutsourcingOrder> selectOrderList(OutsourcingOrderDto dto) {
|
LambdaQueryWrapper<OutsourcingOrder> wrapper = new LambdaQueryWrapper<>();
|
wrapper.like(StringUtils.isNotBlank(dto.getOrderNo()), OutsourcingOrder::getOrderNo, dto.getOrderNo())
|
.eq(dto.getPartnerId() != null, OutsourcingOrder::getPartnerId, dto.getPartnerId())
|
.like(StringUtils.isNotBlank(dto.getPartnerName()), OutsourcingOrder::getPartnerName, dto.getPartnerName())
|
.eq(dto.getStatus() != null, OutsourcingOrder::getStatus, dto.getStatus())
|
.like(StringUtils.isNotBlank(dto.getProjectName()), OutsourcingOrder::getProjectName, dto.getProjectName())
|
.ge(StringUtils.isNotBlank(dto.getStartDate()), OutsourcingOrder::getApplyDate, dto.getStartDate())
|
.le(StringUtils.isNotBlank(dto.getEndDate()), OutsourcingOrder::getApplyDate, dto.getEndDate())
|
.orderByDesc(OutsourcingOrder::getId);
|
return this.list(wrapper);
|
}
|
|
@Override
|
public OutsourcingOrderDto getDetail(Long id) {
|
OutsourcingOrder order = outsourcingOrderMapper.selectById(id);
|
if (order == null) {
|
throw new ServiceException("委外订单不存在");
|
}
|
OutsourcingOrderDto dto = new OutsourcingOrderDto();
|
BeanUtils.copyProperties(order, dto);
|
dto.setProductData(listProducts(id));
|
return dto;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long saveOrder(OutsourcingOrderDto dto) {
|
OutsourcingOrder order = new OutsourcingOrder();
|
BeanUtils.copyProperties(dto, order);
|
order.setId(null);
|
order.setStatus(OutsourcingOrderStatusEnum.DRAFT.getCode());
|
order.setOrderNo(OrderUtils.countTodayByCreateTime(
|
outsourcingOrderMapper, ORDER_NO_PREFIX, "order_no", LocalDateTime.now()));
|
order.setConfirmTime(null);
|
order.setCancelReason(null);
|
fillPartnerName(order);
|
if (order.getApplyDate() == null) {
|
order.setApplyDate(LocalDate.now());
|
}
|
order.setContractAmount(sumProducts(dto.getProductData()));
|
this.save(order);
|
|
saveProducts(order.getId(), dto.getProductData());
|
return order.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public boolean updateOrder(OutsourcingOrderDto dto) {
|
if (dto.getId() == null) {
|
throw new ServiceException("委外订单id不能为空");
|
}
|
OutsourcingOrder existing = outsourcingOrderMapper.selectById(dto.getId());
|
if (existing == null) {
|
throw new ServiceException("委外订单不存在");
|
}
|
if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(existing.getStatus())) {
|
throw new ServiceException("仅草稿状态的委外订单允许修改");
|
}
|
|
OutsourcingOrder order = new OutsourcingOrder();
|
BeanUtils.copyProperties(dto, order);
|
// 单号、状态、确认信息由系统维护,置空后 MyBatis-Plus 不会更新这些列
|
order.setOrderNo(null);
|
order.setStatus(null);
|
order.setConfirmTime(null);
|
order.setCancelReason(null);
|
order.setTenantId(null);
|
order.setDeptId(null);
|
order.setCreateUser(null);
|
order.setCreateTime(null);
|
order.setUpdateTime(null);
|
fillPartnerName(order);
|
order.setContractAmount(sumProducts(dto.getProductData()));
|
boolean updated = this.updateById(order);
|
|
if (updated) {
|
outsourcingOrderProductMapper.delete(Wrappers.<OutsourcingOrderProduct>lambdaQuery()
|
.eq(OutsourcingOrderProduct::getOutsourcingOrderId, dto.getId()));
|
saveProducts(dto.getId(), dto.getProductData());
|
}
|
return updated;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public boolean deleteOrders(List<Long> ids) {
|
if (ids == null || ids.isEmpty()) {
|
throw new ServiceException("请选择要删除的委外订单");
|
}
|
List<String> notDraftNos = this.listByIds(ids).stream()
|
.filter(order -> !OutsourcingOrderStatusEnum.DRAFT.getCode().equals(order.getStatus()))
|
.map(OutsourcingOrder::getOrderNo)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toList());
|
if (!notDraftNos.isEmpty()) {
|
throw new ServiceException("仅草稿状态的委外订单允许删除:" + String.join("、", notDraftNos));
|
}
|
|
outsourcingOrderProductMapper.delete(Wrappers.<OutsourcingOrderProduct>lambdaQuery()
|
.in(OutsourcingOrderProduct::getOutsourcingOrderId, ids));
|
return this.removeByIds(ids);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public boolean submit(Long id) {
|
OutsourcingOrder order = requireOrder(id);
|
if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(order.getStatus())) {
|
throw new ServiceException("仅草稿状态的委外订单允许提交");
|
}
|
if (order.getPartnerId() == null
|
|| outsourcingPartnerMapper.selectById(order.getPartnerId()) == null) {
|
throw new ServiceException("请先选择有效合作商再提交");
|
}
|
List<OutsourcingOrderProduct> products = listProducts(id);
|
if (products.isEmpty()) {
|
throw new ServiceException("委外订单没有产品明细,无法提交");
|
}
|
if (order.getContractAmount() == null
|
|| order.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
throw new ServiceException("委外订单金额必须大于 0 才能提交");
|
}
|
|
// 条件更新抢占草稿状态,避免并发下重复提交
|
boolean submitted = this.update(Wrappers.<OutsourcingOrder>lambdaUpdate()
|
.eq(OutsourcingOrder::getId, id)
|
.eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.DRAFT.getCode())
|
.set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode())
|
.set(OutsourcingOrder::getConfirmTime, LocalDateTime.now()));
|
if (!submitted) {
|
throw new ServiceException("该委外订单已提交,请勿重复操作");
|
}
|
occupyScrapStock(products);
|
return true;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public boolean complete(Long id) {
|
OutsourcingOrder order = requireOrder(id);
|
if (!OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(order.getStatus())) {
|
throw new ServiceException("仅已确认状态的委外订单允许完成");
|
}
|
requireScrapStockOutApproved(order.getId());
|
boolean completed = this.update(Wrappers.<OutsourcingOrder>lambdaUpdate()
|
.eq(OutsourcingOrder::getId, id)
|
.eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode())
|
.set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.COMPLETED.getCode()));
|
if (!completed) {
|
throw new ServiceException("仅已确认状态的委外订单允许完成");
|
}
|
return true;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public boolean cancel(Long id, String cancelReason) {
|
OutsourcingOrder order = requireOrder(id);
|
Integer status = order.getStatus();
|
if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(status)
|
&& !OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(status)) {
|
throw new ServiceException("该委外订单已完成或已作废,不允许重复作废");
|
}
|
boolean cancelled = this.update(Wrappers.<OutsourcingOrder>lambdaUpdate()
|
.eq(OutsourcingOrder::getId, id)
|
.in(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.DRAFT.getCode(),
|
OutsourcingOrderStatusEnum.CONFIRMED.getCode())
|
.set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CANCELLED.getCode())
|
.set(OutsourcingOrder::getCancelReason, cancelReason));
|
if (!cancelled) {
|
throw new ServiceException("该委外订单已完成或已作废,不允许重复作废");
|
}
|
if (OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(status)) {
|
// 提交时占用的报废品库存要释放:未审批的记录删掉即释放,已审批通过的按批次归还库存
|
stockOutRecordService.releaseByRecordIds(scrapLineIds(id),
|
StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode());
|
}
|
return true;
|
}
|
|
@Override
|
public void exportOrders(HttpServletResponse response, OutsourcingOrderDto dto) {
|
List<OutsourcingOrder> orders = selectOrderList(dto);
|
List<OutsourcingOrderExcelDto> excelList = orders.stream().map(order -> {
|
OutsourcingOrderExcelDto excel = new OutsourcingOrderExcelDto();
|
BeanUtils.copyProperties(order, excel);
|
excel.setStatusLabel(OutsourcingOrderStatusEnum.getLabelByValue(order.getStatus()));
|
return excel;
|
}).collect(Collectors.toList());
|
ExcelUtil<OutsourcingOrderExcelDto> util = new ExcelUtil<>(OutsourcingOrderExcelDto.class);
|
util.exportExcel(response, excelList, "委外订单");
|
}
|
|
private OutsourcingOrder requireOrder(Long id) {
|
if (id == null) {
|
throw new ServiceException("委外订单id不能为空");
|
}
|
OutsourcingOrder order = outsourcingOrderMapper.selectById(id);
|
if (order == null) {
|
throw new ServiceException("委外订单不存在");
|
}
|
return order;
|
}
|
|
/**
|
* 报废品选料行的 id,作为出库记录的 record_id。
|
* 出库记录一条对应一行明细(一个规格+批号),与采购退货出库的记法一致
|
*/
|
private List<Long> scrapLineIds(Long orderId) {
|
return listProducts(orderId).stream()
|
.filter(product -> Boolean.TRUE.equals(product.getIsScrap()))
|
.map(OutsourcingOrderProduct::getId)
|
.collect(Collectors.toList());
|
}
|
|
/**
|
* 报废品选料占用库存:每条选料行写一条待审批出库记录。
|
*
|
* <p>报废品取的是<b>合格库存</b>(stock_inventory,出库记录 type=0)。
|
* 记录一落库就通过 stock_out_record 的待审批量把可用量占住
|
* (可用量 = qualitity − locked_quantity − 待审批出库量),因此同一批次不会被多张委外订单超选,
|
* 但 qualitity 本身要等出库管理审批通过才真正扣减 —— 也就是「提交即占用、审批才实扣」。</p>
|
*/
|
private void occupyScrapStock(List<OutsourcingOrderProduct> products) {
|
for (OutsourcingOrderProduct product : products) {
|
if (!Boolean.TRUE.equals(product.getIsScrap())) {
|
continue;
|
}
|
if (product.getProductModelId() == null || StringUtils.isEmpty(product.getBatchNo())) {
|
throw new ServiceException("报废品选料行缺少产品规格或批号,无法提交");
|
}
|
BigDecimal quantity = product.getQuantity();
|
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
|
throw new ServiceException("报废品选料行数量必须大于 0");
|
}
|
StockInventoryDto stockInventoryDto = new StockInventoryDto();
|
stockInventoryDto.setRecordId(product.getId());
|
stockInventoryDto.setRecordType(StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode());
|
stockInventoryDto.setProductModelId(product.getProductModelId());
|
stockInventoryDto.setBatchNo(product.getBatchNo());
|
stockInventoryDto.setQualitity(quantity);
|
try {
|
stockInventoryService.addStockOutRecordOnly(stockInventoryDto);
|
} catch (RuntimeException e) {
|
// 合格库存不足抛的是 ServiceException,不合格那种 BaseException 也一并兜住
|
throw new ServiceException("报废品「" + product.getSpecificationModel() + "」批号「"
|
+ product.getBatchNo() + "」" + e.getMessage());
|
}
|
}
|
}
|
|
/**
|
* 报废品选料的出库记录必须全部审批通过才允许完成订单,
|
* 否则会出现「委外已完成、库存没扣」的账实不符
|
*/
|
private void requireScrapStockOutApproved(Long orderId) {
|
List<Long> lineIds = scrapLineIds(orderId);
|
if (lineIds.isEmpty()) {
|
return;
|
}
|
List<StockOutRecord> records = stockOutRecordService.listByRecordIds(
|
lineIds, StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode());
|
if (records.size() < lineIds.size()) {
|
throw new ServiceException("报废品选料缺少出库记录,请先作废后重新提交");
|
}
|
List<String> pendingBatches = records.stream()
|
.filter(record -> !ReviewStatusEnum.APPROVED.getCode().equals(record.getApprovalStatus()))
|
.map(StockOutRecord::getOutboundBatches)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toList());
|
if (!pendingBatches.isEmpty()) {
|
throw new ServiceException("报废品出库未审批通过,请先到出库管理审批出库批次:"
|
+ String.join("、", pendingBatches));
|
}
|
}
|
|
private List<OutsourcingOrderProduct> listProducts(Long orderId) {
|
return outsourcingOrderProductMapper.selectList(Wrappers.<OutsourcingOrderProduct>lambdaQuery()
|
.eq(OutsourcingOrderProduct::getOutsourcingOrderId, orderId)
|
.orderByAsc(OutsourcingOrderProduct::getId));
|
}
|
|
private void saveProducts(Long orderId, List<OutsourcingOrderProduct> products) {
|
if (products == null || products.isEmpty()) {
|
return;
|
}
|
fillProductNames(products);
|
for (OutsourcingOrderProduct product : products) {
|
product.setId(null);
|
product.setOutsourcingOrderId(orderId);
|
product.setTenantId(null);
|
product.setDeptId(null);
|
product.setCreateUser(null);
|
product.setCreateTime(null);
|
product.setUpdateTime(null);
|
outsourcingOrderProductMapper.insert(product);
|
}
|
}
|
|
/**
|
* 委外金额以明细含税总价合计为准,避免前端传入金额与明细对不上导致往来应付错乱
|
*/
|
private BigDecimal sumProducts(List<OutsourcingOrderProduct> products) {
|
if (products == null || products.isEmpty()) {
|
return BigDecimal.ZERO;
|
}
|
return products.stream()
|
.map(OutsourcingOrderProduct::getTaxInclusiveTotalPrice)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
}
|
|
/**
|
* 补全产品大类与规格型号,便于订单直接展示产品名称
|
*/
|
private void fillProductNames(List<OutsourcingOrderProduct> products) {
|
Set<Long> productIds = products.stream()
|
.map(OutsourcingOrderProduct::getProductId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
Set<Long> modelIds = products.stream()
|
.map(OutsourcingOrderProduct::getProductModelId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
|
Map<Long, String> productMap = new HashMap<>();
|
if (!productIds.isEmpty()) {
|
for (Product product : productMapper.selectBatchIds(productIds)) {
|
productMap.put(product.getId(), product.getProductName());
|
}
|
}
|
Map<Long, String> modelMap = new HashMap<>();
|
if (!modelIds.isEmpty()) {
|
for (ProductModel model : productModelMapper.selectBatchIds(modelIds)) {
|
modelMap.put(model.getId(), model.getModel());
|
}
|
}
|
|
for (OutsourcingOrderProduct product : products) {
|
String productName = productMap.get(product.getProductId());
|
if (productName != null) {
|
product.setProductCategory(productName);
|
}
|
String model = modelMap.get(product.getProductModelId());
|
if (model != null) {
|
product.setSpecificationModel(model);
|
}
|
}
|
}
|
|
private void fillPartnerName(OutsourcingOrder order) {
|
if (order.getPartnerId() == null) {
|
return;
|
}
|
OutsourcingPartner partner = outsourcingPartnerMapper.selectById(order.getPartnerId());
|
if (partner == null) {
|
throw new ServiceException("合作商不存在,请重新选择");
|
}
|
order.setPartnerName(partner.getPartnerName());
|
}
|
}
|