diff --git a/.agents/skills/karpathy-guidelines/SKILL.md b/.agents/skills/karpathy-guidelines/SKILL.md new file mode 100644 index 00000000..6a62d044 --- /dev/null +++ b/.agents/skills/karpathy-guidelines/SKILL.md @@ -0,0 +1,67 @@ +--- +name: karpathy-guidelines +description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria. +license: MIT +--- + +# Karpathy Guidelines + +Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. diff --git a/jeecg-boot/db/mes-xsl-mixing-production-plan-menu.sql b/jeecg-boot/db/mes-xsl-mixing-production-plan-menu.sql new file mode 100644 index 00000000..83a2b2eb --- /dev/null +++ b/jeecg-boot/db/mes-xsl-mixing-production-plan-menu.sql @@ -0,0 +1,79 @@ +-- 密炼生产计划维护 菜单与权限(挂载到 MES密炼工程) +SET NAMES utf8mb4; + +SET @mixer_parent_id = ( + SELECT id + FROM sys_permission + WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0 + ORDER BY create_time ASC + LIMIT 1 +); +SET @mixer_parent_id = IFNULL(@mixer_parent_id, ( + SELECT id + FROM sys_permission + WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0 + ORDER BY create_time ASC + LIMIT 1 +)); +SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001'); + +INSERT INTO sys_permission +(`id`,`parent_id`,`name`,`url`,`component`,`component_name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`always_show`,`icon`,`is_route`,`is_leaf`,`keep_alive`,`hidden`,`hide_tab`,`description`,`status`,`del_flag`,`create_by`,`create_time`,`rule_flag`,`internal_or_external`) +VALUES +('1860000000000000301',@mixer_parent_id,'密炼生产计划维护','/mes/mixingproductionplaninfo','mes/mixingproductionplaninfo/index','MesXslMixingProductionPlanList',1,NULL,'1',90,0,'ant-design:calendar-outlined',1,1,1,0,0,'密炼生产计划维护',1,0,'admin',NOW(),0,0) +ON DUPLICATE KEY UPDATE +`parent_id`=VALUES(`parent_id`), +`name`=VALUES(`name`), +`url`=VALUES(`url`), +`component`=VALUES(`component`), +`component_name`=VALUES(`component_name`), +`menu_type`=VALUES(`menu_type`), +`sort_no`=VALUES(`sort_no`), +`is_route`=VALUES(`is_route`), +`is_leaf`=VALUES(`is_leaf`), +`keep_alive`=VALUES(`keep_alive`), +`icon`=VALUES(`icon`), +`status`=VALUES(`status`), +`del_flag`=VALUES(`del_flag`); + +INSERT INTO sys_permission +(`id`,`parent_id`,`name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`status`,`del_flag`,`create_by`,`create_time`) +VALUES +('1860000000000000302','1860000000000000301','查询',2,'xslmes:mes_xsl_mixing_production_plan:list','1',1,'1',0,'admin',NOW()), +('1860000000000000303','1860000000000000301','整表保存',2,'xslmes:mes_xsl_mixing_production_plan:saveAll','1',2,'1',0,'admin',NOW()) +ON DUPLICATE KEY UPDATE +`name`=VALUES(`name`), +`perms`=VALUES(`perms`), +`sort_no`=VALUES(`sort_no`), +`status`=VALUES(`status`), +`del_flag`=VALUES(`del_flag`); + +-- admin 角色授权 +INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip) +SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1' +FROM sys_permission p +WHERE p.id IN ( + '1860000000000000301', + '1860000000000000302', '1860000000000000303' +) + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_permission rp + WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b' + AND rp.permission_id = p.id + ); + +-- 强制修复:确保菜单路由与组件路径正确 +UPDATE sys_permission +SET + parent_id = @mixer_parent_id, + url = '/mes/mixingproductionplaninfo', + component = 'mes/mixingproductionplaninfo/index', + component_name = 'MesXslMixingProductionPlanList', + menu_type = 1, + is_route = 1, + is_leaf = 1, + hidden = 0, + status = '1', + del_flag = 0 +WHERE id = '1860000000000000301'; diff --git a/jeecg-boot/db/mes-xsl-raw-material-demand-plan-menu.sql b/jeecg-boot/db/mes-xsl-raw-material-demand-plan-menu.sql new file mode 100644 index 00000000..3272f47a --- /dev/null +++ b/jeecg-boot/db/mes-xsl-raw-material-demand-plan-menu.sql @@ -0,0 +1,78 @@ +-- 原材料需求计划 菜单与权限(挂载到 MES密炼工程,兼容 MES管理) +SET NAMES utf8mb4; + +SET @raw_parent_id = ( + SELECT id + FROM sys_permission + WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0 + ORDER BY create_time ASC + LIMIT 1 +); +SET @raw_parent_id = IFNULL(@raw_parent_id, ( + SELECT id + FROM sys_permission + WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0 + ORDER BY create_time ASC + LIMIT 1 +)); +SET @raw_parent_id = IFNULL(@raw_parent_id, '1860000000000000001'); + +INSERT INTO sys_permission +(`id`,`parent_id`,`name`,`url`,`component`,`component_name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`always_show`,`icon`,`is_route`,`is_leaf`,`keep_alive`,`hidden`,`hide_tab`,`description`,`status`,`del_flag`,`create_by`,`create_time`,`rule_flag`,`internal_or_external`) +VALUES +('1900000000000000710',@raw_parent_id,'原材料需求计划','/mes/rawmaterialdemandplan','mes/rawmaterialdemandplan/index','MesXslRawMaterialDemandPlanList',1,NULL,'1',95,0,'ant-design:ordered-list-outlined',1,1,1,0,0,'原材料需求计划',1,0,'admin',NOW(),0,0) +ON DUPLICATE KEY UPDATE +`parent_id`=VALUES(`parent_id`), +`name`=VALUES(`name`), +`url`=VALUES(`url`), +`component`=VALUES(`component`), +`component_name`=VALUES(`component_name`), +`menu_type`=VALUES(`menu_type`), +`sort_no`=VALUES(`sort_no`), +`is_route`=VALUES(`is_route`), +`is_leaf`=VALUES(`is_leaf`), +`keep_alive`=VALUES(`keep_alive`), +`icon`=VALUES(`icon`), +`status`=VALUES(`status`), +`hidden`=VALUES(`hidden`), +`del_flag`=VALUES(`del_flag`); + +INSERT INTO sys_permission +(`id`,`parent_id`,`name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`status`,`del_flag`,`create_by`,`create_time`) +VALUES +('1900000000000000711','1900000000000000710','导出',2,'xslmes:mes_xsl_raw_material_demand_plan:exportXls','1',1,'1',0,'admin',NOW()) +ON DUPLICATE KEY UPDATE +`name`=VALUES(`name`), +`perms`=VALUES(`perms`), +`sort_no`=VALUES(`sort_no`), +`status`=VALUES(`status`), +`del_flag`=VALUES(`del_flag`); + +INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip) +SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1' +FROM sys_permission p +WHERE p.id IN ('1900000000000000710', '1900000000000000711') + AND NOT EXISTS ( + SELECT 1 + FROM sys_role_permission rp + WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b' + AND rp.permission_id = p.id + ); + +-- 强制修复:按 ID + 名称 + URL 三重兜底,确保能显示 +UPDATE sys_permission +SET + parent_id = @raw_parent_id, + url = '/mes/rawmaterialdemandplan', + component = 'mes/rawmaterialdemandplan/index', + component_name = 'MesXslRawMaterialDemandPlanList', + menu_type = 1, + is_route = 1, + is_leaf = 1, + hidden = 0, + status = '1', + del_flag = 0, + redirect = NULL +WHERE id = '1900000000000000710' + OR name = '原材料需求计划' + OR url = '/mes/rawmaterialdemandplan'; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java new file mode 100644 index 00000000..084bbb42 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java @@ -0,0 +1,74 @@ +package org.jeecg.modules.xslmes.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.jeecg.common.system.base.controller.JeecgController; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; +import org.jeecg.modules.xslmes.service.IMesXslMixingProductionPlanService; +import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanOrderOptionVO; +import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanSaveAllVO; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "密炼生产计划维护") +@RestController +@RequestMapping("/xslmes/mesXslMixingProductionPlan") +public class MesXslMixingProductionPlanController + extends JeecgController { + + private final IMesXslMixingProductionPlanService mixingProductionPlanService; + + public MesXslMixingProductionPlanController( + IMesXslMixingProductionPlanService mixingProductionPlanService) { + this.mixingProductionPlanService = mixingProductionPlanService; + } + + @Operation(summary = "密炼生产计划维护-分页列表查询") + @GetMapping("/list") + public Result> queryPageList( + MesXslMixingProductionPlan model, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = + QueryGenerator.initQueryWrapper(model, req.getParameterMap()); + queryWrapper.orderByAsc("sort_no").orderByAsc("create_time"); + IPage pageList = + mixingProductionPlanService.page(new Page<>(pageNo, pageSize), queryWrapper); + return Result.OK(pageList); + } + + @AutoLog(value = "密炼生产计划维护-整表保存") + @Operation(summary = "密炼生产计划维护-整表保存") + @RequiresPermissions("xslmes:mes_xsl_mixing_production_plan:saveAll") + @PostMapping("/saveAll") + public Result saveAll(@RequestBody MesXslMixingProductionPlanSaveAllVO req) { + mixingProductionPlanService.saveAllRows(req == null ? null : req.getRows()); + return Result.OK("保存成功"); + } + + @Operation(summary = "密炼生产计划维护-班次生产订单候选分页") + @GetMapping("/orderOptionPage") + public Result> orderOptionPage( + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "keyword", required = false) String keyword, + @RequestParam(name = "machineId", required = false) String machineId, + @RequestParam(name = "machineName", required = false) String machineName) { + return Result.OK( + mixingProductionPlanService.queryOrderOptions( + pageNo, pageSize, keyword, machineId, machineName)); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslProductionOrderController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslProductionOrderController.java index 5d7deaf1..9753dfb1 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslProductionOrderController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslProductionOrderController.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; +import java.util.List; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.jeecg.common.api.vo.Result; import org.jeecg.common.aspect.annotation.AutoLog; @@ -97,9 +98,19 @@ public class MesXslProductionOrderController @Operation(summary = "生产订单-拆分生成母胶计划") @RequiresPermissions("xslmes:mes_xsl_production_order:split") @PostMapping("/split") - public Result split(@RequestParam(name = "id", required = true) String id) { - MesXslMasterBatchPlan plan = mesXslProductionOrderService.splitToMasterBatchPlan(id); - return Result.OK("拆分成功", plan); + public Result> split(@RequestParam(name = "id", required = true) String id) { + List plans = mesXslProductionOrderService.splitToMasterBatchPlan(id); + return Result.OK("拆分成功", plans); + } + + @AutoLog(value = "生产订单-批量拆分生成计划") + @Operation(summary = "生产订单-批量拆分生成计划") + @RequiresPermissions("xslmes:mes_xsl_production_order:split") + @PostMapping("/splitBatch") + public Result splitBatch(@RequestParam(name = "ids", required = true) String ids) { + List idList = Arrays.asList(ids.split(",")); + int count = mesXslProductionOrderService.splitToMasterBatchPlanBatch(idList); + return Result.OK("批量拆分成功", count); } @RequiresPermissions("xslmes:mes_xsl_production_order:exportXls") diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialDemandPlanController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialDemandPlanController.java new file mode 100644 index 00000000..67281711 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialDemandPlanController.java @@ -0,0 +1,178 @@ +package org.jeecg.modules.xslmes.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.xslmes.entity.MesXslRawMaterialDemandPlanSummary; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +/** + * 原材料需求计划(汇总查询) + */ +@Tag(name = "原材料需求计划") +@RestController +@RequestMapping("/xslmes/mesXslRawMaterialDemandPlan") +public class MesXslRawMaterialDemandPlanController { + + @Autowired private JdbcTemplate jdbcTemplate; + + @Operation(summary = "原材料需求计划-分页列表查询") + @GetMapping("/list") + public Result> queryPageList( + MesXslRawMaterialDemandPlanSummary query, + @RequestParam(name = "groupByMachine", required = false, defaultValue = "0") Integer groupByMachine, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) { + boolean groupedByMachine = groupByMachine != null && groupByMachine == 1; + List params = new ArrayList<>(); + String groupedSql = buildGroupedSql(query, groupedByMachine, params); + + String countSql = "SELECT COUNT(1) FROM (" + groupedSql + ") t"; + Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray()); + long totalCount = total == null ? 0L : total; + + int offset = Math.max((pageNo - 1) * pageSize, 0); + String pageSql = groupedSql + buildOrderBy(groupedByMachine) + " LIMIT ? OFFSET ?"; + List pageParams = new ArrayList<>(params); + pageParams.add(pageSize); + pageParams.add(offset); + List rows = queryRows(pageSql, pageParams, groupedByMachine); + + Page page = new Page<>(pageNo, pageSize, totalCount); + page.setRecords(rows); + return Result.OK(page); + } + + @RequiresPermissions("xslmes:mes_xsl_raw_material_demand_plan:exportXls") + @RequestMapping("/exportXls") + public ModelAndView exportXls( + HttpServletRequest request, + MesXslRawMaterialDemandPlanSummary query, + @RequestParam(name = "groupByMachine", required = false, defaultValue = "0") Integer groupByMachine) { + boolean groupedByMachine = groupByMachine != null && groupByMachine == 1; + List params = new ArrayList<>(); + String groupedSql = buildGroupedSql(query, groupedByMachine, params) + buildOrderBy(groupedByMachine); + List exportList = queryRows(groupedSql, params, groupedByMachine); + + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "原材料需求计划"); + mv.addObject(NormalExcelConstants.CLASS, MesXslRawMaterialDemandPlanSummary.class); + mv.addObject( + NormalExcelConstants.PARAMS, + new ExportParams( + "原材料需求计划", + "导出人:" + (sysUser == null ? "admin" : sysUser.getRealname()), + "原材料需求计划", + ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, exportList); + String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS); + if (oConvertUtils.isNotEmpty(exportFields)) { + mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields); + } + return mv; + } + + private String buildGroupedSql( + MesXslRawMaterialDemandPlanSummary query, boolean groupedByMachine, List params) { + String machineExpr = "COALESCE(NULLIF(TRIM(t.machine_name),''), '')"; + String erpCodeExpr = "COALESCE(NULLIF(TRIM(t.erp_code),''), '')"; + String rawMaterialExpr = "COALESCE(NULLIF(TRIM(t.raw_material_name),''), '')"; + + StringBuilder sql = new StringBuilder(); + sql.append("SELECT "); + if (groupedByMachine) { + sql.append(machineExpr).append(" AS machineName, "); + } else { + sql.append("'' AS machineName, "); + } + sql.append(erpCodeExpr) + .append(" AS erpCode, ") + .append(rawMaterialExpr) + .append(" AS rawMaterialName, ") + .append("SUM(COALESCE(t.demand_weight,0)) AS demandWeight, ") + .append("SUM(COALESCE(t.standard_weight,0)) AS standardWeight, ") + .append("SUM(COALESCE(t.actual_weight,0)) AS actualWeight ") + .append("FROM mes_xsl_raw_material_demand_plan t ") + .append("WHERE (t.del_flag = 0 OR t.del_flag IS NULL) "); + + if (query != null && StringUtils.isNotBlank(query.getErpCode())) { + sql.append("AND ").append(erpCodeExpr).append(" LIKE ? "); + params.add("%" + query.getErpCode().trim() + "%"); + } + if (query != null && StringUtils.isNotBlank(query.getRawMaterialName())) { + sql.append("AND ").append(rawMaterialExpr).append(" LIKE ? "); + params.add("%" + query.getRawMaterialName().trim() + "%"); + } + if (groupedByMachine && query != null && StringUtils.isNotBlank(query.getMachineName())) { + sql.append("AND ").append(machineExpr).append(" LIKE ? "); + params.add("%" + query.getMachineName().trim() + "%"); + } + + sql.append("GROUP BY "); + if (groupedByMachine) { + sql.append(machineExpr).append(", "); + } + sql.append(erpCodeExpr).append(", ").append(rawMaterialExpr); + return sql.toString(); + } + + private List queryRows( + String sql, List params, boolean groupedByMachine) { + return jdbcTemplate.query( + sql, + rs -> { + List list = new ArrayList<>(); + int seq = 1; + while (rs.next()) { + MesXslRawMaterialDemandPlanSummary row = new MesXslRawMaterialDemandPlanSummary(); + row.setMachineName(groupedByMachine ? trim(rs.getString("machineName")) : ""); + row.setErpCode(trim(rs.getString("erpCode"))); + row.setRawMaterialName(trim(rs.getString("rawMaterialName"))); + row.setDemandWeight(rs.getBigDecimal("demandWeight")); + row.setStandardWeight(rs.getBigDecimal("standardWeight")); + row.setActualWeight(rs.getBigDecimal("actualWeight")); + row.setId(buildRowId(row, groupedByMachine, seq++)); + list.add(row); + } + return list; + }, + params.toArray()); + } + + private String buildOrderBy(boolean groupedByMachine) { + if (groupedByMachine) { + return " ORDER BY machineName, rawMaterialName, erpCode"; + } + return " ORDER BY rawMaterialName, erpCode"; + } + + private String buildRowId(MesXslRawMaterialDemandPlanSummary row, boolean groupedByMachine, int seq) { + String machine = groupedByMachine ? StringUtils.defaultString(row.getMachineName()) : "ALL"; + return machine + "_" + StringUtils.defaultString(row.getErpCode()) + "_" + seq; + } + + private String trim(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingProductionPlan.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingProductionPlan.java new file mode 100644 index 00000000..a5648996 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingProductionPlan.java @@ -0,0 +1,132 @@ +package org.jeecg.modules.xslmes.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecg.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * 密炼生产计划维护 + */ +@Data +@TableName("mes_xsl_mixing_production_plan") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "密炼生产计划维护") +public class MesXslMixingProductionPlan implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + @Excel(name = "排序号", width = 10) + @Schema(description = "排序号") + private Integer sortNo; + + @Excel(name = "机台", width = 20, dictTable = "mes_xsl_equipment_ledger", dicText = "equipment_name", dicCode = "id") + @Dict(dictTable = "mes_xsl_equipment_ledger", dicText = "equipment_name", dicCode = "id") + @Schema(description = "机台ID") + private String machineId; + + @Excel(name = "机台", width = 20) + @Schema(description = "机台名称") + private String machineName; + + @Schema(description = "早班计划ID") + private String morningPlanId; + @Schema(description = "早班计划类型 M母胶/F终胶") + private String morningPlanType; + @Excel(name = "早班生产订单", width = 20) + private String morningOrderNo; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Schema(description = "早班订单日期") + private Date morningOrderDate; + @Excel(name = "早班配方名称", width = 20) + private String morningFormulaName; + @Excel(name = "早班计划重量", width = 15) + private BigDecimal morningPlanWeight; + @Excel(name = "早班计划车数", width = 12) + private Integer morningPlannedCarCount; + @Excel(name = "早班已排产车数", width = 12) + private Integer morningScheduledCarCount; + @Excel(name = "早班完成车数", width = 12) + private Integer morningFinishedCarCount; + @Excel(name = "早班计划", width = 12) + private Integer morningPlanCount; + @Excel(name = "早班备注", width = 20) + private String morningRemark; + + @Schema(description = "中班计划ID") + private String noonPlanId; + @Schema(description = "中班计划类型 M母胶/F终胶") + private String noonPlanType; + @Excel(name = "中班生产订单", width = 20) + private String noonOrderNo; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Schema(description = "中班订单日期") + private Date noonOrderDate; + @Excel(name = "中班配方名称", width = 20) + private String noonFormulaName; + @Excel(name = "中班计划重量", width = 15) + private BigDecimal noonPlanWeight; + @Excel(name = "中班计划车数", width = 12) + private Integer noonPlannedCarCount; + @Excel(name = "中班已排产车数", width = 12) + private Integer noonScheduledCarCount; + @Excel(name = "中班完成车数", width = 12) + private Integer noonFinishedCarCount; + @Excel(name = "中班计划", width = 12) + private Integer noonPlanCount; + @Excel(name = "中班备注", width = 20) + private String noonRemark; + + @Schema(description = "晚班计划ID") + private String nightPlanId; + @Schema(description = "晚班计划类型 M母胶/F终胶") + private String nightPlanType; + @Excel(name = "晚班生产订单", width = 20) + private String nightOrderNo; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Schema(description = "晚班订单日期") + private Date nightOrderDate; + @Excel(name = "晚班配方名称", width = 20) + private String nightFormulaName; + @Excel(name = "晚班计划重量", width = 15) + private BigDecimal nightPlanWeight; + @Excel(name = "晚班计划车数", width = 12) + private Integer nightPlannedCarCount; + @Excel(name = "晚班已排产车数", width = 12) + private Integer nightScheduledCarCount; + @Excel(name = "晚班完成车数", width = 12) + private Integer nightFinishedCarCount; + @Excel(name = "晚班计划", width = 12) + private Integer nightPlanCount; + @Excel(name = "晚班备注", width = 20) + private String nightRemark; + + private Integer tenantId; + private String sysOrgCode; + private String createBy; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + private String updateBy; + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + private Integer delFlag; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialDemandPlanSummary.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialDemandPlanSummary.java new file mode 100644 index 00000000..ba7176bc --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialDemandPlanSummary.java @@ -0,0 +1,44 @@ +package org.jeecg.modules.xslmes.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * 原材料需求计划汇总(按原材料或按机台+原材料) + */ +@Data +@Schema(description = "原材料需求计划汇总") +public class MesXslRawMaterialDemandPlanSummary implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主键") + private String id; + + @Excel(name = "机台", width = 20) + @Schema(description = "机台") + private String machineName; + + @Excel(name = "ERP编号", width = 18) + @Schema(description = "ERP编号") + private String erpCode; + + @Excel(name = "原材料名称", width = 24) + @Schema(description = "原材料名称") + private String rawMaterialName; + + @Excel(name = "需求重量", width = 15) + @Schema(description = "需求重量") + private BigDecimal demandWeight; + + @Excel(name = "标准重量", width = 15) + @Schema(description = "标准重量") + private BigDecimal standardWeight; + + @Excel(name = "实际重量", width = 15) + @Schema(description = "实际重量") + private BigDecimal actualWeight; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixingProductionPlanMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixingProductionPlanMapper.java new file mode 100644 index 00000000..33415a7a --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixingProductionPlanMapper.java @@ -0,0 +1,6 @@ +package org.jeecg.modules.xslmes.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; + +public interface MesXslMixingProductionPlanMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMasterBatchPlanService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMasterBatchPlanService.java index b56eb5af..9161b474 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMasterBatchPlanService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMasterBatchPlanService.java @@ -1,10 +1,13 @@ package org.jeecg.modules.xslmes.service; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan; import org.jeecg.modules.xslmes.entity.MesXslProductionOrder; public interface IMesXslMasterBatchPlanService extends IService { + List generateBatchFromProductionOrder(MesXslProductionOrder productionOrder); + MesXslMasterBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixingProductionPlanService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixingProductionPlanService.java new file mode 100644 index 00000000..0c27d63b --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixingProductionPlanService.java @@ -0,0 +1,14 @@ +package org.jeecg.modules.xslmes.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; +import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanOrderOptionVO; + +public interface IMesXslMixingProductionPlanService extends IService { + void saveAllRows(List rows); + + IPage queryOrderOptions( + Integer pageNo, Integer pageSize, String keyword, String machineId, String machineName); +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslProductionOrderService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslProductionOrderService.java index fd86e614..5a86a4b6 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslProductionOrderService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslProductionOrderService.java @@ -1,10 +1,13 @@ package org.jeecg.modules.xslmes.service; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan; import org.jeecg.modules.xslmes.entity.MesXslProductionOrder; public interface IMesXslProductionOrderService extends IService { - MesXslMasterBatchPlan splitToMasterBatchPlan(String id); + List splitToMasterBatchPlan(String id); + + int splitToMasterBatchPlanBatch(List ids); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFinalBatchPlanServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFinalBatchPlanServiceImpl.java index 87cc43ec..f0e8f090 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFinalBatchPlanServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFinalBatchPlanServiceImpl.java @@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.modules.mes.material.entity.MesMaterial; @@ -33,7 +37,7 @@ public class MesXslFinalBatchPlanServiceImpl return exists; } - MesMaterial finalMaterial = resolveFinalMaterial(productionOrder.getMaterialCode()); + MesMaterial finalMaterial = resolveFinalMaterial(productionOrder); if (finalMaterial == null) { throw new JeecgBootException("未找到对应终胶物料,请确认MES物料编码"); } @@ -60,16 +64,67 @@ public class MesXslFinalBatchPlanServiceImpl return plan; } - private MesMaterial resolveFinalMaterial(String mesMaterialCode) { - if (StringUtils.isBlank(mesMaterialCode)) { + private MesMaterial resolveFinalMaterial(MesXslProductionOrder productionOrder) { + for (String seedCode : resolveSplitSeedCodes(productionOrder)) { + for (String candidateCode : buildFinalCandidates(seedCode)) { + MesMaterial matched = resolveMaterialByBaseCode(candidateCode); + if (matched != null) { + MesMaterial resolved = new MesMaterial(); + resolved.setMaterialCode(candidateCode); + resolved.setMaterialName(StringUtils.defaultIfBlank(matched.getMaterialName(), candidateCode)); + return resolved; + } + } + } + return null; + } + + private MesMaterial resolveMaterialByBaseCode(String baseCode) { + if (StringUtils.isBlank(baseCode)) { return null; } + MesMaterial exact = + mesMaterialMapper.selectOne( + new LambdaQueryWrapper() + .eq(MesMaterial::getMaterialCode, baseCode) + .last("LIMIT 1")); + if (exact != null) { + return exact; + } return mesMaterialMapper.selectOne( new LambdaQueryWrapper() - .eq(MesMaterial::getMaterialCode, mesMaterialCode.trim()) + .likeRight(MesMaterial::getMaterialCode, baseCode) .last("LIMIT 1")); } + private List resolveSplitSeedCodes(MesXslProductionOrder productionOrder) { + Set seeds = new LinkedHashSet<>(); + if (productionOrder != null) { + if (StringUtils.isNotBlank(productionOrder.getMesMaterialName())) { + seeds.add(productionOrder.getMesMaterialName().trim()); + } + if (StringUtils.isNotBlank(productionOrder.getMaterialCode())) { + seeds.add(productionOrder.getMaterialCode().trim()); + } + } + return new ArrayList<>(seeds); + } + + private List buildFinalCandidates(String code) { + List candidates = new ArrayList<>(2); + if (StringUtils.isBlank(code)) { + return candidates; + } + String trimmed = code.trim(); + if (trimmed.length() > 0 && (trimmed.startsWith("F") || trimmed.startsWith("f"))) { + candidates.add(trimmed); + return candidates; + } + candidates.add("F" + trimmed); + candidates.add(trimmed); + return candidates; + } + private String buildOrderSerialNo(MesXslProductionOrder productionOrder) { String orderNo = StringUtils.defaultIfBlank(productionOrder.getProductionOrderNo(), productionOrder.getId()); return orderNo + "-F-" + System.currentTimeMillis(); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMasterBatchPlanServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMasterBatchPlanServiceImpl.java index 4f83ded0..0d5d9d48 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMasterBatchPlanServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMasterBatchPlanServiceImpl.java @@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.modules.mes.material.entity.MesMaterial; @@ -25,28 +27,55 @@ public class MesXslMasterBatchPlanServiceImpl @Autowired private MesMaterialMapper mesMaterialMapper; @Override - public MesXslMasterBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder) { + public List generateBatchFromProductionOrder(MesXslProductionOrder productionOrder) { if (productionOrder == null || StringUtils.isBlank(productionOrder.getId())) { throw new JeecgBootException("生产订单不存在,无法拆分"); } - MesMaterial motherMaterial = resolveMotherMaterial(productionOrder.getMaterialCode()); - if (motherMaterial == null) { - throw new JeecgBootException("未找到对应母胶物料(优先B1,其次B2)"); + int totalSegments = normalizeSegmentCount(productionOrder.getProcessSegmentCount()); + int motherSegments = Math.max(totalSegments - 1, 0); + if (motherSegments <= 0) { + return new ArrayList<>(); } - - MesXslMasterBatchPlan exists = - this.getOne(new LambdaQueryWrapper().eq(MesXslMasterBatchPlan::getSourceOrderId, productionOrder.getId())); - if (exists != null) { - return exists; + List existingPlans = + this.list( + new LambdaQueryWrapper() + .eq(MesXslMasterBatchPlan::getSourceOrderId, productionOrder.getId()) + .orderByAsc(MesXslMasterBatchPlan::getCreateTime) + .orderByAsc(MesXslMasterBatchPlan::getId)); + List result = new ArrayList<>(); + for (int stageIndex = 1; stageIndex <= motherSegments; stageIndex++) { + MesMaterial motherMaterial = resolveMotherMaterialByStage(productionOrder, stageIndex); + if (motherMaterial == null) { + throw new JeecgBootException("未找到对应母胶物料(第" + stageIndex + "段,编码前缀B" + stageIndex + ")"); + } + MesXslMasterBatchPlan matched = findExistingByMaterialCode(existingPlans, motherMaterial.getMaterialCode()); + if (matched != null) { + result.add(matched); + continue; + } + MesXslMasterBatchPlan plan = buildMotherPlan(productionOrder, motherMaterial, stageIndex); + this.save(plan); + result.add(plan); + existingPlans.add(plan); } + return result; + } + @Override + public MesXslMasterBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder) { + List plans = generateBatchFromProductionOrder(productionOrder); + return plans.isEmpty() ? null : plans.get(0); + } + + private MesXslMasterBatchPlan buildMotherPlan( + MesXslProductionOrder productionOrder, MesMaterial motherMaterial, int stageIndex) { BigDecimal planWeight = productionOrder.getPlanQty() == null ? BigDecimal.ZERO : productionOrder.getPlanQty(); BigDecimal perCarWeight = BigDecimal.ZERO; int planCarCount = calcPlanCarCount(planWeight, perCarWeight); MesXslMasterBatchPlan plan = new MesXslMasterBatchPlan(); plan.setSourceOrderId(productionOrder.getId()); - plan.setOrderSerialNo(buildOrderSerialNo(productionOrder)); + plan.setOrderSerialNo(buildOrderSerialNo(productionOrder, stageIndex)); plan.setOrderNo(productionOrder.getProductionOrderNo()); plan.setProductionSegmentCount(productionOrder.getProcessSegmentCount()); plan.setOrderDate(productionOrder.getOrderDate()); @@ -58,45 +87,93 @@ public class MesXslMasterBatchPlanServiceImpl plan.setScheduledCarCount(0); plan.setFinishedCarCount(0); plan.setStatus(0); - this.save(plan); return plan; } - private MesMaterial resolveMotherMaterial(String mesMaterialCode) { - if (StringUtils.isBlank(mesMaterialCode)) { + private MesXslMasterBatchPlan findExistingByMaterialCode( + List existingPlans, String materialCode) { + if (StringUtils.isBlank(materialCode) || existingPlans == null || existingPlans.isEmpty()) { return null; } - String code = mesMaterialCode.trim(); - List candidates = buildMotherCandidates(code); - for (String c : candidates) { - MesMaterial found = - mesMaterialMapper.selectOne( - new LambdaQueryWrapper() - .eq(MesMaterial::getMaterialCode, c) - .last("LIMIT 1")); - if (found != null) { - return found; + for (MesXslMasterBatchPlan plan : existingPlans) { + if (plan != null && materialCode.equalsIgnoreCase(StringUtils.trimToEmpty(plan.getMaterialCode()))) { + return plan; } } return null; } - private List buildMotherCandidates(String code) { - List list = new ArrayList<>(2); + private MesMaterial resolveMotherMaterialByStage(MesXslProductionOrder productionOrder, int stageIndex) { + for (String seedCode : resolveSplitSeedCodes(productionOrder)) { + List candidates = buildMotherCandidates(seedCode, stageIndex); + for (String candidateCode : candidates) { + MesMaterial matched = resolveMaterialByBaseCode(candidateCode); + if (matched != null) { + MesMaterial resolved = new MesMaterial(); + resolved.setMaterialCode(candidateCode); + resolved.setMaterialName(StringUtils.defaultIfBlank(matched.getMaterialName(), candidateCode)); + return resolved; + } + } + } + return null; + } + + private MesMaterial resolveMaterialByBaseCode(String baseCode) { + if (StringUtils.isBlank(baseCode)) { + return null; + } + MesMaterial exact = + mesMaterialMapper.selectOne( + new LambdaQueryWrapper() + .eq(MesMaterial::getMaterialCode, baseCode) + .last("LIMIT 1")); + if (exact != null) { + return exact; + } + return mesMaterialMapper.selectOne( + new LambdaQueryWrapper() + .likeRight(MesMaterial::getMaterialCode, baseCode) + .last("LIMIT 1")); + } + + private List resolveSplitSeedCodes(MesXslProductionOrder productionOrder) { + Set seeds = new LinkedHashSet<>(); + if (productionOrder != null) { + if (StringUtils.isNotBlank(productionOrder.getMesMaterialName())) { + seeds.add(productionOrder.getMesMaterialName().trim()); + } + if (StringUtils.isNotBlank(productionOrder.getMaterialCode())) { + seeds.add(productionOrder.getMaterialCode().trim()); + } + } + return new ArrayList<>(seeds); + } + + private List buildMotherCandidates(String code, int stageIndex) { + List list = new ArrayList<>(1); + if (StringUtils.isBlank(code) || stageIndex <= 0) { + return list; + } if (code.length() > 1 && (code.startsWith("F") || code.startsWith("f"))) { String suffix = code.substring(1); - list.add("B1" + suffix); - list.add("B2" + suffix); + list.add("B" + stageIndex + suffix); } else { - list.add("B1" + code); - list.add("B2" + code); + list.add("B" + stageIndex + code); } return list; } - private String buildOrderSerialNo(MesXslProductionOrder productionOrder) { + private int normalizeSegmentCount(Integer segmentCount) { + if (segmentCount == null || segmentCount <= 0) { + return 1; + } + return segmentCount; + } + + private String buildOrderSerialNo(MesXslProductionOrder productionOrder, int stageIndex) { String orderNo = StringUtils.defaultIfBlank(productionOrder.getProductionOrderNo(), productionOrder.getId()); - return orderNo + "-" + System.currentTimeMillis(); + return orderNo + "-B" + stageIndex + "-" + System.currentTimeMillis(); } private int calcPlanCarCount(BigDecimal planWeight, BigDecimal perCarWeight) { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingProductionPlanServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingProductionPlanServiceImpl.java new file mode 100644 index 00000000..8718c239 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingProductionPlanServiceImpl.java @@ -0,0 +1,416 @@ +package org.jeecg.modules.xslmes.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.common.constant.CommonConstant; +import org.jeecg.modules.xslmes.entity.MesXslEquipmentLedger; +import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan; +import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan; +import org.jeecg.modules.xslmes.entity.MesXslMixingSpec; +import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; +import org.jeecg.modules.xslmes.mapper.MesXslEquipmentLedgerMapper; +import org.jeecg.modules.xslmes.mapper.MesXslFinalBatchPlanMapper; +import org.jeecg.modules.xslmes.mapper.MesXslMasterBatchPlanMapper; +import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecMapper; +import org.jeecg.modules.xslmes.mapper.MesXslMixingProductionPlanMapper; +import org.jeecg.modules.xslmes.service.IMesXslMixingProductionPlanService; +import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanOrderOptionVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +@Service +public class MesXslMixingProductionPlanServiceImpl + extends ServiceImpl + implements IMesXslMixingProductionPlanService { + + private final MesXslEquipmentLedgerMapper equipmentLedgerMapper; + private final MesXslMasterBatchPlanMapper masterBatchPlanMapper; + private final MesXslFinalBatchPlanMapper finalBatchPlanMapper; + private final MesXslMixingSpecMapper mixingSpecMapper; + + public MesXslMixingProductionPlanServiceImpl( + MesXslEquipmentLedgerMapper equipmentLedgerMapper, + MesXslMasterBatchPlanMapper masterBatchPlanMapper, + MesXslFinalBatchPlanMapper finalBatchPlanMapper, + MesXslMixingSpecMapper mixingSpecMapper) { + this.equipmentLedgerMapper = equipmentLedgerMapper; + this.masterBatchPlanMapper = masterBatchPlanMapper; + this.finalBatchPlanMapper = finalBatchPlanMapper; + this.mixingSpecMapper = mixingSpecMapper; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveAllRows(List rows) { + this.remove(new LambdaQueryWrapper<>()); + if (CollectionUtils.isEmpty(rows)) { + return; + } + Map machineNameCache = new HashMap<>(); + List saveList = new ArrayList<>(); + int sort = 1; + for (MesXslMixingProductionPlan row : rows) { + if (row == null || !hasBusinessData(row)) { + continue; + } + normalizeRow(row, machineNameCache); + row.setSortNo(sort++); + saveList.add(row); + } + if (!saveList.isEmpty()) { + this.saveBatch(saveList, 200); + } + } + + private boolean hasBusinessData(MesXslMixingProductionPlan row) { + return StringUtils.isNotBlank(row.getMachineId()) + || StringUtils.isNotBlank(row.getMorningPlanId()) + || StringUtils.isNotBlank(row.getNoonPlanId()) + || StringUtils.isNotBlank(row.getNightPlanId()) + || StringUtils.isNotBlank(row.getMorningRemark()) + || StringUtils.isNotBlank(row.getNoonRemark()) + || StringUtils.isNotBlank(row.getNightRemark()) + || row.getMorningPlanCount() != null + || row.getNoonPlanCount() != null + || row.getNightPlanCount() != null; + } + + private void normalizeRow( + MesXslMixingProductionPlan row, Map machineNameCache) { + row.setId(null); + row.setMachineId(StringUtils.trimToNull(row.getMachineId())); + row.setMachineName(resolveMachineName(row.getMachineId(), machineNameCache)); + fillShiftFromPlan(row, "morning"); + fillShiftFromPlan(row, "noon"); + fillShiftFromPlan(row, "night"); + Date now = new Date(); + if (row.getCreateTime() == null) { + row.setCreateTime(now); + } + row.setUpdateTime(now); + if (row.getDelFlag() == null) { + row.setDelFlag(CommonConstant.DEL_FLAG_0); + } + } + + private String resolveMachineName(String machineId, Map cache) { + if (StringUtils.isBlank(machineId)) { + return null; + } + return cache.computeIfAbsent( + machineId, + id -> { + MesXslEquipmentLedger ledger = equipmentLedgerMapper.selectById(id); + return ledger == null ? null : ledger.getEquipmentName(); + }); + } + + private void fillShiftFromPlan(MesXslMixingProductionPlan row, String shift) { + String planId = getPlanId(row, shift); + String planType = StringUtils.upperCase(StringUtils.trimToEmpty(getPlanType(row, shift))); + if (StringUtils.isBlank(planId)) { + clearShiftPlanFields(row, shift); + return; + } + if ("M".equals(planType)) { + MesXslMasterBatchPlan plan = masterBatchPlanMapper.selectById(planId); + if (plan == null) { + clearShiftPlanFields(row, shift); + return; + } + setShiftFromMasterPlan(row, shift, plan); + return; + } + MesXslFinalBatchPlan finalPlan = finalBatchPlanMapper.selectById(planId); + if (finalPlan == null) { + clearShiftPlanFields(row, shift); + return; + } + setShiftFromFinalPlan(row, shift, finalPlan); + } + + private String getPlanId(MesXslMixingProductionPlan row, String shift) { + return switch (shift) { + case "morning" -> StringUtils.trimToNull(row.getMorningPlanId()); + case "noon" -> StringUtils.trimToNull(row.getNoonPlanId()); + default -> StringUtils.trimToNull(row.getNightPlanId()); + }; + } + + private String getPlanType(MesXslMixingProductionPlan row, String shift) { + return switch (shift) { + case "morning" -> row.getMorningPlanType(); + case "noon" -> row.getNoonPlanType(); + default -> row.getNightPlanType(); + }; + } + + private void setShiftFromMasterPlan( + MesXslMixingProductionPlan row, String shift, MesXslMasterBatchPlan plan) { + setShiftPlanType(row, shift, "M"); + setShiftOrderNo(row, shift, plan.getOrderNo()); + setShiftOrderDate(row, shift, plan.getOrderDate()); + setShiftFormulaName( + row, + shift, + resolveFormulaNameByMachineAndMaterial( + row.getMachineId(), row.getMachineName(), plan.getMaterialCode(), plan.getMesMaterialName())); + setShiftPlanWeight(row, shift, plan.getPlanWeight()); + setShiftPlannedCarCount(row, shift, plan.getPlannedCarCount()); + setShiftScheduledCarCount(row, shift, plan.getScheduledCarCount()); + setShiftFinishedCarCount(row, shift, plan.getFinishedCarCount()); + } + + private void setShiftFromFinalPlan( + MesXslMixingProductionPlan row, String shift, MesXslFinalBatchPlan plan) { + setShiftPlanType(row, shift, "F"); + setShiftOrderNo(row, shift, plan.getOrderNo()); + setShiftOrderDate(row, shift, plan.getOrderDate()); + setShiftFormulaName( + row, + shift, + resolveFormulaNameByMachineAndMaterial( + row.getMachineId(), row.getMachineName(), plan.getMaterialCode(), plan.getMesMaterialName())); + setShiftPlanWeight(row, shift, plan.getPlanWeight()); + setShiftPlannedCarCount(row, shift, plan.getPlannedCarCount()); + setShiftScheduledCarCount(row, shift, plan.getScheduledCarCount()); + setShiftFinishedCarCount(row, shift, plan.getFinishedCarCount()); + } + + private void clearShiftPlanFields(MesXslMixingProductionPlan row, String shift) { + setShiftPlanType(row, shift, null); + setShiftOrderNo(row, shift, null); + setShiftOrderDate(row, shift, null); + setShiftFormulaName(row, shift, null); + setShiftPlanWeight(row, shift, null); + setShiftPlannedCarCount(row, shift, null); + setShiftScheduledCarCount(row, shift, null); + setShiftFinishedCarCount(row, shift, null); + } + + private void setShiftPlanType(MesXslMixingProductionPlan row, String shift, String value) { + switch (shift) { + case "morning" -> row.setMorningPlanType(value); + case "noon" -> row.setNoonPlanType(value); + default -> row.setNightPlanType(value); + } + } + + private void setShiftOrderNo(MesXslMixingProductionPlan row, String shift, String value) { + switch (shift) { + case "morning" -> row.setMorningOrderNo(value); + case "noon" -> row.setNoonOrderNo(value); + default -> row.setNightOrderNo(value); + } + } + + private void setShiftOrderDate(MesXslMixingProductionPlan row, String shift, Date value) { + switch (shift) { + case "morning" -> row.setMorningOrderDate(value); + case "noon" -> row.setNoonOrderDate(value); + default -> row.setNightOrderDate(value); + } + } + + private void setShiftFormulaName(MesXslMixingProductionPlan row, String shift, String value) { + switch (shift) { + case "morning" -> row.setMorningFormulaName(value); + case "noon" -> row.setNoonFormulaName(value); + default -> row.setNightFormulaName(value); + } + } + + private void setShiftPlanWeight( + MesXslMixingProductionPlan row, String shift, java.math.BigDecimal value) { + switch (shift) { + case "morning" -> row.setMorningPlanWeight(value); + case "noon" -> row.setNoonPlanWeight(value); + default -> row.setNightPlanWeight(value); + } + } + + private void setShiftPlannedCarCount(MesXslMixingProductionPlan row, String shift, Integer value) { + switch (shift) { + case "morning" -> row.setMorningPlannedCarCount(value); + case "noon" -> row.setNoonPlannedCarCount(value); + default -> row.setNightPlannedCarCount(value); + } + } + + private void setShiftScheduledCarCount(MesXslMixingProductionPlan row, String shift, Integer value) { + switch (shift) { + case "morning" -> row.setMorningScheduledCarCount(value); + case "noon" -> row.setNoonScheduledCarCount(value); + default -> row.setNightScheduledCarCount(value); + } + } + + private void setShiftFinishedCarCount(MesXslMixingProductionPlan row, String shift, Integer value) { + switch (shift) { + case "morning" -> row.setMorningFinishedCarCount(value); + case "noon" -> row.setNoonFinishedCarCount(value); + default -> row.setNightFinishedCarCount(value); + } + } + + @Override + public IPage queryOrderOptions( + Integer pageNo, Integer pageSize, String keyword, String machineId, String machineName) { + String machineIdTrimmed = StringUtils.trimToNull(machineId); + String machineNameTrimmed = StringUtils.trimToNull(machineName); + List all = new ArrayList<>(); + LambdaQueryWrapper masterWrapper = + new LambdaQueryWrapper() + .and(w -> w.eq(MesXslMasterBatchPlan::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslMasterBatchPlan::getDelFlag)); + LambdaQueryWrapper finalWrapper = + new LambdaQueryWrapper() + .and(w -> w.eq(MesXslFinalBatchPlan::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslFinalBatchPlan::getDelFlag)); + if (StringUtils.isNotBlank(keyword)) { + masterWrapper.and( + w -> + w.like(MesXslMasterBatchPlan::getOrderNo, keyword) + .or() + .like(MesXslMasterBatchPlan::getMesMaterialName, keyword)); + finalWrapper.and( + w -> + w.like(MesXslFinalBatchPlan::getOrderNo, keyword) + .or() + .like(MesXslFinalBatchPlan::getMesMaterialName, keyword)); + } + List masters = masterBatchPlanMapper.selectList(masterWrapper); + for (MesXslMasterBatchPlan m : masters) { + MesXslMixingProductionPlanOrderOptionVO vo = new MesXslMixingProductionPlanOrderOptionVO(); + vo.setPlanId(m.getId()); + vo.setPlanType("M"); + vo.setOrderNo(m.getOrderNo()); + vo.setOrderDate(m.getOrderDate()); + vo.setFormulaName( + resolveFormulaNameByMachineAndMaterial( + machineIdTrimmed, machineNameTrimmed, m.getMaterialCode(), m.getMesMaterialName())); + vo.setPlanWeight(m.getPlanWeight()); + vo.setPlannedCarCount(m.getPlannedCarCount()); + vo.setScheduledCarCount(m.getScheduledCarCount()); + vo.setFinishedCarCount(m.getFinishedCarCount()); + all.add(vo); + } + List finals = finalBatchPlanMapper.selectList(finalWrapper); + for (MesXslFinalBatchPlan f : finals) { + MesXslMixingProductionPlanOrderOptionVO vo = new MesXslMixingProductionPlanOrderOptionVO(); + vo.setPlanId(f.getId()); + vo.setPlanType("F"); + vo.setOrderNo(f.getOrderNo()); + vo.setOrderDate(f.getOrderDate()); + vo.setFormulaName( + resolveFormulaNameByMachineAndMaterial( + machineIdTrimmed, machineNameTrimmed, f.getMaterialCode(), f.getMesMaterialName())); + vo.setPlanWeight(f.getPlanWeight()); + vo.setPlannedCarCount(f.getPlannedCarCount()); + vo.setScheduledCarCount(f.getScheduledCarCount()); + vo.setFinishedCarCount(f.getFinishedCarCount()); + all.add(vo); + } + all.sort( + Comparator.comparing( + MesXslMixingProductionPlanOrderOptionVO::getOrderDate, + Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing( + MesXslMixingProductionPlanOrderOptionVO::getOrderNo, + Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing( + MesXslMixingProductionPlanOrderOptionVO::getPlanId, + Comparator.nullsLast(Comparator.reverseOrder()))); + long current = pageNo == null || pageNo < 1 ? 1 : pageNo; + long size = pageSize == null || pageSize < 1 ? 20 : pageSize; + int from = (int) ((current - 1) * size); + int to = (int) Math.min(from + size, all.size()); + List pageRecords = + from >= all.size() ? Collections.emptyList() : all.subList(from, to); + Page page = new Page<>(current, size); + page.setTotal(all.size()); + page.setRecords( + pageRecords.stream().filter(Objects::nonNull).collect(Collectors.toList())); + return page; + } + + private String resolveFormulaNameByMachineAndMaterial( + String machineId, String machineName, String materialCode, String fallbackName) { + String code = StringUtils.trimToEmpty(materialCode); + if (StringUtils.isBlank(code)) { + return StringUtils.defaultString(fallbackName); + } + String targetSpec = buildTargetSpecName(code); + LambdaQueryWrapper exactWrapper = + new LambdaQueryWrapper() + .eq(MesXslMixingSpec::getSpecName, targetSpec) + .and( + w -> + w.eq(MesXslMixingSpec::getDelFlag, CommonConstant.DEL_FLAG_0) + .or() + .isNull(MesXslMixingSpec::getDelFlag)) + .last("LIMIT 1"); + appendMachineCondition(exactWrapper, machineId, machineName); + MesXslMixingSpec exact = mixingSpecMapper.selectOne(exactWrapper); + if (exact != null && StringUtils.isNotBlank(exact.getSpecName())) { + return exact.getSpecName().trim(); + } + + LambdaQueryWrapper prefixWrapper = + new LambdaQueryWrapper() + .likeRight(MesXslMixingSpec::getSpecName, code) + .and( + w -> + w.eq(MesXslMixingSpec::getDelFlag, CommonConstant.DEL_FLAG_0) + .or() + .isNull(MesXslMixingSpec::getDelFlag)) + .orderByAsc(MesXslMixingSpec::getSpecName) + .last("LIMIT 1"); + appendMachineCondition(prefixWrapper, machineId, machineName); + MesXslMixingSpec prefix = mixingSpecMapper.selectOne(prefixWrapper); + if (prefix != null && StringUtils.isNotBlank(prefix.getSpecName())) { + return prefix.getSpecName().trim(); + } + return StringUtils.defaultIfBlank(fallbackName, targetSpec); + } + + private String buildTargetSpecName(String materialCode) { + String code = StringUtils.trimToEmpty(materialCode).toUpperCase(); + if (code.matches(".*SA\\d{2}$")) { + return code; + } + return code + "SA01"; + } + + private void appendMachineCondition( + LambdaQueryWrapper wrapper, String machineId, String machineName) { + String machineIdTrimmed = StringUtils.trimToNull(machineId); + String machineNameTrimmed = StringUtils.trimToNull(machineName); + if (machineIdTrimmed != null && machineNameTrimmed != null) { + wrapper.and( + w -> + w.eq(MesXslMixingSpec::getMachineId, machineIdTrimmed) + .or() + .eq(MesXslMixingSpec::getMachineName, machineNameTrimmed)); + return; + } + if (machineIdTrimmed != null) { + wrapper.eq(MesXslMixingSpec::getMachineId, machineIdTrimmed); + return; + } + if (machineNameTrimmed != null) { + wrapper.eq(MesXslMixingSpec::getMachineName, machineNameTrimmed); + } + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslProductionOrderServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslProductionOrderServiceImpl.java index 9ed61591..17499181 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslProductionOrderServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslProductionOrderServiceImpl.java @@ -1,11 +1,17 @@ package org.jeecg.modules.xslmes.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.ArrayList; +import java.util.List; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan; +import org.jeecg.modules.xslmes.entity.MesXslMixingSpec; import org.jeecg.modules.xslmes.service.IMesXslFinalBatchPlanService; import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan; import org.jeecg.modules.xslmes.entity.MesXslProductionOrder; +import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecMapper; import org.jeecg.modules.xslmes.mapper.MesXslProductionOrderMapper; import org.jeecg.modules.xslmes.service.IMesXslMasterBatchPlanService; import org.jeecg.modules.xslmes.service.IMesXslProductionOrderService; @@ -20,10 +26,11 @@ public class MesXslProductionOrderServiceImpl @Autowired private IMesXslMasterBatchPlanService masterBatchPlanService; @Autowired private IMesXslFinalBatchPlanService finalBatchPlanService; + @Autowired private MesXslMixingSpecMapper mixingSpecMapper; @Override @Transactional(rollbackFor = Exception.class) - public MesXslMasterBatchPlan splitToMasterBatchPlan(String id) { + public List splitToMasterBatchPlan(String id) { if (StringUtils.isBlank(id)) { throw new JeecgBootException("生产订单ID不能为空"); } @@ -34,12 +41,67 @@ public class MesXslProductionOrderServiceImpl if (order.getSplitStatus() != null && order.getSplitStatus() == 1) { throw new JeecgBootException("该生产订单已拆分,无需重复操作"); } - MesXslMasterBatchPlan plan = masterBatchPlanService.generateFromProductionOrder(order); - finalBatchPlanService.generateFromProductionOrder(order); + List plans = masterBatchPlanService.generateBatchFromProductionOrder(order); + MesXslFinalBatchPlan finalPlan = finalBatchPlanService.generateFromProductionOrder(order); + validateMixingSpecExists(plans, finalPlan); MesXslProductionOrder update = new MesXslProductionOrder(); update.setId(order.getId()); update.setSplitStatus(1); this.updateById(update); - return plan; + return plans; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int splitToMasterBatchPlanBatch(List ids) { + if (ids == null || ids.isEmpty()) { + throw new JeecgBootException("请至少选择一条生产订单"); + } + int successCount = 0; + for (String id : ids) { + if (StringUtils.isBlank(id)) { + continue; + } + splitToMasterBatchPlan(id.trim()); + successCount++; + } + return successCount; + } + + private void validateMixingSpecExists( + List masterPlans, MesXslFinalBatchPlan finalPlan) { + List requiredSpecs = new ArrayList<>(); + if (masterPlans != null) { + for (MesXslMasterBatchPlan plan : masterPlans) { + if (plan != null && StringUtils.isNotBlank(plan.getMaterialCode())) { + requiredSpecs.add(plan.getMaterialCode().trim() + "SA01"); + } + } + } + if (finalPlan != null && StringUtils.isNotBlank(finalPlan.getMaterialCode())) { + requiredSpecs.add(finalPlan.getMaterialCode().trim() + "SA01"); + } + if (requiredSpecs.isEmpty()) { + return; + } + List missingSpecs = new ArrayList<>(); + for (String specName : requiredSpecs) { + MesXslMixingSpec found = + mixingSpecMapper.selectOne( + new LambdaQueryWrapper() + .eq(MesXslMixingSpec::getSpecName, specName) + .and( + w -> + w.eq(MesXslMixingSpec::getDelFlag, 0) + .or() + .isNull(MesXslMixingSpec::getDelFlag)) + .last("LIMIT 1")); + if (found == null) { + missingSpecs.add(specName); + } + } + if (!missingSpecs.isEmpty()) { + throw new JeecgBootException("对应物料的混炼示方不存在,请先生成混炼示方!"); + } } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslMixingProductionPlanOrderOptionVO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslMixingProductionPlanOrderOptionVO.java new file mode 100644 index 00000000..8a47a8b2 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslMixingProductionPlanOrderOptionVO.java @@ -0,0 +1,41 @@ +package org.jeecg.modules.xslmes.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Date; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +@Data +@Schema(description = "密炼生产计划-班次生产订单候选") +public class MesXslMixingProductionPlanOrderOptionVO { + @Schema(description = "计划ID") + private String planId; + + @Schema(description = "计划类型 M母胶/F终胶") + private String planType; + + @Schema(description = "订单编号") + private String orderNo; + + @Schema(description = "订单日期") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date orderDate; + + @Schema(description = "胶料名称") + private String formulaName; + + @Schema(description = "计划重量") + private BigDecimal planWeight; + + @Schema(description = "计划车数") + private Integer plannedCarCount; + + @Schema(description = "已排产车数") + private Integer scheduledCarCount; + + @Schema(description = "完成车数") + private Integer finishedCarCount; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslMixingProductionPlanSaveAllVO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslMixingProductionPlanSaveAllVO.java new file mode 100644 index 00000000..ed21a046 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslMixingProductionPlanSaveAllVO.java @@ -0,0 +1,10 @@ +package org.jeecg.modules.xslmes.vo; + +import java.util.List; +import lombok.Data; +import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; + +@Data +public class MesXslMixingProductionPlanSaveAllVO { + private List rows; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__mes_xsl_mixing_production_plan.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__mes_xsl_mixing_production_plan.sql new file mode 100644 index 00000000..3509bf82 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__mes_xsl_mixing_production_plan.sql @@ -0,0 +1,57 @@ +-- 密炼生产计划维护(早/中/晚班) +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `mes_xsl_mixing_production_plan` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `sort_no` int DEFAULT NULL COMMENT '排序号', + `machine_id` varchar(32) DEFAULT NULL COMMENT '机台ID(mes_xsl_equipment_ledger.id)', + `machine_name` varchar(128) DEFAULT NULL COMMENT '机台名称冗余', + + `morning_plan_id` varchar(32) DEFAULT NULL COMMENT '早班计划ID(母胶/终胶计划)', + `morning_plan_type` varchar(2) DEFAULT NULL COMMENT '早班计划类型:M母胶/F终胶', + `morning_order_no` varchar(64) DEFAULT NULL COMMENT '早班生产订单', + `morning_order_date` date DEFAULT NULL COMMENT '早班订单日期', + `morning_formula_name` varchar(128) DEFAULT NULL COMMENT '早班配方名称', + `morning_plan_weight` decimal(18,6) DEFAULT NULL COMMENT '早班计划重量', + `morning_planned_car_count` int DEFAULT NULL COMMENT '早班计划车数', + `morning_scheduled_car_count` int DEFAULT NULL COMMENT '早班已排产车数', + `morning_finished_car_count` int DEFAULT NULL COMMENT '早班完成车数', + `morning_plan_count` int DEFAULT NULL COMMENT '早班计划', + `morning_remark` varchar(500) DEFAULT NULL COMMENT '早班备注', + + `noon_plan_id` varchar(32) DEFAULT NULL COMMENT '中班计划ID(母胶/终胶计划)', + `noon_plan_type` varchar(2) DEFAULT NULL COMMENT '中班计划类型:M母胶/F终胶', + `noon_order_no` varchar(64) DEFAULT NULL COMMENT '中班生产订单', + `noon_order_date` date DEFAULT NULL COMMENT '中班订单日期', + `noon_formula_name` varchar(128) DEFAULT NULL COMMENT '中班配方名称', + `noon_plan_weight` decimal(18,6) DEFAULT NULL COMMENT '中班计划重量', + `noon_planned_car_count` int DEFAULT NULL COMMENT '中班计划车数', + `noon_scheduled_car_count` int DEFAULT NULL COMMENT '中班已排产车数', + `noon_finished_car_count` int DEFAULT NULL COMMENT '中班完成车数', + `noon_plan_count` int DEFAULT NULL COMMENT '中班计划', + `noon_remark` varchar(500) DEFAULT NULL COMMENT '中班备注', + + `night_plan_id` varchar(32) DEFAULT NULL COMMENT '晚班计划ID(母胶/终胶计划)', + `night_plan_type` varchar(2) DEFAULT NULL COMMENT '晚班计划类型:M母胶/F终胶', + `night_order_no` varchar(64) DEFAULT NULL COMMENT '晚班生产订单', + `night_order_date` date DEFAULT NULL COMMENT '晚班订单日期', + `night_formula_name` varchar(128) DEFAULT NULL COMMENT '晚班配方名称', + `night_plan_weight` decimal(18,6) DEFAULT NULL COMMENT '晚班计划重量', + `night_planned_car_count` int DEFAULT NULL COMMENT '晚班计划车数', + `night_scheduled_car_count` int DEFAULT NULL COMMENT '晚班已排产车数', + `night_finished_car_count` int DEFAULT NULL COMMENT '晚班完成车数', + `night_plan_count` int DEFAULT NULL COMMENT '晚班计划', + `night_remark` varchar(500) DEFAULT NULL COMMENT '晚班备注', + + `tenant_id` int DEFAULT NULL COMMENT '租户', + `sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门编码', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `del_flag` int DEFAULT '0' COMMENT '删除标记(0正常1删除)', + PRIMARY KEY (`id`), + KEY `idx_mxmp_machine` (`machine_id`), + KEY `idx_mxmp_sort` (`sort_no`), + KEY `idx_mxmp_tenant` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES密炼生产计划维护'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_129__mes_xsl_raw_material_demand_plan.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_129__mes_xsl_raw_material_demand_plan.sql new file mode 100644 index 00000000..15ce51e8 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_129__mes_xsl_raw_material_demand_plan.sql @@ -0,0 +1,23 @@ +-- 原材料需求计划明细表 +CREATE TABLE IF NOT EXISTS `mes_xsl_raw_material_demand_plan` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `machine_id` varchar(32) DEFAULT NULL COMMENT '机台ID', + `machine_name` varchar(64) DEFAULT NULL COMMENT '机台名称', + `erp_code` varchar(64) NOT NULL COMMENT 'ERP编号', + `raw_material_name` varchar(128) NOT NULL COMMENT '原材料名称', + `demand_weight` decimal(18,6) DEFAULT '0.000000' COMMENT '需求重量', + `standard_weight` decimal(18,6) DEFAULT '0.000000' COMMENT '标准重量', + `actual_weight` decimal(18,6) DEFAULT '0.000000' COMMENT '实际重量', + `tenant_id` int DEFAULT NULL COMMENT '租户ID', + `sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门', + `create_by` varchar(50) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(50) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `del_flag` tinyint(1) DEFAULT '0' COMMENT '删除标识(0-正常,1-删除)', + PRIMARY KEY (`id`), + KEY `idx_raw_material_demand_machine` (`machine_name`), + KEY `idx_raw_material_demand_erp` (`erp_code`), + KEY `idx_raw_material_demand_name` (`raw_material_name`), + KEY `idx_raw_material_demand_del` (`del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原材料需求计划明细'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/xslmes/test/MesXslProductionOrderSplitIntegrationTest.java b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/xslmes/test/MesXslProductionOrderSplitIntegrationTest.java new file mode 100644 index 00000000..05d2d9f4 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/xslmes/test/MesXslProductionOrderSplitIntegrationTest.java @@ -0,0 +1,141 @@ +package org.jeecg.modules.xslmes.test; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.annotation.Resource; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import org.jeecg.JeecgSystemApplication; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.modules.mes.material.entity.MesMaterial; +import org.jeecg.modules.mes.material.mapper.MesMaterialMapper; +import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan; +import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan; +import org.jeecg.modules.xslmes.entity.MesXslMixingSpec; +import org.jeecg.modules.xslmes.entity.MesXslProductionOrder; +import org.jeecg.modules.xslmes.mapper.MesXslFinalBatchPlanMapper; +import org.jeecg.modules.xslmes.mapper.MesXslMasterBatchPlanMapper; +import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecMapper; +import org.jeecg.modules.xslmes.mapper.MesXslProductionOrderMapper; +import org.jeecg.modules.xslmes.service.IMesXslProductionOrderService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = JeecgSystemApplication.class) +@Transactional +class MesXslProductionOrderSplitIntegrationTest { + + @Resource private IMesXslProductionOrderService productionOrderService; + @Resource private MesXslProductionOrderMapper productionOrderMapper; + @Resource private MesXslMasterBatchPlanMapper masterBatchPlanMapper; + @Resource private MesXslFinalBatchPlanMapper finalBatchPlanMapper; + @Resource private MesMaterialMapper materialMapper; + @Resource private MesXslMixingSpecMapper mixingSpecMapper; + + @Test + void splitShouldGenerateB1B2AndFWhenSegmentCountIs3() { + String base = "UT" + System.currentTimeMillis(); + MesXslProductionOrder order = createOrder(base, 3); + + createMaterial("B1" + base + "SA01", "B1-" + base); + createMaterial("B2" + base + "SA01", "B2-" + base); + createMaterial("F" + base + "SA01", "F-" + base); + + createMixingSpec("B1" + base + "SA01"); + createMixingSpec("B2" + base + "SA01"); + createMixingSpec("F" + base + "SA01"); + + List masters = productionOrderService.splitToMasterBatchPlan(order.getId()); + Assertions.assertEquals(2, masters.size(), "段数=3时应生成2条母胶计划"); + + List dbMasters = + masterBatchPlanMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslMasterBatchPlan::getSourceOrderId, order.getId()) + .orderByAsc(MesXslMasterBatchPlan::getMaterialCode)); + Assertions.assertEquals(2, dbMasters.size()); + Assertions.assertEquals("B1" + base, dbMasters.get(0).getMaterialCode()); + Assertions.assertEquals("B2" + base, dbMasters.get(1).getMaterialCode()); + + MesXslFinalBatchPlan finalPlan = + finalBatchPlanMapper.selectOne( + new LambdaQueryWrapper() + .eq(MesXslFinalBatchPlan::getSourceOrderId, order.getId()) + .last("LIMIT 1")); + Assertions.assertNotNull(finalPlan, "应生成1条终胶计划"); + Assertions.assertEquals("F" + base, finalPlan.getMaterialCode()); + + MesXslProductionOrder updated = productionOrderMapper.selectById(order.getId()); + Assertions.assertEquals(1, updated.getSplitStatus()); + } + + @Test + void splitShouldFailWhenAnyMixingSpecMissing() { + String base = "UT" + (System.currentTimeMillis() + 7); + MesXslProductionOrder order = createOrder(base, 3); + + createMaterial("B1" + base + "SA01", "B1-" + base); + createMaterial("B2" + base + "SA01", "B2-" + base); + createMaterial("F" + base + "SA01", "F-" + base); + + // 故意缺少 FxxxSA01 + createMixingSpec("B1" + base + "SA01"); + createMixingSpec("B2" + base + "SA01"); + + JeecgBootException ex = + Assertions.assertThrows( + JeecgBootException.class, + () -> productionOrderService.splitToMasterBatchPlan(order.getId())); + Assertions.assertEquals("对应物料的混炼示方不存在,请先生成混炼示方!", ex.getMessage()); + + List dbMasters = + masterBatchPlanMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslMasterBatchPlan::getSourceOrderId, order.getId())); + Assertions.assertTrue(dbMasters.isEmpty(), "失败后母胶计划应回滚"); + + MesXslFinalBatchPlan finalPlan = + finalBatchPlanMapper.selectOne( + new LambdaQueryWrapper() + .eq(MesXslFinalBatchPlan::getSourceOrderId, order.getId()) + .last("LIMIT 1")); + Assertions.assertNull(finalPlan, "失败后终胶计划应回滚"); + + MesXslProductionOrder unchanged = productionOrderMapper.selectById(order.getId()); + Assertions.assertTrue( + unchanged.getSplitStatus() == null || unchanged.getSplitStatus() == 0, "失败后拆分状态不应置为已拆分"); + } + + private MesXslProductionOrder createOrder(String baseCode, int segmentCount) { + MesXslProductionOrder order = new MesXslProductionOrder(); + order.setProductionOrderNo("PO-" + baseCode); + order.setOrderDate(new Date()); + order.setProcessSegmentCount(segmentCount); + order.setMesMaterialName(baseCode); + order.setMaterialCode(baseCode); + order.setPlanQty(new BigDecimal("100.00")); + order.setSplitStatus(0); + order.setDelFlag(0); + productionOrderMapper.insert(order); + return order; + } + + private void createMaterial(String code, String name) { + MesMaterial material = new MesMaterial(); + material.setMaterialCode(code); + material.setMaterialName(name); + material.setDelFlag(0); + materialMapper.insert(material); + } + + private void createMixingSpec(String specName) { + MesXslMixingSpec spec = new MesXslMixingSpec(); + spec.setSpecName(specName); + spec.setDelFlag(0); + mixingSpecMapper.insert(spec); + } +} diff --git a/jeecgboot-vue3/src/views/mes/mixingproductionplaninfo/index.vue b/jeecgboot-vue3/src/views/mes/mixingproductionplaninfo/index.vue new file mode 100644 index 00000000..88112b8f --- /dev/null +++ b/jeecgboot-vue3/src/views/mes/mixingproductionplaninfo/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/jeecgboot-vue3/src/views/mes/rawmaterialdemandplan/index.vue b/jeecgboot-vue3/src/views/mes/rawmaterialdemandplan/index.vue new file mode 100644 index 00000000..72a06c71 --- /dev/null +++ b/jeecgboot-vue3/src/views/mes/rawmaterialdemandplan/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlan.api.ts b/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlan.api.ts new file mode 100644 index 00000000..baf89d15 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlan.api.ts @@ -0,0 +1,13 @@ +import { defHttp } from '/@/utils/http/axios'; + +enum Api { + list = '/xslmes/mesXslMixingProductionPlan/list', + saveAll = '/xslmes/mesXslMixingProductionPlan/saveAll', + orderOptionPage = '/xslmes/mesXslMixingProductionPlan/orderOptionPage', +} + +export const list = (params) => defHttp.get({ url: Api.list, params }); + +export const saveAll = (params) => defHttp.post({ url: Api.saveAll, params }); + +export const orderOptionPage = (params) => defHttp.get({ url: Api.orderOptionPage, params }, { successMessageMode: 'none' }); diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlanList.vue b/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlanList.vue new file mode 100644 index 00000000..2a41c03d --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/MesXslMixingProductionPlanList.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/components/MesXslMixingPlanOrderSelectModal.vue b/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/components/MesXslMixingPlanOrderSelectModal.vue new file mode 100644 index 00000000..831032af --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixingProductionPlan/components/MesXslMixingPlanOrderSelectModal.vue @@ -0,0 +1,89 @@ + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrder.api.ts b/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrder.api.ts index 29cd232e..ba683c70 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrder.api.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrder.api.ts @@ -8,6 +8,7 @@ enum Api { deleteBatch = '/xslmes/mesXslProductionOrder/deleteBatch', queryById = '/xslmes/mesXslProductionOrder/queryById', split = '/xslmes/mesXslProductionOrder/split', + splitBatch = '/xslmes/mesXslProductionOrder/splitBatch', exportXls = '/xslmes/mesXslProductionOrder/exportXls', } @@ -26,4 +27,7 @@ export const queryById = (params) => defHttp.get({ url: Api.queryById, params }) export const splitToMasterBatchPlan = (params, handleSuccess) => defHttp.post({ url: Api.split, params }, { joinParamsToUrl: true }).then(() => handleSuccess()); +export const splitToMasterBatchPlanBatch = (params, handleSuccess) => + defHttp.post({ url: Api.splitBatch, params }, { joinParamsToUrl: true }).then(() => handleSuccess()); + export const getExportUrl = Api.exportXls; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrderList.vue b/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrderList.vue index 67ab5582..41b45257 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrderList.vue +++ b/jeecgboot-vue3/src/views/xslmes/mesXslProductionOrder/MesXslProductionOrderList.vue @@ -11,6 +11,15 @@ > 导出 + + 拆分 +