package com.ruoyi.production.service.impl;
|
|
import cn.hutool.core.bean.BeanUtil;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.production.bean.dto.ProductionPlanDto;
|
import com.ruoyi.production.bean.dto.ProductionOrderDto;
|
import com.ruoyi.production.bean.vo.ProductionPlanVo;
|
import com.ruoyi.production.mapper.ProductionPlanMapper;
|
import com.ruoyi.production.pojo.ProductionOrder;
|
import com.ruoyi.production.pojo.ProductionPlan;
|
import com.ruoyi.production.service.ProductionOrderService;
|
import com.ruoyi.production.service.ProductionPlanService;
|
import com.ruoyi.sales.mapper.SalesLedgerMapper;
|
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
|
import com.ruoyi.sales.pojo.SalesLedger;
|
import com.ruoyi.sales.pojo.SalesLedgerProduct;
|
import jakarta.servlet.http.HttpServletResponse;
|
import lombok.AllArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.Comparator;
|
import java.util.List;
|
import java.util.Objects;
|
import java.util.stream.Collectors;
|
|
@Service
|
@RequiredArgsConstructor
|
@Transactional(rollbackFor = Exception.class)
|
public class ProductionPlanServiceImpl extends ServiceImpl<ProductionPlanMapper, ProductionPlan> implements ProductionPlanService {
|
|
private final ProductionOrderService productionOrderService;
|
private final SalesLedgerMapper salesLedgerMapper;
|
private final SalesLedgerProductMapper salesLedgerProductMapper;
|
|
@Override
|
public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
|
Page<ProductionPlan> entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
return this.page(entityPage, buildQueryWrapper(productionPlanDto))
|
.convert(item -> BeanUtil.copyProperties(item, ProductionPlanVo.class));
|
}
|
|
@Override
|
public void loadProdData() {
|
// 用销售明细作为来源同步生产计划,source 字段承担幂等去重作用。
|
List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(
|
Wrappers.<SalesLedgerProduct>lambdaQuery()
|
.isNotNull(SalesLedgerProduct::getProductModelId)
|
.gt(SalesLedgerProduct::getQuantity, BigDecimal.ZERO));
|
for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
|
String source = buildSalesSource(salesLedgerProduct.getId());
|
long exists = this.count(Wrappers.<ProductionPlan>lambdaQuery().eq(ProductionPlan::getSource, source));
|
if (exists > 0) {
|
continue;
|
}
|
SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerProduct.getSalesLedgerId());
|
ProductionPlan productionPlan = new ProductionPlan();
|
productionPlan.setMpsNo(generateNextPlanNo());
|
productionPlan.setProductModelId(salesLedgerProduct.getProductModelId());
|
productionPlan.setQtyRequired(defaultDecimal(salesLedgerProduct.getQuantity()));
|
productionPlan.setRequiredDate(resolveDeliveryTime(salesLedger));
|
productionPlan.setPromisedDeliveryDate(resolveDeliveryTime(salesLedger));
|
productionPlan.setSource(source);
|
productionPlan.setIssued(Boolean.FALSE);
|
productionPlan.setState("1");
|
this.save(productionPlan);
|
}
|
}
|
|
@Override
|
public void syncProdDataJob() {
|
loadProdData();
|
}
|
|
@Override
|
public boolean combine(ProductionPlanDto productionPlanDto) {
|
// 多个计划合并转单后,仍统一走生产订单保存逻辑,避免两套下单流程不一致。
|
if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
|
throw new ServiceException("请选择生产计划");
|
}
|
List<ProductionPlan> productionPlans = this.listByIds(productionPlanDto.getIds());
|
if (productionPlans.size() != productionPlanDto.getIds().size()) {
|
throw new ServiceException("部分生产计划不存在");
|
}
|
if (productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued()))) {
|
throw new ServiceException("已下发的生产计划不能重复转单");
|
}
|
Long productModelId = productionPlans.stream()
|
.map(ProductionPlan::getProductModelId)
|
.distinct()
|
.reduce((left, right) -> {
|
throw new ServiceException("仅支持相同产品规格的生产计划合并转单");
|
})
|
.orElseThrow(() -> new ServiceException("生产计划缺少产品规格"));
|
BigDecimal totalRequiredQuantity = productionPlans.stream()
|
.map(ProductionPlan::getQtyRequired)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
BigDecimal assignedQuantity = defaultDecimal(productionPlanDto.getTotalAssignedQuantity());
|
if (assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
|
assignedQuantity = totalRequiredQuantity;
|
}
|
if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
|
throw new ServiceException("下发数量不能大于计划总需求数量");
|
}
|
|
ProductionOrder productionOrder = new ProductionOrder();
|
productionOrder.setProductModelId(productModelId);
|
productionOrder.setQuantity(assignedQuantity);
|
productionOrder.setProductionPlanIds(formatPlanIds(productionPlans));
|
productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime() != null
|
? productionPlanDto.getPlanCompleteTime()
|
: resolveEarliestPlanDate(productionPlans));
|
productionOrder.setStrength(productionPlanDto.getStrength());
|
return productionOrderService.saveProductionOrder(productionOrder);
|
}
|
|
@Override
|
public boolean add(ProductionPlanDto productionPlanDto) {
|
// 手工建计划时补齐默认编号和状态,保证后续可以直接下发转单。
|
ProductionPlan productionPlan = BeanUtil.copyProperties(productionPlanDto, ProductionPlan.class);
|
validateProductionPlan(productionPlan, false);
|
if (productionPlan.getMpsNo() == null || productionPlan.getMpsNo().trim().isEmpty()) {
|
productionPlan.setMpsNo(generateNextPlanNo());
|
}
|
if (productionPlan.getIssued() == null) {
|
productionPlan.setIssued(Boolean.FALSE);
|
}
|
if (productionPlan.getState() == null || productionPlan.getState().trim().isEmpty()) {
|
productionPlan.setState("1");
|
}
|
return this.save(productionPlan);
|
}
|
|
@Override
|
public boolean update(ProductionPlanDto productionPlanDto) {
|
// 已下发计划的核心字段不能再变更,否则会和已生成订单脱节。
|
if (productionPlanDto == null || productionPlanDto.getId() == null) {
|
throw new ServiceException("生产计划ID不能为空");
|
}
|
ProductionPlan current = this.getById(productionPlanDto.getId());
|
if (current == null) {
|
throw new ServiceException("生产计划不存在");
|
}
|
if (Boolean.TRUE.equals(current.getIssued()) && hasPlanCoreChanges(current, productionPlanDto)) {
|
throw new ServiceException("已下发的生产计划不允许修改产品、数量和交期");
|
}
|
ProductionPlan update = BeanUtil.copyProperties(productionPlanDto, ProductionPlan.class);
|
validateProductionPlan(update, true);
|
return this.updateById(update);
|
}
|
|
@Override
|
public boolean delete(List<Long> ids) {
|
// 删除前校验 issued,避免把已转单计划直接从源头删掉。
|
if (ids == null || ids.isEmpty()) {
|
return false;
|
}
|
List<ProductionPlan> productionPlans = this.listByIds(ids);
|
if (productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued()))) {
|
throw new ServiceException("已下发的生产计划不允许删除");
|
}
|
return this.removeByIds(ids);
|
}
|
|
@Override
|
public void importProdData(MultipartFile file) {
|
|
}
|
|
@Override
|
public void exportProdData(HttpServletResponse response, List<Long> ids) {
|
|
}
|
|
private LambdaQueryWrapper<ProductionPlan> buildQueryWrapper(ProductionPlanDto dto) {
|
ProductionPlan query = dto == null ? new ProductionPlan() : dto;
|
return Wrappers.<ProductionPlan>lambdaQuery()
|
.eq(query.getId() != null, ProductionPlan::getId, query.getId())
|
.eq(query.getProductModelId() != null, ProductionPlan::getProductModelId, query.getProductModelId())
|
.eq(query.getIssued() != null, ProductionPlan::getIssued, query.getIssued())
|
.eq(query.getState() != null && !query.getState().trim().isEmpty(), ProductionPlan::getState, query.getState())
|
.eq(query.getIsAudit() != null && !query.getIsAudit().trim().isEmpty(), ProductionPlan::getIsAudit, query.getIsAudit())
|
.like(query.getMpsNo() != null && !query.getMpsNo().trim().isEmpty(), ProductionPlan::getMpsNo, query.getMpsNo())
|
.like(query.getSource() != null && !query.getSource().trim().isEmpty(), ProductionPlan::getSource, query.getSource())
|
.orderByDesc(ProductionPlan::getId);
|
}
|
|
private void validateProductionPlan(ProductionPlan productionPlan, boolean update) {
|
if (productionPlan == null) {
|
throw new ServiceException("生产计划不能为空");
|
}
|
if (update && productionPlan.getId() == null) {
|
throw new ServiceException("生产计划ID不能为空");
|
}
|
if (productionPlan.getProductModelId() == null) {
|
throw new ServiceException("productModelId不能为空");
|
}
|
if (defaultDecimal(productionPlan.getQtyRequired()).compareTo(BigDecimal.ZERO) <= 0) {
|
throw new ServiceException("qtyRequired必须大于0");
|
}
|
}
|
|
private boolean hasPlanCoreChanges(ProductionPlan current, ProductionPlanDto update) {
|
// 这些字段会直接影响转单结果,用来判断是否属于核心变更。
|
return !Objects.equals(current.getProductModelId(), update.getProductModelId())
|
|| defaultDecimal(current.getQtyRequired()).compareTo(defaultDecimal(update.getQtyRequired())) != 0
|
|| !Objects.equals(toLocalDate(current.getPromisedDeliveryDate()), update.getPlanCompleteTime())
|
|| !Objects.equals(toLocalDate(current.getRequiredDate()), toLocalDate(update.getRequiredDate()));
|
}
|
|
private String generateNextPlanNo() {
|
// 编号按日期递增生成,方便和订单、工单统一追踪。
|
String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
String prefix = "MPS" + datePrefix;
|
ProductionPlan latestPlan = this.getOne(Wrappers.<ProductionPlan>lambdaQuery()
|
.likeRight(ProductionPlan::getMpsNo, prefix)
|
.orderByDesc(ProductionPlan::getMpsNo)
|
.last("limit 1"));
|
int sequence = 1;
|
if (latestPlan != null && latestPlan.getMpsNo() != null && latestPlan.getMpsNo().startsWith(prefix)) {
|
try {
|
sequence = Integer.parseInt(latestPlan.getMpsNo().substring(prefix.length())) + 1;
|
} catch (NumberFormatException ignored) {
|
sequence = 1;
|
}
|
}
|
return prefix + String.format("%04d", sequence);
|
}
|
|
private String formatPlanIds(List<ProductionPlan> productionPlans) {
|
return productionPlans.stream()
|
.map(ProductionPlan::getId)
|
.distinct()
|
.map(String::valueOf)
|
.collect(Collectors.joining(",", "[", "]"));
|
}
|
|
private LocalDate resolveEarliestPlanDate(List<ProductionPlan> productionPlans) {
|
return productionPlans.stream()
|
.map(this::resolvePlanDate)
|
.filter(Objects::nonNull)
|
.min(Comparator.naturalOrder())
|
.orElse(null);
|
}
|
|
private LocalDate resolvePlanDate(ProductionPlan productionPlan) {
|
if (productionPlan.getPromisedDeliveryDate() != null) {
|
return productionPlan.getPromisedDeliveryDate().toLocalDate();
|
}
|
if (productionPlan.getRequiredDate() != null) {
|
return productionPlan.getRequiredDate().toLocalDate();
|
}
|
return null;
|
}
|
|
private LocalDateTime resolveDeliveryTime(SalesLedger salesLedger) {
|
if (salesLedger == null || salesLedger.getDeliveryDate() == null) {
|
return null;
|
}
|
return salesLedger.getDeliveryDate().atStartOfDay();
|
}
|
|
private String buildSalesSource(Long salesLedgerProductId) {
|
return "salesLedgerProduct:" + salesLedgerProductId;
|
}
|
|
private LocalDate toLocalDate(LocalDateTime value) {
|
return value == null ? null : value.toLocalDate();
|
}
|
|
private BigDecimal defaultDecimal(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
}
|