chenhj
2026-04-24 8b749305a8e8c3f4bf669f6980edf2bbb7bfa9c7
Merge branch 'dev_New_pro' of http://114.132.189.42:9002/r/product-inventory-management-after into dev_New_pro
已修改11个文件
704 ■■■■■ 文件已修改
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanDto.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanImportDto.java 159 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionPlanVo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionPlanMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionPlan.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 316 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionPlanMapper.xml 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanDto.java
@@ -16,30 +16,10 @@
@Data
public class ProductionPlanDto extends ProductionPlan {
    /**
     * 物料编码
     */
    @Schema(description = "物料编码")
    @Excel(name = "物料编码")
    private String materialCode;
    /**
     * 产品名称
     */
    @Schema(description = "产品名称")
    @Excel(name = "产品名称")
    private String productName;
    /**
     * 客户名称
     */
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    /**
     * 产品规格
     */
    @Schema(description = "产品规格")
    @Excel(name = "产品规格")
    private String model;
@@ -59,17 +39,7 @@
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate planCompleteTime;
    /**
     * 关联物料信息表
     */
    @Schema(description = "关联物料信息表ID")
    private Long productMaterialId;
    /**
     * 强度
     */
    @Schema(description = "强度")
    @Excel(name = "强度")
    private String strength;
    @Schema(description = "产品ID")
    private Long productId;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanImportDto.java
@@ -1,12 +1,11 @@
package com.ruoyi.production.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.time.LocalDate;
/**
 * <br>
@@ -20,126 +19,43 @@
@Data
@Schema(name = "销售生产需求 Excel导入导出DTO")
public class ProductionPlanImportDto {
    /**
     * 申请单编号
     */
    @Schema(description = "申请单编号")
    @Excel(name = "申请单编号")
    private String applyNo;
    /**
     * 客户名称
     */
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    @Schema(description = "主生产计划号")
    @Excel(name = "主生产计划号")
    private String mpsNo;
    /**
     * 物料编码
     */
    @Schema(description = "物料编码")
    @Excel(name = "物料编码")
    private String materialCode;
    @Schema(description = "需求日期")
    @Excel(name = "需求日期")
    private LocalDate requiredDate;
    /**
     * 产品名称
     */
    @Schema(description = "备注")
    @Excel(name = "备注")
    private String remark;
    @Schema(description = "需求数量")
    @Excel(name = "需求数量")
    private BigDecimal qtyRequired;
    @Schema(description = "来源 销售/内部")
    @Excel(name = "来源 销售/内部")
    private String source;
    @Schema(description = "承诺日期")
    @Excel(name = "承诺日期")
    private LocalDate promisedDeliveryDate;
    @Schema(description = "产品名称")
    @Excel(name = "产品名称")
    private String productName;
    /**
     * 产品规格
     */
    @Schema(description = "产品规格")
    @Excel(name = "产品规格")
    private String productSpec;
    @Schema(description = "规格型号")
    @Excel(name = "规格型号")
    private String model;
    /**
     * 长
     */
    @Schema(description = "长")
    @Excel(name = "长(mm)")
    private Integer length;
    @Schema(description = "单位")
    @Excel(name = "单位")
    private String unit;
    /**
     * 宽
     */
    @Schema(description = "宽")
    @Excel(name = "宽(mm)")
    private Integer width;
    /**
     * 高
     */
    @Schema(description = "高")
    @Excel(name = "高(mm)")
    private Integer height;
    /**
     * 块数
     */
    @Schema(description = "块数")
    @Excel(name = "块数")
    private Integer quantity;
    /**
     * 方数
     */
    @Schema(description = "方数")
    @Excel(name = "方数")
    private BigDecimal volume;
    /**
     * 强度
     */
    @Schema(description = "强度")
    @Excel(name = "强度")
    private String strength;
    /**
     * 开始日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Schema(description = "开始日期")
    @Excel(name = "开始日期", width = 20, dateFormat = "yyyy-MM-dd")
    private Date startDate;
    /**
     * 结束日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Schema(description = "结束日期")
    @Excel(name = "结束日期", width = 20, dateFormat = "yyyy-MM-dd")
    private Date endDate;
    /**
     * 提交人
     */
    @Schema(description = "提交人")
    @Excel(name = "提交人")
    private String submitter;
    /**
     * 提交人组织
     */
    @Schema(description = "提交人组织")
    @Excel(name = "提交人组织")
    private String submitOrg;
    /**
     * 备注1
     */
    @Schema(description = "备注1")
    @Excel(name = "备注1")
    private String remarkOne;
    /**
     * 备注2
     */
    @Schema(description = "备注2")
    @Excel(name = "备注2")
    private String remarkTwo;
    /**
     * 创建人
@@ -155,22 +71,11 @@
    @Excel(name = "修改人", type = Excel.Type.EXPORT)
    private String modifierName;
    /**
     * 数据同步类型:1=手动 2=定时任务
     */
    @Schema(description = "数据同步类型:1=手动 2=定时任务")
    private Integer dataSyncType;
    /**
     * 数据来源类型:1=同步 2=新增
     * 已下发数量
     */
    @Schema(description = "数据来源类型:1=同步 2=新增")
    private Integer dataSourceType;
    /**
     * 下发数量
     */
    @Schema(description = "下发数量")
    @Excel(name = "下发数量", type = Excel.Type.EXPORT)
    @Schema(description = "已下发数量")
    @Excel(name = "已下发数量", type = Excel.Type.EXPORT)
    private BigDecimal assignedQuantity;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionPlanVo.java
@@ -9,8 +9,6 @@
@Data
@Schema(name = "ProductionPlanVo", description = "生产计划返回对象")
public class ProductionPlanVo extends ProductionPlan {
    @Schema(description = "物料编码")
    private String materialCode;
    @Schema(description = "产品名称")
    private String productName;
@@ -21,6 +19,6 @@
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "关联产品物料ID")
    private Long productMaterialId;
    @Schema(description = "产品ID")
    private Long productId;
}
src/main/java/com/ruoyi/production/mapper/ProductionPlanMapper.java
@@ -27,5 +27,4 @@
    List<ProductionPlanDto> selectWithMaterialByIds(@Param("ids") List<Long> ids);
    ProductionPlanDto selectProductionPlanDtoById(@Param("productionPlanId") Long productionPlanId);
}
src/main/java/com/ruoyi/production/pojo/ProductionPlan.java
@@ -30,6 +30,12 @@
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "销售台账id")
    private Long salesLedgerId;
    @Schema(description = "销售产品规格id")
    private Long salesLedgerProductId;
    @Schema(description = "主生产计划号")
    private String mpsNo;
@@ -55,6 +61,10 @@
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @Schema(description = "部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "产品型号id")
    private Long productModelId;
@@ -64,7 +74,7 @@
    @Schema(description = "是否下发制造订单")
    private Boolean issued;
    @Schema(description = "来源")
    @Schema(description = "来源 销售/内部")
    private String source;
    @Schema(description = "审核状态")
@@ -72,9 +82,6 @@
    @Schema(description = "承诺日期")
    private LocalDate promisedDeliveryDate;
    @Schema(description = "申请单编号")
    private String applyNo;
    @Schema(description = "状态 0未下发 1部分下发 2已下发")
    private Integer status;
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -62,7 +63,7 @@
    private final FileUtil fileUtil;
    @Override
    public com.baomidou.mybatisplus.core.metadata.IPage<ProductionOrderVo> pageProductionOrder(Page<ProductionOrderDto> page, ProductionOrderDto dto) {
    public IPage<ProductionOrderVo> pageProductionOrder(Page<ProductionOrderDto> page, ProductionOrderDto dto) {
        Page<ProductionOrderVo> result = (Page<ProductionOrderVo>) baseMapper.pageProductionOrder(page, dto);
        fillProductImages(result.getRecords());
        return result;
@@ -359,18 +360,18 @@
        if (defaultDecimal(productionOrder.getQuantity()).compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("quantity must be greater than 0");
        }
        if (productionOrder.getTechnologyRoutingId() == null) {
            // 未显式指定工艺路线时,按产品规格选最新一条工艺作为默认路线。
            TechnologyRouting technologyRouting = technologyRoutingMapper.selectOne(
                    Wrappers.<TechnologyRouting>lambdaQuery()
                            .eq(TechnologyRouting::getProductModelId, productionOrder.getProductModelId())
                            .orderByDesc(TechnologyRouting::getId)
                            .last("limit 1"));
            if (technologyRouting == null) {
                throw new ServiceException("No technology routing found for the product model");
            }
            productionOrder.setTechnologyRoutingId(technologyRouting.getId());
        }
//        if (productionOrder.getTechnologyRoutingId() == null) {
//            // 未显式指定工艺路线时,按产品规格选最新一条工艺作为默认路线。
//            TechnologyRouting technologyRouting = technologyRoutingMapper.selectOne(
//                    Wrappers.<TechnologyRouting>lambdaQuery()
//                            .eq(TechnologyRouting::getProductModelId, productionOrder.getProductModelId())
//                            .orderByDesc(TechnologyRouting::getId)
//                            .last("limit 1"));
//            if (technologyRouting == null) {
//                throw new ServiceException("No technology routing found for the product model");
//            }
//            productionOrder.setTechnologyRoutingId(technologyRouting.getId());
//        }
        if (oldOrder != null && ProductOrderStatusEnum.isStarted(oldOrder.getStatus())) {
            // 开工后只允许修正非核心字段,核心生产依据锁定。
            if (!Objects.equals(oldOrder.getProductModelId(), productionOrder.getProductModelId())
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -17,6 +18,7 @@
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 jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@@ -24,20 +26,24 @@
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.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ProductionPlanServiceImpl extends ServiceImpl<ProductionPlanMapper, ProductionPlan> implements ProductionPlanService {
    private final ProductionPlanMapper productionPlanMapper;
    private static final int PLAN_STATUS_WAIT = 0;
    private static final int PLAN_STATUS_PARTIAL = 1;
    private static final int PLAN_STATUS_ISSUED = 2;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderService productionOrderService;
    @Override
    public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
@@ -45,133 +51,98 @@
    }
    /**
     * 合并生产计划
     * 合并生产计划并下发生产订单。
     * 业务约束:
     * 1. 仅允许同一产品型号的计划合并;
     * 2. 已下发或部分下发的计划禁止再次合并;
     * 3. 下发数量不能大于所选计划需求总量;
     * 4. 订单创建统一复用 ProductionOrderService.saveProductionOrder,确保工艺/BOM/领料主单等后续逻辑一致。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean combine(ProductionPlanDto productionPlanDto) {
        if (productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
        if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
            return false;
        }
        //  查询主生产计划
        List<ProductionPlanDto> plans = productionPlanMapper.selectWithMaterialByIds(productionPlanDto.getIds());
        if (plans == null || plans.isEmpty()) {
            throw new ServiceException("下发失败,生产计划不存在");
        List<Long> planIds = productionPlanDto.getIds().stream()
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (planIds.isEmpty()) {
            throw new ServiceException("下发失败,未选择生产计划");
        }
        //  校验是否存在不同的产品名称
        String firstProductName = plans.get(0).getProductName();
        if (plans.stream().anyMatch(p -> p.getProductName() == null || !p.getProductName().equals(firstProductName))) {
            throw new BaseException("合并失败,存在不同的产品名称");
        List<ProductionPlanDto> plans = productionPlanMapper.selectWithMaterialByIds(planIds);
        if (plans == null || plans.isEmpty() || plans.size() != planIds.size()) {
            throw new ServiceException("下发失败,生产计划不存在或已被删除");
        }
        // 校验是否存在不同的产品规格
        String firstProductSpec = plans.get(0).getModel();
        if (plans.stream().anyMatch(p -> p.getModel() == null || !p.getModel().equals(firstProductSpec))) {
            throw new BaseException("合并失败,存在不同的产品规格");
        ProductionPlanDto firstPlan = plans.getFirst();
        if (firstPlan.getProductModelId() == null) {
            throw new ServiceException("下发失败,生产计划缺少产品型号");
        }
        // 创建生产订单
        boolean hasDifferentModel = plans.stream()
                .anyMatch(item -> !Objects.equals(item.getProductModelId(), firstPlan.getProductModelId()));
        if (hasDifferentModel) {
            throw new BaseException("合并失败,所选生产计划的产品型号不一致");
        }
        boolean hasIssuedPlan = plans.stream()
                .anyMatch(item -> item.getStatus() != null && item.getStatus() != PLAN_STATUS_WAIT);
        if (hasIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发或部分下发数据");
        }
        BigDecimal totalRequiredQuantity = plans.stream()
                .map(ProductionPlan::getQtyRequired)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalRequiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,所选生产计划需求总量必须大于0");
        }
        BigDecimal assignedQuantity = productionPlanDto.getTotalAssignedQuantity();
        if (assignedQuantity == null || assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,下发数量必须大于0");
        }
        if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
            throw new ServiceException("下发失败,下发数量不能大于计划需求总量");
        }
        ProductionOrder productionOrder = new ProductionOrder();
        productionOrder.setQuantity(productionPlanDto.getTotalAssignedQuantity());
        productionOrder.setProductionPlanIds(formatPlanIds(planIds));
        productionOrder.setProductModelId(firstPlan.getProductModelId());
        productionOrder.setQuantity(assignedQuantity);
        productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime());
//        // 叠加剩余方数
//        BigDecimal totalRemainingVolume = plans.stream()
//                .map(ProductionPlan::getRemainingVolume)
//                .filter(Objects::nonNull)
//                .reduce(BigDecimal.ZERO, BigDecimal::add);
//        // 判断下发数量是否大于等于剩余方数
//        if (productionPlanDto.getTotalAssignedQuantity().compareTo(totalRemainingVolume) > 0) {
//            throw new BaseException("操作失败,下发数量不能大于剩余方数");
//        }
//
//        // 创建生产订单
//        ProductOrder productOrder = new ProductOrder();
//        productOrder.setQuantity(productionPlanDto.getTotalAssignedQuantity());
//        productOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime());
//        productOrder.setStatus(ProductOrderStatusEnum.WAIT.getCode());
//        productOrder.setStrength(productionPlanDto.getStrength());
//        productOrder.setProductMaterialSkuId(plans.get(0).getProductMaterialSkuId());
//
//        Long orderId = productOrderService.insertProductOrder(productOrder);
//
//        //  当下发的产品为砌块或板材,就拉取BOM子集与工艺路线子集数据存入到附表中
//        if ("砌块".equals(productionPlanDto.getProductName())) {
//            productOrder.setRouteId(productionOrderAppendixService.populateBlocks(orderId, productionPlanDto));
//        }
//        if ("板材".equals(productionPlanDto.getProductName())) {
//            productOrder.setRouteId(productionOrderAppendixService.populatePlates(orderId, productionPlanDto));
//        }
//        //  更新绑定的工艺路线
//        productOrderService.updateById(productOrder);
//
//        // 根据下发数量,从第一个生产计划开始分配方数
//        BigDecimal assignedVolume = BigDecimal.ZERO;
//        for (ProductionPlan plan : plans) {
//            BigDecimal volume = plan.getVolume();
//            if (volume == null) {
//                continue;
//            }
//            // 计算剩余方数
//            BigDecimal remainingVolume = plan.getRemainingVolume();
//            if (remainingVolume.compareTo(BigDecimal.ZERO) <= 0) {
//                continue;
//            }
//
//            ProductOrderPlan productOrderPlan = new ProductOrderPlan();
//            productOrderPlan.setProductOrderId(productOrder.getId());
//            productOrderPlan.setProductionPlanId(plan.getId());
//
//            if (assignedVolume.add(remainingVolume).compareTo(productionPlanDto.getTotalAssignedQuantity()) >= 0) {
//                // 最后一个计划,分配剩余方数
//                BigDecimal lastRemainingVolume = productionPlanDto.getTotalAssignedQuantity().subtract(assignedVolume);
//                BigDecimal assignedQuantity = Optional.ofNullable(plan.getAssignedQuantity()).orElse(BigDecimal.ZERO).add(lastRemainingVolume);
//                plan.setAssignedQuantity(assignedQuantity);
//                plan.setStatus(assignedQuantity.compareTo(plan.getVolume()) >= 0 ? 2 : 1);
//                productOrderPlan.setAssignedQuantity(lastRemainingVolume);
//                productionPlanMapper.updateById(plan);
//                productOrderPlanMapper.insert(productOrderPlan);
//                break;
//            }
//
//            // 分配当前计划方数
//            BigDecimal assignedQuantity = Optional.ofNullable(plan.getAssignedQuantity()).orElse(BigDecimal.ZERO).add(remainingVolume);
//            plan.setAssignedQuantity(assignedQuantity);
//            plan.setStatus(assignedQuantity.compareTo(plan.getVolume()) >= 0 ? 2 : 1);
//            productOrderPlan.setAssignedQuantity(remainingVolume);
//            // 更新生产计划
//            productionPlanMapper.updateById(plan);
//            // 创建关联关系
//            productOrderPlanMapper.insert(productOrderPlan);
//            assignedVolume = assignedVolume.add(remainingVolume);
//        }
//
//        for (ProductionPlan plan : plans) {
//            BigDecimal assignedQuantity = Optional.ofNullable(plan.getAssignedQuantity()).orElse(BigDecimal.ZERO);
//            BigDecimal volume = Optional.ofNullable(plan.getVolume()).orElse(BigDecimal.ZERO);
//            if (assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
//                plan.setStatus(0);
//            } else if (assignedQuantity.compareTo(volume) >= 0) {
//                plan.setStatus(2);
//            } else {
//                plan.setStatus(1);
//            }
//            productionPlanMapper.updateById(plan);
//        }
        boolean saved = productionOrderService.saveProductionOrder(productionOrder);
        if (!saved) {
            throw new ServiceException("下发失败,生产订单保存失败");
        }
        int targetStatus = assignedQuantity.compareTo(totalRequiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED;
        List<ProductionPlan> updates = planIds.stream().map(id -> {
            ProductionPlan update = new ProductionPlan();
            update.setId(id);
            update.setStatus(targetStatus);
            return update;
        }).collect(Collectors.toList());
        if (!updates.isEmpty()) {
            this.updateBatchById(updates);
        }
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean add(ProductionPlanDto dto) {
        if (StringUtils.isBlank(dto.getApplyNo())) {
            throw new ServiceException("新增失败,申请单编号不能为空");
        }
        checkApplyNoUnique(dto.getApplyNo(), null);
        dto.setStatus(0);
        if (StringUtils.isBlank(dto.getMpsNo())) {
            dto.setMpsNo(generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        }else checkMpsNoUnique(dto.getMpsNo(), null);
        dto.setStatus(PLAN_STATUS_WAIT);
        dto.setSource("内部");
        return productionPlanMapper.insert(dto) > 0;
    }
@@ -179,51 +150,49 @@
    @Transactional(rollbackFor = Exception.class)
    public boolean update(ProductionPlanDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("编辑失败,数据不能为空");
            throw new ServiceException("编辑失败,数据不能为空");
        }
        ProductionPlan old = getById(dto.getId());
        if (old == null) {
            throw new ServiceException("编辑失败,主生产计划不存在");
            throw new ServiceException("编辑失败,生产计划不存在");
        }
        // 状态校验
        if (old.getStatus() != 0) {
            throw new BaseException("编辑失败,该生产计划已下发或部分下发状态,禁止编辑");
        if (old.getStatus() != PLAN_STATUS_WAIT) {
            throw new BaseException("编辑失败,该生产计划已下发或部分下发,禁止编辑");
        }
        // applyNo变更才校验
        if (StringUtils.isNotBlank(dto.getApplyNo())
                && !dto.getApplyNo().equals(old.getApplyNo())) {
            checkApplyNoUnique(dto.getApplyNo(), dto.getId());
        if (StringUtils.isNotBlank(dto.getMpsNo()) && !dto.getMpsNo().equals(old.getMpsNo())) {
            checkMpsNoUnique(dto.getMpsNo(), dto.getId());
        }
        return productionPlanMapper.updateById(dto) > 0;
    }
    private void checkApplyNoUnique(String applyNo, Long excludeId) {
    private void checkMpsNoUnique(String mpsNo, Long excludeId) {
        LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(ProductionPlan::getApplyNo, applyNo);
        wrapper.eq(ProductionPlan::getMpsNo, mpsNo);
        if (excludeId != null) {
            wrapper.ne(ProductionPlan::getId, excludeId);
        }
        if (productionPlanMapper.selectCount(wrapper) > 0) {
            throw new ServiceException("申请单编号 " + applyNo + " 已存在");
            throw new ServiceException("生产计划号 " + mpsNo + " 已存在");
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean delete(List<Long> ids) {
        // 如果存在已下发的计划,则不能删除
        if (productionPlanMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getId, ids)).stream().anyMatch(p -> p.getStatus() == 1 || p.getStatus() == 2)) {
        if (productionPlanMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getId, ids))
                .stream()
                .anyMatch(p -> p.getStatus() == PLAN_STATUS_PARTIAL || p.getStatus() == PLAN_STATUS_ISSUED)) {
            throw new BaseException("删除失败,存在已下发或部分下发的计划");
        }
        // 如果有关联订单,则不能删除
        if (productionOrderMapper.selectList(Wrappers.<ProductionOrder>lambdaQuery().in(ProductionOrder::getProductionPlanIds, ids)).stream().anyMatch(p -> p.getId() != null)) {
            throw new BaseException("删除失败,存在关联订单");
        if (productionOrderMapper.selectList(Wrappers.<ProductionOrder>lambdaQuery().in(ProductionOrder::getProductionPlanIds, ids))
                .stream()
                .anyMatch(p -> p.getId() != null)) {
            throw new BaseException("删除失败,存在关联生产订单");
        }
        return productionPlanMapper.deleteBatchIds(ids) > 0;
@@ -240,80 +209,81 @@
        try {
            list = excelUtil.importExcel(file.getInputStream());
        } catch (Exception e) {
            log.error("生产需求Excel导入失败", e);
            throw new ServiceException("Excel解析失败");
        }
        if (list == null || list.isEmpty()) {
            throw new ServiceException("Excel没有数据");
        }
        Set<String> applyNos = new HashSet<>();
        Set<String> materialCodes = new HashSet<>();
        Set<String> mpsNos = new HashSet<>();
        for (int i = 0; i < list.size(); i++) {
            ProductionPlanImportDto dto = list.get(i);
            String applyNo = dto.getApplyNo();
            String materialCode = dto.getMaterialCode();
            if (StringUtils.isEmpty(applyNo)) {
                throw new ServiceException("导入失败:第 " + (i + 2) + " 行申请单编号不能为空");
            String mpsNo = dto.getMpsNo();
            if (StringUtils.isEmpty(mpsNo)) {
                generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
            }
            if (!applyNos.add(applyNo)) {
                throw new ServiceException("导入失败:Excel 中存在重复的申请单编号: " + applyNo);
            }
            if (StringUtils.isEmpty(materialCode)) {
                throw new ServiceException("导入失败:第 " + (i + 2) + " 行物料编码不能为空");
            }
            String strength = dto.getStrength();
            if (StringUtils.isNotEmpty(strength)) {
                if (!"A3.5".equals(strength) && !"A5.0".equals(strength)) {
                    throw new ServiceException("导入失败:第 " + (i + 2) + " 行强度只能是 A3.5 或 A5.0");
            if (!mpsNos.add(mpsNo)) {
                throw new ServiceException("导入失败:Excel 中存在重复的申请单编号 " + mpsNo);
                }
            }
            materialCodes.add(materialCode);
        }
        //  申请单编号是否已存在
        Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery()
                .in(ProductionPlan::getApplyNo, applyNos));
                .in(ProductionPlan::getMpsNo, mpsNos));
        if (existApplyNoCount > 0) {
            List<String> existApplyNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery()
                            .in(ProductionPlan::getApplyNo, applyNos))
                    .stream().map(ProductionPlan::getApplyNo).collect(Collectors.toList());
            throw new ServiceException("导入失败,申请单编号已存在: " + String.join(", ", existApplyNos));
            List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery()
                            .in(ProductionPlan::getMpsNo, mpsNos))
                    .stream()
                    .map(ProductionPlan::getMpsNo)
                    .collect(Collectors.toList());
            throw new ServiceException("导入失败,生产计划号已存在: " + String.join(", ", existMpsNos));
        }
        LocalDateTime now = LocalDateTime.now();
        List<ProductionPlan> entityList = list.stream().map(dto -> {
            ProductionPlan entity = new ProductionPlan();
            BeanUtils.copyProperties(dto, entity);
            entity.setStatus(0);
            entity.setStatus(PLAN_STATUS_WAIT);
            entity.setSource("内部");
            entity.setCreateTime(now);
            entity.setUpdateTime(now);
            return entity;
        }).collect(Collectors.toList());
        this.saveBatch(entityList);
    }
    @Override
    public void exportProdData(HttpServletResponse response, List<Long> ids) {
        List<ProductionPlan> list;
        if (ids != null && !ids.isEmpty()) {
            list = baseMapper.selectBatchIds(ids);
        } else {
            list = baseMapper.selectList(null);
        }
        List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids);
        List<ProductionPlanImportDto> exportList = new ArrayList<>();
        for (ProductionPlan entity : list) {
        for (ProductionPlanDto entity : list) {
            ProductionPlanImportDto dto = new ProductionPlanImportDto();
            BeanUtils.copyProperties(entity, dto);
            exportList.add(dto);
        }
        ExcelUtil<ProductionPlanImportDto> util = new ExcelUtil<>(ProductionPlanImportDto.class);
        util.exportExcel(response, exportList, "销售生产需求数据");
        util.exportExcel(response, exportList, "主生产计划");
    }
    private String formatPlanIds(List<Long> planIds) {
        return planIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    }
    private String generateNextPlanNo(String datePrefix) {
        QueryWrapper<ProductionPlan> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("mps_no", "JH" + datePrefix);
        queryWrapper.orderByDesc("mps_no");
        queryWrapper.last("LIMIT 1");
        ProductionPlan latestPlan = productionPlanMapper.selectOne(queryWrapper);
        int sequence = 1;
        if (latestPlan != null && latestPlan.getMpsNo() != null && !latestPlan.getMpsNo().isEmpty()) {
            String sequenceStr = latestPlan.getMpsNo().substring(("JH" + datePrefix).length());
            try {
                sequence = Integer.parseInt(sequenceStr) + 1;
            } catch (NumberFormatException e) {
                sequence = 1;
            }
        }
        return "JH" + datePrefix + String.format("%04d", sequence);
    }
}
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
@@ -232,6 +232,7 @@
    private BigDecimal ticketsTotal = BigDecimal.ZERO;
    @Schema(description = "是否质检")
    //针对采购台账,是否质检
    private Boolean isChecked;
    @TableField(exist = false)
@@ -251,4 +252,8 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "是否生产")
    //针对销售台账,是否生产
    private Boolean isProduction;
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -6,20 +6,14 @@
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.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.sales.dto.InvoiceRegistrationProductDto;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.mapper.InvoiceRegistrationProductMapper;
@@ -68,7 +62,7 @@
    private final ProductionAccountMapper productionAccountMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderService productionOrderService;
    private final TechnologyRoutingMapper technologyRoutingMapper;
@@ -256,70 +250,36 @@
     * 新增生产数据
     */
    public void addProductionData(SalesLedgerProduct salesLedgerProduct) {
        ProductionOrder productionOrder = new ProductionOrder();
        productionOrder.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
        productionOrder.setProductModelId(salesLedgerProduct.getProductModelId());
        productionOrder.setSaleLedgerProductId(salesLedgerProduct.getId().intValue());
        productionOrder.setNpsNo(generateNextOrderNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        productionOrder.setQuantity(salesLedgerProduct.getQuantity());
        productionOrder.setCompleteQuantity(BigDecimal.ZERO);
        //先判断该产品是否需要生产
        if (!salesLedgerProduct.getIsProduction()) {
            return;
        }
        ProductionPlan productionPlan = new ProductionPlan();
        productionPlan.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
        productionPlan.setProductModelId(salesLedgerProduct.getProductModelId());
        productionPlan.setSalesLedgerProductId(salesLedgerProduct.getId());
        productionPlan.setMpsNo(generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        productionPlan.setQtyRequired(salesLedgerProduct.getQuantity());
        productionPlanMapper.insert(productionPlan);
        TechnologyRouting routing = technologyRoutingMapper.selectOne(
                new QueryWrapper<TechnologyRouting>().lambda()
                        .eq(TechnologyRouting::getProductModelId, salesLedgerProduct.getProductModelId())
                        .orderByDesc(TechnologyRouting::getCreateTime)
                        .last("LIMIT 1"));
        if (routing != null) {
            productionOrder.setTechnologyRoutingId(routing.getId());
        }
        productionOrderMapper.insert(productionOrder);
        if (productionOrder.getTechnologyRoutingId() != null) {
            productionOrderService.syncProductionOrderSnapshot(productionOrder.getId());
        }
    }
    /**
     * 删除生产数据
     * 删除生产计划
     */
    public void deleteProductionData(List<Long> productIds) {
        List<ProductionOrder> productionOrders = productionOrderMapper.selectList(
                new LambdaQueryWrapper<ProductionOrder>()
                        .in(ProductionOrder::getSaleLedgerProductId, productIds.stream().map(Long::intValue).collect(Collectors.toList())));
        if (org.springframework.util.CollectionUtils.isEmpty(productionOrders)) {
        List<ProductionPlan> productionPlans = productionPlanMapper.selectList(
                new LambdaQueryWrapper<ProductionPlan>()
                        .in(ProductionPlan::getSalesLedgerProductId, productIds.stream().map(Long::intValue).collect(Collectors.toList())));
        if (org.springframework.util.CollectionUtils.isEmpty(productionPlans)) {
            return;
        }
        List<Long> orderIds = productionOrders.stream().map(ProductionOrder::getId).collect(Collectors.toList());
        List<Long> taskIds = productionOperationTaskMapper.selectList(
                        new LambdaQueryWrapper<ProductionOperationTask>()
                                .in(ProductionOperationTask::getProductionOrderId, orderIds))
                .stream().map(ProductionOperationTask::getId).collect(Collectors.toList());
        if (!taskIds.isEmpty()) {
            List<ProductionProductMain> productMains = productionProductMainMapper.selectList(
                    new LambdaQueryWrapper<ProductionProductMain>()
                            .in(ProductionProductMain::getProductionOperationTaskId, taskIds));
            List<Long> productMainIds = productMains.stream().map(ProductionProductMain::getId).collect(Collectors.toList());
            if (!productMainIds.isEmpty()) {
                List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(
                        new LambdaQueryWrapper<QualityInspect>().in(QualityInspect::getProductMainId, productMainIds));
                qualityInspects.forEach(qualityInspect -> {
                    if (qualityInspect.getInspectState() == 1) {
                        throw new RuntimeException("已提交的检验单不能删除");
        //如果生产计划已下发则不能删除
        if (productionPlans.stream().anyMatch(productionPlan -> productionPlan.getStatus() != 0)) {
            throw new RuntimeException("生产计划已下发,不能删除该销售产品");
                    }
                });
                productionProductOutputMapper.deleteByProductMainIds(productMainIds);
                productionProductInputMapper.deleteByProductMainIds(productMainIds);
                qualityInspectMapper.deleteByProductMainIds(productMainIds);
                productionAccountMapper.delete(new LambdaQueryWrapper<ProductionAccount>()
                        .in(ProductionAccount::getProductionProductMainId, productMainIds));
                for (Long productMainId : productMainIds) {
                    stockUtils.deleteStockOutRecord(productMainId, StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode());
                    stockUtils.deleteStockInRecord(productMainId, StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
                }
            }
            productionProductMainMapper.delete(new LambdaQueryWrapper<ProductionProductMain>()
                    .in(ProductionProductMain::getProductionOperationTaskId, taskIds));
        }
        productionOrderService.removeProductionOrder(orderIds);
        List<Long> ids = productionPlans.stream().map(ProductionPlan::getId).collect(Collectors.toList());
        productionPlanMapper.deleteByIds(ids);
    }
    @Override
@@ -432,21 +392,21 @@
        return R.ok();
    }
    private String generateNextOrderNo(String datePrefix) {
        QueryWrapper<ProductionOrder> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("nps_no", "SC" + datePrefix);
        queryWrapper.orderByDesc("nps_no");
    private String generateNextPlanNo(String datePrefix) {
        QueryWrapper<ProductionPlan> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("mps_no", "JH" + datePrefix);
        queryWrapper.orderByDesc("mps_no");
        queryWrapper.last("LIMIT 1");
        ProductionOrder latestOrder = productionOrderMapper.selectOne(queryWrapper);
        ProductionPlan latestPlan = productionPlanMapper.selectOne(queryWrapper);
        int sequence = 1;
        if (latestOrder != null && latestOrder.getNpsNo() != null && !latestOrder.getNpsNo().isEmpty()) {
            String sequenceStr = latestOrder.getNpsNo().substring(("SC" + datePrefix).length());
        if (latestPlan != null && latestPlan.getMpsNo() != null && !latestPlan.getMpsNo().isEmpty()) {
            String sequenceStr = latestPlan.getMpsNo().substring(("JH" + datePrefix).length());
            try {
                sequence = Integer.parseInt(sequenceStr) + 1;
            } catch (NumberFormatException e) {
                sequence = 1;
            }
        }
        return "SC" + datePrefix + String.format("%04d", sequence);
        return "JH" + datePrefix + String.format("%04d", sequence);
    }
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -520,7 +520,7 @@
        List<Long> productIds = products.stream()
                .map(SalesLedgerProduct::getId)
                .collect(Collectors.toList());
        //删除生产数据
        //删除生产计划
        salesLedgerProductServiceImpl.deleteProductionData(productIds);
        // 批量删除产品子表
src/main/resources/mapper/production/ProductionPlanMapper.xml
@@ -23,31 +23,25 @@
    <select id="listPage" resultType="com.ruoyi.production.bean.vo.ProductionPlanVo">
        SELECT
        pp.*,
        pms.material_code AS materialCode,
        pmdl.model,
        pms.product_id AS productMaterialId,
        pm.model,
        p.id as productId,
        p.product_name AS productName,
        pmdl.unit
        pm.unit
        FROM production_plan pp
        left join product_material_sku pms on pp.product_material_sku_id = pms.id
        left join product_model pmdl on pp.product_model_id = pmdl.id
        left join product p on pmdl.product_id = p.id
        ORDER BY COALESCE(pp.form_modified_time, pp.id) DESC
        left join product_model pm on pp.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        ORDER BY COALESCE(pp.id) DESC
    </select>
    <select id="selectWithMaterialByIds" resultType="com.ruoyi.production.bean.dto.ProductionPlanDto">
        SELECT
        pp.*,
        pms.material_code AS materialCode,
        pmdl.model,
        pm.model,
        p.product_name AS productName,
        pmdl.unit,
        pms.product_id AS productMaterialId
        pm.unit
        FROM production_plan pp
        LEFT JOIN product_material_sku pms ON pp.product_material_sku_id = pms.id
        LEFT JOIN product_model pmdl ON pp.product_model_id = pmdl.id
        LEFT JOIN product p ON pmdl.product_id = p.id
        LEFT JOIN product_model pm ON pp.product_model_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        WHERE pp.id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
@@ -57,15 +51,12 @@
    <select id="selectProductionPlanDtoById" resultType="com.ruoyi.production.bean.dto.ProductionPlanDto">
        SELECT
        pp.*,
        pms.material_code AS materialCode,
        pmdl.model,
        pms.product_id AS productMaterialId,
        pm.model,
        p.product_name AS productName,
        pmdl.unit
        pm.unit
        FROM production_plan pp
        left join product_material_sku pms on pp.product_material_sku_id = pms.id
        left join product_model pmdl on pp.product_model_id = pmdl.id
        left join product p on pmdl.product_id = p.id
        left join product_model pm on pp.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        WHERE pp.id = #{productionPlanId}
    </select>
</mapper>