4 天以前 741918a903e17b2ec7522556d2c043b8d35dd8a1
生产取消bom,不合格管理定制化
已添加7个文件
已修改36个文件
已删除32个文件
5595 ■■■■ 文件已修改
doc/20260615_add_actual_time_to_production_product_main.sql 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260615_add_disposal_method_to_production_order.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260615_add_opinion_fields_to_quality_unqualified_order.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260615_change_operation_task_time_to_datetime.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/production_remove_bom_pick.md 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/production_report_split.md 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/quality_unqualified_order.md 693 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/ProductModelDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/ProductionTaskStatisticsDto.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/BomImportDto.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductStructureDto.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionBomStructureDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickRecordDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionBomStructureVo.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickVo.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionBomStructureController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderBomController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderPickController.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionBomStructureMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderBomMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickMapper.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionBomStructure.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOperationTask.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderBom.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderRouting.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductMain.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionBomStructureService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderBomService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderPickService.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionProductMainService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 889 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderBomServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 1181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedOrderController.java 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/mapper/QualityUnqualifiedOrderMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityUnqualifiedOrder.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/IQualityUnqualifiedOrderService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedOrderServiceImpl.java 313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionBomStructureMapper.xml 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderBomMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderPickMapper.xml 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderRoutingMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductMainMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedOrderMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/不合格品处理单.xls 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260615_add_actual_time_to_production_product_main.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
-- ç”Ÿäº§æŠ¥å·¥æ‹†åˆ†ï¼šproduction_product_main æ–°å¢žå®žé™…开始/结束时间字段
ALTER TABLE production_product_main
    ADD COLUMN actual_start_time datetime NULL COMMENT '实际开始时间' AFTER work_hour,
    ADD COLUMN actual_end_time datetime NULL COMMENT '实际结束时间' AFTER actual_start_time;
doc/20260615_add_disposal_method_to_production_order.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- ç”Ÿäº§è®¢å•增加处置方式字段(不合格品处理单回传标记)
ALTER TABLE production_order
    ADD COLUMN disposal_method int NULL COMMENT '处置方式(2=厂内维修,3=返厂维修)' AFTER is_end_order;
doc/20260615_add_opinion_fields_to_quality_unqualified_order.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
-- ä¸åˆæ ¼å“å¤„理单增加意见/决定字段
ALTER TABLE quality_unqualified_order
    ADD COLUMN dept_opinion varchar(500) NULL COMMENT '责任部门主管意见' AFTER remark,
    ADD COLUMN company_decision varchar(500) NULL COMMENT '公司处理决定' AFTER dept_opinion,
    ADD COLUMN general_manager_opinion varchar(500) NULL COMMENT '总经理意见' AFTER company_decision;
doc/20260615_change_operation_task_time_to_datetime.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
-- ç”Ÿäº§å·¥å•时间字段:date æ”¹ä¸º datetime,支持时分秒
ALTER TABLE production_operation_task
    MODIFY COLUMN plan_start_time datetime NULL COMMENT '计划开始时间',
    MODIFY COLUMN plan_end_time datetime NULL COMMENT '计划结束时间',
    MODIFY COLUMN actual_start_time datetime NULL COMMENT '实际开始时间',
    MODIFY COLUMN actual_end_time datetime NULL COMMENT '实际结束时间';
docs/production_remove_bom_pick.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
# ç”Ÿäº§æ¨¡å—移除 BOM å’Œé¢†æ–™/补料
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§è®¢å•详情页(领料单 Tab)
- ç”Ÿäº§è®¢å•列表页
- ç”Ÿäº§é¢†æ–™ç®¡ç†é¡µ
- ç”Ÿäº§è¡¥æ–™/退料记录页
- åº“存入库管理页(退料审核)
## API
### åˆ é™¤çš„æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /productionOrder/pick/{productionOrderId} | æ ¹æ®è®¢å•ID查询BOM领料单 |
| POST | /productionOrderPick/savePick | ä¿å­˜é¢†æ–™åˆ°çº¿è¾¹ä»“ |
| POST | /productionOrderPick/updatePick | å˜æ›´é¢†æ–™ï¼ˆå«è¡¥æ–™/退料) |
| GET | /productionOrderPick/detail/{productionOrderId} | æŸ¥è¯¢å·²é¢†æ–™è¯¦æƒ… |
| GET | /productionOrderPickRecord/feeding | æŸ¥è¯¢è¡¥æ–™è®°å½• |
| GET | /productionBomStructure/listByBomId/{bomId} | æŸ¥è¯¢BOM结构树 |
| POST | /productionBomStructure/addOrUpdateBomStructs | æ–°å¢ž/修改BOM结构 |
### å“åº”字段变更
**`GET /productionOrder/page`、`/productionOrder/list`、`/productionOrder/{id}`**:
移除响应字段:
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| bomNo | String | BOM编号(已移除) |
| returned | Boolean | æ˜¯å¦å·²é€€æ–™ï¼ˆå·²ç§»é™¤ï¼‰ |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ç”Ÿäº§è®¢å•详情页
删除领料单 Tab åŠç›¸å…³æŽ¥å£è°ƒç”¨ï¼š
```html
<!-- åˆ é™¤ä»¥ä¸‹ Tab -->
<el-tab-pane label="领料单" name="pick">
  <pick-list :order-id="orderId" />
</el-tab-pane>
```
### 2. ç”Ÿäº§è®¢å•列表页
移除表格中的 BOM ç¼–号和退料状态列:
```html
<!-- åˆ é™¤ä»¥ä¸‹åˆ— -->
<el-table-column label="BOM编号" prop="bomNo" />
<el-table-column label="退料状态" prop="returned" />
```
### 3. ç”Ÿäº§é¢†æ–™/补料/退料页面
整个页面模块可移除,包括:
- é¢†æ–™é¡µé¢ï¼ˆ`productionOrderPick` ç›¸å…³è·¯ç”±ã€ç»„件)
- è¡¥æ–™è®°å½•页面(`productionOrderPickRecord` ç›¸å…³è·¯ç”±ã€ç»„件)
- BOM ç»“构管理页面(`productionBomStructure` ç›¸å…³è·¯ç”±ã€ç»„件)
### 4. åº“存入库管理页
移除退料审核相关的 `ProductionOrderPick` æ•°æ®å¤„理逻辑。入库记录不再关联领料记录表的 `returnQty` è®¡ç®—。
### 5. æŠ¥å·¥æŠ•入品
报工时投入品不再从 BOM ç»“构解析,改为直接使用当前工序的产品规格作为投入品。前端无需改动,后端自动处理。
## æ³¨æ„äº‹é¡¹
- æœ¬æ¬¡å˜æ›´åŽï¼Œé¢†æ–™æ“ä½œç”±ç”¨æˆ·ç›´æŽ¥åœ¨åº“存管理模块进行,不再与生产工单关联
- ç”Ÿäº§è®¢å•列表和历史数据中 `bomNo` å’Œ `returned` å­—段不再返回,前端如缓存了这些字段需做兼容处理
- æ‰€æœ‰ `/productionOrderPick`、`/productionOrderPickRecord`、`/productionBomStructure`、`/productionOrderBom` æŽ¥å£å·²ç§»é™¤ï¼Œè°ƒç”¨è¿™äº›æŽ¥å£çš„前端代码需全部清理
- æ•°æ®åº“表 `production_order_pick`、`production_order_pick_record`、`production_order_bom`、`production_bom_structure` å¦‚需保留历史数据可暂不删除
docs/production_report_split.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,146 @@
# ç”Ÿäº§æŠ¥å·¥æ‹†åˆ†ï¼ˆå¼€å§‹æŠ¥å·¥/结束报工)及工人权限过滤
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§å·¥å•列表页(可报工工单筛选)
- ç”Ÿäº§æŠ¥å·¥é¡µï¼ˆå¼€å§‹æŠ¥å·¥ / ç»“束报工)
- æŠ¥å·¥å°è´¦é¡µï¼ˆå®žé™…工时展示)
## API
### æ–°å¢žæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /productionProductMain/startWork | å¼€å§‹æŠ¥å·¥ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| productionOperationTaskId | Long | æ˜¯ | ç”Ÿäº§å·¥å•ID |
| userId | Long | æ˜¯ | æŠ¥å·¥äººå‘˜ID |
**响应:** `{ "code": 200, "msg": "操作成功", "data": true }`
### å˜æ›´æŽ¥å£
**`POST /productionProductMain/addProductMain`(结束报工)**:
入参增加:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å¼€å§‹æŠ¥å·¥è¿”回的 ProductionProductMain ID |
**`GET /productionOperationTask/page`、`/productionOperationTask/list`**:
新增查询参数:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| filterMine | Boolean | å¦ | ä¸º true æ—¶ä»…返回当前用户被指派的工单 |
### å“åº”字段变更
**`GET /productionProductMain/listPage`、`/page`、`/{id}`**:
新增响应字段:
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| actualStartTime | String | å®žé™…开始时间 (yyyy-MM-dd HH:mm:ss) |
| actualEndTime | String | å®žé™…结束时间 (yyyy-MM-dd HH:mm:ss) |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ç”Ÿäº§æŠ¥å·¥é¡µ â€” æ‹†åˆ†ä¸ºä¸¤æ­¥æ“ä½œ
**开始报工按钮:**
```html
<el-button type="primary" @click="handleStartWork">开始报工</el-button>
```
```js
handleStartWork() {
  this.$confirm('确认开始报工?', '提示', { type: 'info' }).then(() => {
    startWork({
      productionOperationTaskId: this.taskId,
      userId: this.selectedUserId
    }).then(res => {
      this.startRecordId = res.data; // ä¿å­˜è¿”回的报工记录ID,供结束报工使用
      this.$message.success('开始报工成功');
      this.refreshTaskStatus();
    });
  });
}
```
**结束报工按钮(原报工提交按钮改造):**
```html
<el-button type="success" @click="handleFinishWork" :disabled="!startRecordId">结束报工</el-button>
```
```js
handleFinishWork() {
  this.$refs.form.validate(valid => {
    if (valid) {
      addProductMain({
        id: this.startRecordId, // å¿…须传入开始报工记录ID
        quantity: this.form.quantity,
        scrapQty: this.form.scrapQty,
        userId: this.selectedUserId,
        productionOperationParamList: this.paramList
      }).then(() => {
        this.$message.success('结束报工成功');
        this.resetForm();
      });
    }
  });
}
```
### 2. å·¥å•列表页 â€” æƒé™è¿‡æ»¤
增加"仅看我的"筛选开关:
```html
<el-switch v-model="filterMine" active-text="仅看我的" @change="handleFilterChange" />
```
```js
data() {
  return {
    filterMine: false,
  }
},
methods: {
  handleFilterChange(val) {
    this.loadTaskList();
  },
  loadTaskList() {
    const params = { ...this.queryParams };
    if (this.filterMine) {
      params.filterMine = true;
    }
    listTask(params).then(res => {
      this.taskList = res.rows;
    });
  }
}
```
### 3. æŠ¥å·¥å°è´¦é¡µ â€” å±•示实际工时
台账中的 `workHour` å­—段现在基于实际开始/结束时间自动计算(以小时为单位),前端直接展示即可,无需额外处理。
## æ³¨æ„äº‹é¡¹
- æŠ¥å·¥æµç¨‹å˜ä¸ºä¸¤æ­¥ï¼šå…ˆè°ƒç”¨ `/startWork` å¼€å§‹ï¼Œå†è°ƒç”¨ `/addProductMain`(传入开始报工返回的 `id`)结束
- å¼€å§‹æŠ¥å·¥åŽå·¥å•状态变为"生产中",结束报工后才创建产出品、投入品和核算记录
- å®žé™…工时 = actualEndTime - actualStartTime,自动计算(也可手动传入 workHour è¦†ç›–)
- å·¥å•未指派(userIds ä¸ºç©ºæˆ– `[]`)时所有工人可操作;已指派时仅被指派人可操作
- å‰ç«¯éœ€ç¼“å­˜ `startRecordId`(开始报工返回的 ID),结束报工时传入
- å¦‚果用户关闭页面后重新打开,需要查询 `status=0` çš„进行中报工记录来恢复 `startRecordId`
docs/quality_unqualified_order.md
@@ -1,34 +1,49 @@
# ä¸åˆæ ¼å“å¤„理单 â€” å‰ç«¯è”调文档
# ä¸åˆæ ¼å“å¤„理单模块(QualityUnqualifiedOrder)
## æ¦‚è¿°
不合格品处理单是正式的不合格品处置流程模块,替代旧的不合格管理(`/quality/qualityUnqualified`)中的处置功能。旧模块的不合格品发现(新增/列表/详情)继续使用,但**处置操作统一使用本模块**。
### è‡ªåŠ¨åˆ›å»ºæœºåˆ¶
当检验单(原材料/过程/出厂检验)提交时,如果**不合格数量 > 0**,系统在创建 `QualityUnqualified` è®°å½•的同时,**自动创建一条不合格品处理单**,`unqualifiedProcess`(不合格工序)根据检验类别自动映射:
| æ£€éªŒç±»åˆ« | inspectType | unqualifiedProcess |
|----------|-------------|-------------------|
| åŽŸææ–™æ£€éªŒ | 0 | 1(来料) |
| è¿‡ç¨‹æ£€éªŒ | 0 | 2(制程) |
| å‡ºåŽ‚æ£€éªŒ | 0 | 3(成品) |
处理单初始状态为 `0`(草稿),后续可编辑处置方式并提交审批。
### ä¸Žæ—§æ¨¡å—的关系
| | æ—§ï¼šä¸åˆæ ¼ç®¡ç† | æ–°ï¼šä¸åˆæ ¼å“å¤„理单 |
|---|---|---|
| è·¯å¾„ | `/quality/qualityUnqualified` | `/qualityUnqualifiedOrder` |
| ç”¨é€” | ä¸åˆæ ¼å“é¦–次记录、查看 | æ­£å¼å¤„置流程(含审批) |
| å¤„置方式 | `dealResult` è‡ªç”±æ–‡æœ¬ | `disposalMethod` ç»“构化枚举 |
| åˆ›å»ºæ–¹å¼ | æ£€éªŒå•提交自动创建 / æ‰‹åŠ¨æ–°å¢ž | æ£€éªŒå•提交自动创建 / æ‰‹åŠ¨æ–°å¢ž |
## æ¶‰åŠé¡µé¢
- **不合格品处理单列表/详情** â€” æ–°å¢žé¡µé¢
## å˜æ›´è¯´æ˜Ž
新增 `quality_unqualified_order` ä¸åˆæ ¼å“å¤„理单模块,支持对不合格品进行正式的处置记录,包含审批状态机(草稿→待审批→审批中→已完成/已驳回)。附件使用系统统一的 `storage_attachment` è¡¨ã€‚
- ä¸åˆæ ¼å“å¤„理单列表页
- ä¸åˆæ ¼å“å¤„理单新增/编辑页
- ä¸åˆæ ¼å“å¤„理单详情页
## API
### ä¸åˆæ ¼å“å¤„理单
### 1. æ–°å¢žå¤„理单
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/qualityUnqualifiedOrder/save` | æ–°å¢žå¤„理单(编号自动生成) |
| PUT | `/qualityUnqualifiedOrder/update` | ä¿®æ”¹å¤„理单 |
| DELETE | `/qualityUnqualifiedOrder/delete` | æ‰¹é‡åˆ é™¤ï¼ˆé€»è¾‘删除) |
| GET | `/qualityUnqualifiedOrder/listPage` | åˆ†é¡µæŸ¥è¯¢ |
| GET | `/qualityUnqualifiedOrder/{id}` | æŸ¥çœ‹è¯¦æƒ… |
| POST | /qualityUnqualifiedOrder/save | æ–°å¢žä¸åˆæ ¼å“å¤„理单 |
### è¯·æ±‚/响应参数
**QualityUnqualifiedOrder å¯¹è±¡ï¼š**
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | ä¸»é”®ï¼ˆä¿®æ”¹æ—¶å¿…填) |
| orderNo | String | å¦ | å¤„理单编号,自动生成,前缀 BHG+日期+序号 |
| unqualifiedId | Long | å¦ | å…³è”不合格品ID |
| unqualifiedId | Long | æ˜¯ | å…³è”不合格品ID |
| projectName | String | å¦ | é¡¹ç›®åç§° |
| projectNo | String | å¦ | é¡¹ç›®ç¼–号 |
| equipmentId | Long | å¦ | å…³è”设备ID |
@@ -41,7 +56,7 @@
| materialQuality | String | å¦ | æè´¨ |
| quantity | BigDecimal | å¦ | æ€»æ•°é‡ |
| unqualifiedQuantity | BigDecimal | å¦ | ä¸åˆæ ¼æ•°é‡ |
| unqualifiedProcess | Integer | å¦ | ä¸åˆæ ¼å·¥åºï¼š1=来料,2=制程,3=成品 |
| unqualifiedProcess | Integer | å¦ | ä¸åˆæ ¼å·¥åºï¼š1=来料, 2=制程, 3=成品 |
| supplierName | String | å¦ | ä¾›åº”商名称 |
| inspectorName | String | å¦ | æ£€éªŒå‘˜ |
| inspectDate | Date | å¦ | æ£€éªŒæ—¥æœŸ (yyyy-MM-dd) |
@@ -50,233 +65,501 @@
| problemDescription | String | å¦ | é—®é¢˜æè¿° |
| reasonAnalysis | String | å¦ | åŽŸå› åˆ†æžåŠå»ºè®® |
| correctionAction | String | å¦ | çº æ­£æŽªæ–½ |
| disposalMethod | Integer | å¦ | å¤„置方式:1=让步接收,2=厂内维修,3=返厂维修,4=换货,5=退货,6=报废 |
| **disposalMethod** | **Integer** | **是** | **处置方式:1=让步接收, 2=厂内维修, 3=返厂维修, 4=换货, 5=退货, 6=报废** |
| repairEvaluation | String | å¦ | åނ内/返厂维修评估 |
| preventiveAction | String | å¦ | é¢„防措施 |
| status | Integer | å¦ | çŠ¶æ€ï¼š0=草稿,1=待审批,2=审批中,3=已完成,4=已驳回 |
| remark | String | å¦ | å¤‡æ³¨ |
| storageBlobDTOs | List\<StorageBlobDTO\> | å¦ | é™„件上传列表 |
| storageBlobVOs | List\<StorageBlobVO\> | å¦ | é™„件回显列表(查询时返回) |
| deptOpinion | String | å¦ | è´£ä»»éƒ¨é—¨ä¸»ç®¡æ„è§ |
| companyDecision | String | å¦ | å…¬å¸å¤„理决定 |
| generalManagerOpinion | String | å¦ | æ€»ç»ç†æ„è§ |
| storageBlobDTOs | List | å¦ | é™„件列表 |
### åˆ†é¡µæŸ¥è¯¢å‚æ•°
**自动行为:**
- è‡ªåŠ¨ç”Ÿæˆå¤„ç†å•ç¼–å·ï¼ˆ`BHGyyMMdd+流水号`)
- **手动新增时**:选了处置方式 â†’ çŠ¶æ€è‡ªåŠ¨ä¸º `3`(已完成);没选 â†’ çŠ¶æ€ä¸º `0`(草稿)
- **质检自动创建时**:状态初始为 `0`(草稿),需通过处理接口补充处置方式
- **当 `disposalMethod` é€‰æ‹© 2(厂内维修)或 3(返厂维修)时,自动创建返修生产订单**
**响应:** `{ "code": 200, "msg": "操作成功", "data": true }`
### 2. ä¿®æ”¹å¤„理单
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | /qualityUnqualifiedOrder/update | ä¿®æ”¹ä¸åˆæ ¼å“å¤„理单 |
**请求参数:** ä¸Žæ–°å¢žç›¸åŒï¼Œé¢å¤–需要 `id` å­—段。
### 3. åˆ é™¤å¤„理单
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| DELETE | /qualityUnqualifiedOrder/delete | åˆ é™¤ä¸åˆæ ¼å“å¤„理单 |
**请求参数:** `[id1, id2, ...]` â€” ID æ•°ç»„
### 4. åˆ†é¡µæŸ¥è¯¢
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /qualityUnqualifiedOrder/listPage | åˆ†é¡µæŸ¥è¯¢ |
**查询参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| pageNum | Integer | å¦ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Integer | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
| status | Integer | å¦ | çŠ¶æ€ç­›é€‰ |
| projectName | String | å¦ | é¡¹ç›®åç§°æ¨¡ç³Šæœç´¢ |
| orderNo | String | å¦ | ç¼–号模糊搜索 |
| entryDateStart | String | å¦ | åˆ›å»ºæ—¶é—´èµ·å§‹ |
| entryDateEnd | String | å¦ | åˆ›å»ºæ—¶é—´ç»“束 |
| page | int | å¦ | é¡µç ï¼ˆé»˜è®¤1) |
| size | int | å¦ | æ¯é¡µæ¡æ•°ï¼ˆé»˜è®¤10) |
| status | Integer | å¦ | çŠ¶æ€ï¼š0=草稿, 1=待审批, 2=审批中, 3=已完成, 4=已驳回 |
| projectName | String | å¦ | é¡¹ç›®åç§°ï¼ˆæ¨¡ç³ŠåŒ¹é…ï¼‰ |
| orderNo | String | å¦ | å¤„理单编号(模糊匹配) |
| entryDateStart | String | å¦ | åˆ›å»ºæ—¶é—´èµ· (yyyy-MM-dd) |
| entryDateEnd | String | å¦ | åˆ›å»ºæ—¶é—´æ­¢ (yyyy-MM-dd) |
### åˆ é™¤è¯·æ±‚体
**响应字段:** è¿”回 `QualityUnqualifiedOrder` å…¨éƒ¨å­—段,含 `storageBlobVOs`(附件列表)。
```json
[1, 2, 3]
```
### 5. è¯¦æƒ…查询
## æ•°æ®ç»“æž„
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /qualityUnqualifiedOrder/{id} | å¤„理单详情 |
### StorageBlobDTO(上传时传入)
**响应:** è¿”回单条 `QualityUnqualifiedOrder` å…¨éƒ¨å­—段,含附件。
```json
{
  "id": "临时文件ID(字符串)",
  "name": "文件名",
  "url": "文件路径",
  "fileSize": 1024
}
```
### 6. å¤„理(处置)
### StorageBlobVO(查询时返回)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /qualityUnqualifiedOrder/deal | å¯¹è‰ç¨¿çŠ¶æ€çš„å¤„ç†å•è¡¥å……å¤„ç½®æ–¹å¼å¹¶å®Œæˆ |
```json
{
  "id": 1,
  "name": "检验报告.pdf",
  "url": "/upload/20260613/xxx.pdf",
  "fileSize": 102400,
  "application": "FILE"
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| **id** | **Long** | **是** | å¤„理单ID |
| **disposalMethod** | **Integer** | **是** | **处置方式:1=让步接收, 2=厂内维修, 3=返厂维修, 4=换货, 5=退货, 6=报废** |
| repairEvaluation | String | å¦ | åނ内/返厂维修评估 |
| reasonAnalysis | String | å¦ | åŽŸå› åˆ†æžåŠå»ºè®® |
| correctionAction | String | å¦ | çº æ­£æŽªæ–½ |
| preventiveAction | String | å¦ | é¢„防措施 |
| remark | String | å¦ | å¤‡æ³¨ |
| deptOpinion | String | å¦ | è´£ä»»éƒ¨é—¨ä¸»ç®¡æ„è§ |
| companyDecision | String | å¦ | å…¬å¸å¤„理决定 |
| generalManagerOpinion | String | å¦ | æ€»ç»ç†æ„è§ |
**自动行为:**
- çŠ¶æ€æ›´æ–°ä¸º `3`(已完成)
- å¤„置方式为 2(厂内维修)或 3(返厂维修)时,自动创建返修生产订单
**响应:** `{ "code": 200, "msg": "操作成功", "data": true }`
### 7. å¯¼å‡ºå¤„理单
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /qualityUnqualifiedOrder/export/{id} | å¯¼å‡ºä¸åˆæ ¼å“å¤„理单为 Excel |
**请求参数:** `id` â€” å¤„理单ID(路径参数)
**响应:** æ–‡ä»¶æµï¼Œ`Content-Type: application/vnd.ms-excel`,文件名 `不合格品处理单_{编号}.xls`
**模板字段映射:**
| æ¨¡æ¿ä½ç½® | å­—段 |
|----------|------|
| é¡¹ç›®åç§° | projectName |
| é¡¹ç›®ç¼–号 | projectNo |
| è®¾å¤‡åç§° | equipmentName |
| è®¾å¤‡å›¾å· | equipmentDrawingNo |
| ç‰©æ–™åç§° | materialName |
| ç‰©æ–™å›¾å· | materialDrawingNo |
| åž‹å·è§„æ ¼ | specificationModel |
| æè´¨ | materialQuality |
| æ€»æ•°é‡ | quantity |
| ä¸åˆæ ¼æ•° | unqualifiedQuantity |
| ä¸åˆæ ¼å·¥åº | unqualifiedProcess(勾选来料/制程/成品) |
| ä¾›è´§å•†åç§° | supplierName |
| æ£€éªŒå‘˜ | inspectorName |
| æ£€éªŒæ—¥æœŸ | inspectDate |
| è´£ä»»äºº | responsiblePerson |
| è´£ä»»éƒ¨é—¨ | responsibleDept |
| é—®é¢˜æè¿° | problemDescription |
| åŽŸå› åˆ†æžåŠå»ºè®® | reasonAnalysis |
| çº æ­£æŽªæ–½ | correctionAction |
| å¤„置方式 | disposalMethod(勾选对应选项) |
| åނ内/返厂维修评估 | repairEvaluation |
| é¢„防措施 | preventiveAction |
| è´£ä»»éƒ¨é—¨ä¸»ç®¡æ„è§ | deptOpinion |
| å…¬å¸å¤„理决定 | companyDecision |
| æ€»ç»ç†æ„è§ | generalManagerOpinion |
**前端调用示例:**
```js
// åˆ—表页操作列增加导出按钮
<el-button text type="primary" @click="handleExport(row.id)">导出</el-button>
// è¯¦æƒ…页增加导出按钮
<el-button type="primary" @click="handleExport">导出</el-button>
methods: {
  handleExport(id) {
    window.open(`/api/qualityUnqualifiedOrder/export/${id}`);
  },
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ä¸åˆæ ¼å“å¤„理单 â€” æ–°å¢ž/编辑表单
### 1. å¤„理单列表页
```html
<el-form :model="form" :rules="rules" ref="formRef">
  <el-form-item label="项目名称" prop="projectName">
    <el-input v-model="form.projectName" />
  </el-form-item>
  <el-form-item label="项目编号" prop="projectNo">
    <el-input v-model="form.projectNo" />
  </el-form-item>
  <el-form-item label="设备名称" prop="equipmentName">
    <el-input v-model="form.equipmentName" />
  </el-form-item>
  <el-form-item label="物料/部件名称" prop="materialName">
    <el-input v-model="form.materialName" />
  </el-form-item>
  <el-form-item label="型号规格" prop="specificationModel">
    <el-input v-model="form.specificationModel" />
  </el-form-item>
  <el-form-item label="材质" prop="materialQuality">
    <el-input v-model="form.materialQuality" />
  </el-form-item>
  <el-form-item label="总数量" prop="quantity">
    <el-input-number v-model="form.quantity" />
  </el-form-item>
  <el-form-item label="不合格数量" prop="unqualifiedQuantity">
    <el-input-number v-model="form.unqualifiedQuantity" />
  </el-form-item>
  <el-form-item label="不合格工序" prop="unqualifiedProcess">
    <el-select v-model="form.unqualifiedProcess">
      <el-option label="来料" :value="1" />
      <el-option label="制程" :value="2" />
      <el-option label="成品" :value="3" />
    </el-select>
  </el-form-item>
  <el-form-item label="供应商名称" prop="supplierName">
    <el-input v-model="form.supplierName" />
  </el-form-item>
  <el-form-item label="检验员" prop="inspectorName">
    <el-input v-model="form.inspectorName" />
  </el-form-item>
  <el-form-item label="检验日期" prop="inspectDate">
    <el-date-picker v-model="form.inspectDate" type="date" value-format="yyyy-MM-dd" />
  </el-form-item>
  <el-form-item label="责任人" prop="responsiblePerson">
    <el-input v-model="form.responsiblePerson" />
  </el-form-item>
  <el-form-item label="责任部门" prop="responsibleDept">
    <el-input v-model="form.responsibleDept" />
  </el-form-item>
  <el-form-item label="问题描述" prop="problemDescription">
    <el-input type="textarea" v-model="form.problemDescription" />
  </el-form-item>
  <el-form-item label="原因分析" prop="reasonAnalysis">
    <el-input type="textarea" v-model="form.reasonAnalysis" />
  </el-form-item>
  <el-form-item label="纠正措施" prop="correctionAction">
    <el-input type="textarea" v-model="form.correctionAction" />
  </el-form-item>
  <el-form-item label="处置方式" prop="disposalMethod">
    <el-select v-model="form.disposalMethod">
      <el-option label="让步接收" :value="1" />
      <el-option label="厂内维修" :value="2" />
      <el-option label="返厂维修" :value="3" />
      <el-option label="换货" :value="4" />
      <el-option label="退货" :value="5" />
      <el-option label="报废" :value="6" />
    </el-select>
  </el-form-item>
  <el-form-item label="维修评估" prop="repairEvaluation">
    <el-input type="textarea" v-model="form.repairEvaluation" />
  </el-form-item>
  <el-form-item label="预防措施" prop="preventiveAction">
    <el-input type="textarea" v-model="form.preventiveAction" />
  </el-form-item>
  <el-form-item label="备注" prop="remark">
    <el-input type="textarea" v-model="form.remark" />
  </el-form-item>
  <el-form-item label="附件">
    <file-upload
      v-model="form.storageBlobDTOs"
      :file-list="form.storageBlobVOs"
    />
  </el-form-item>
</el-form>
<template>
  <div class="app-container">
    <!-- æœç´¢æ  -->
    <el-form :model="queryParams" :inline="true">
      <el-form-item label="处理单编号">
        <el-input v-model="queryParams.orderNo" placeholder="输入编号" />
      </el-form-item>
      <el-form-item label="项目名称">
        <el-input v-model="queryParams.projectName" placeholder="输入项目" />
      </el-form-item>
      <el-form-item label="状态">
        <el-select v-model="queryParams.status" placeholder="全部" clearable>
          <el-option label="草稿" :value="0" />
          <el-option label="待审批" :value="1" />
          <el-option label="审批中" :value="2" />
          <el-option label="已完成" :value="3" />
          <el-option label="已驳回" :value="4" />
        </el-select>
      </el-form-item>
      <el-form-item label="创建时间">
        <el-date-picker v-model="dateRange" type="daterange" value-format="yyyy-MM-dd"
          start-placeholder="开始" end-placeholder="结束" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-row :gutter="10" class="mb8">
      <el-button type="primary" @click="handleAdd">新增处理单</el-button>
      <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete">删除</el-button>
    </el-row>
    <!-- è¡¨æ ¼ -->
    <el-table :data="list" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" />
      <el-table-column label="处理单编号" prop="orderNo" width="160" />
      <el-table-column label="项目名称" prop="projectName" />
      <el-table-column label="型号规格" prop="specificationModel" width="120" />
      <el-table-column label="不合格数量" prop="unqualifiedQuantity" width="100" />
      <el-table-column label="处置方式" width="110">
        <template #default="{ row }">
          {{ disposalMethodMap[row.disposalMethod] }}
        </template>
      </el-table-column>
      <el-table-column label="状态" width="80">
        <template #default="{ row }">
          <el-tag :type="statusTagType(row.status)">{{ statusMap[row.status] }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="检验员" prop="inspectorName" width="100" />
      <el-table-column label="检验日期" prop="inspectDate" width="110" />
      <el-table-column label="创建时间" prop="createTime" width="160" />
      <el-table-column label="操作" width="120" fixed="right">
        <template #default="{ row }">
          <el-button text type="primary" @click="handleDetail(row.id)">详情</el-button>
          <el-button text type="primary" @click="handleEdit(row)">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination :total="total" v-model:page="page" v-model:limit="size"
      @pagination="loadList" />
  </div>
</template>
```
### 2. ä¸åˆæ ¼å“å¤„理单 â€” åˆ—表页
```js
data() {
  return {
    list: [],
    total: 0,
    page: 1,
    size: 10,
    selectedIds: [],
    dateRange: [],
    queryParams: {
      orderNo: '',
      projectName: '',
      status: null,
    },
    disposalMethodMap: { 1: '让步接收', 2: '厂内维修', 3: '返厂维修', 4: '换货', 5: '退货', 6: '报废' },
    statusMap: { 0: '草稿', 1: '待审批', 2: '审批中', 3: '已完成', 4: '已驳回' },
  }
},
methods: {
  statusTagType(status) {
    const map = { 0: 'info', 1: 'warning', 2: '', 3: 'success', 4: 'danger' };
    return map[status] || 'info';
  },
  loadList() {
    const params = { ...this.queryParams, page: this.page, size: this.size };
    if (this.dateRange && this.dateRange.length === 2) {
      params.entryDateStart = this.dateRange[0];
      params.entryDateEnd = this.dateRange[1];
    }
    listPage(params).then(res => {
      this.list = res.rows;
      this.total = res.total;
    });
  },
  handleQuery() { this.page = 1; this.loadList(); },
  resetQuery() {
    this.queryParams = { orderNo: '', projectName: '', status: null };
    this.dateRange = [];
    this.handleQuery();
  },
  handleSelectionChange(selection) { this.selectedIds = selection.map(i => i.id); },
  handleAdd() { this.$router.push('/quality/unqualified-order/add'); },
  handleEdit(row) { this.$router.push({ path: '/quality/unqualified-order/edit', query: { id: row.id } }); },
  handleDetail(id) { this.$router.push({ path: '/quality/unqualified-order/detail', query: { id } }); },
  handleDelete() {
    this.$confirm('确认删除选中的处理单?').then(() => {
      deleteOrders(this.selectedIds).then(() => {
        this.$message.success('删除成功');
        this.loadList();
      });
    });
  },
}
```
### 2. æ–°å¢ž/编辑处理单页
```html
<el-table :data="tableData" border>
  <el-table-column prop="orderNo" label="处理单编号" />
  <el-table-column prop="projectName" label="项目名称" />
  <el-table-column prop="materialName" label="物料名称" />
  <el-table-column prop="specificationModel" label="型号规格" />
  <el-table-column prop="unqualifiedQuantity" label="不合格数量" />
  <el-table-column prop="disposalMethod" label="处置方式">
    <template #default="{ row }">
      {{ disposalMethodMap[row.disposalMethod] }}
    </template>
  </el-table-column>
  <el-table-column prop="status" label="状态">
    <template #default="{ row }">
      <el-tag :type="statusTagType(row.status)">{{ statusMap[row.status] }}</el-tag>
    </template>
  </el-table-column>
  <el-table-column label="操作">
    <template #default="{ row }">
      <el-button type="text" @click="handleDetail(row.id)">详情</el-button>
      <el-button type="text" @click="handleEdit(row)">编辑</el-button>
      <el-button type="text" @click="handleDelete(row.id)">删除</el-button>
    </template>
  </el-table-column>
</el-table>
```
<template>
  <div class="app-container">
    <el-form ref="form" :model="form" :rules="rules" label-width="120px">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="关联不合格品" prop="unqualifiedId">
            <el-select v-model="form.unqualifiedId" placeholder="选择不合格品" filterable>
              <el-option v-for="item in unqualifiedList" :key="item.id"
                :label="item.productName + ' ' + item.model + ' (' + item.quantity + ')'"
                :value="item.id" />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="不合格工序" prop="unqualifiedProcess">
            <el-select v-model="form.unqualifiedProcess">
              <el-option label="来料" :value="1" />
              <el-option label="制程" :value="2" />
              <el-option label="成品" :value="3" />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
### 3. data æ•°æ®
      <el-divider>基本信息</el-divider>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="项目名称" prop="projectName">
            <el-input v-model="form.projectName" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="项目编号" prop="projectNo">
            <el-input v-model="form.projectNo" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="物料/部件名称" prop="materialName">
            <el-input v-model="form.materialName" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="物料图号" prop="materialDrawingNo">
            <el-input v-model="form.materialDrawingNo" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="8">
          <el-form-item label="型号规格" prop="specificationModel">
            <el-input v-model="form.specificationModel" />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="材质" prop="materialQuality">
            <el-input v-model="form.materialQuality" />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="供应商" prop="supplierName">
            <el-input v-model="form.supplierName" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-divider>不合格信息</el-divider>
      <el-row :gutter="20">
        <el-col :span="8">
          <el-form-item label="总数量" prop="quantity">
            <el-input-number v-model="form.quantity" :min="0" :precision="2" style="width:100%" />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="不合格数量" prop="unqualifiedQuantity">
            <el-input-number v-model="form.unqualifiedQuantity" :min="0" :precision="2" style="width:100%" />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="检验日期" prop="inspectDate">
            <el-date-picker v-model="form.inspectDate" type="date" value-format="yyyy-MM-dd" style="width:100%" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="8">
          <el-form-item label="检验员" prop="inspectorName">
            <el-input v-model="form.inspectorName" />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="责任人" prop="responsiblePerson">
            <el-input v-model="form.responsiblePerson" />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="责任部门" prop="responsibleDept">
            <el-input v-model="form.responsibleDept" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item label="问题描述" prop="problemDescription">
        <el-input v-model="form.problemDescription" type="textarea" :rows="2" />
      </el-form-item>
      <el-divider>处置决策</el-divider>
      <el-form-item label="处置方式" prop="disposalMethod">
        <el-radio-group v-model="form.disposalMethod">
          <el-radio :value="1">让步接收</el-radio>
          <el-radio :value="2">厂内维修</el-radio>
          <el-radio :value="3">返厂维修</el-radio>
          <el-radio :value="4">换货</el-radio>
          <el-radio :value="5">退货</el-radio>
          <el-radio :value="6">报废</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item v-if="form.disposalMethod === 2 || form.disposalMethod === 3" label="维修评估" prop="repairEvaluation">
        <el-input v-model="form.repairEvaluation" type="textarea" :rows="3"
          placeholder="评估维修可行性、所需工时、物料等" />
      </el-form-item>
      <el-form-item label="原因分析及建议" prop="reasonAnalysis">
        <el-input v-model="form.reasonAnalysis" type="textarea" :rows="3" />
      </el-form-item>
      <el-form-item label="纠正措施" prop="correctionAction">
        <el-input v-model="form.correctionAction" type="textarea" :rows="3" />
      </el-form-item>
      <el-form-item label="预防措施" prop="preventiveAction">
        <el-input v-model="form.preventiveAction" type="textarea" :rows="3" />
      </el-form-item>
      <el-divider>审批意见</el-divider>
      <el-form-item label="责任部门主管意见" prop="deptOpinion">
        <el-input v-model="form.deptOpinion" type="textarea" :rows="2" />
      </el-form-item>
      <el-form-item label="公司处理决定" prop="companyDecision">
        <el-input v-model="form.companyDecision" type="textarea" :rows="2" />
      </el-form-item>
      <el-form-item label="总经理意见" prop="generalManagerOpinion">
        <el-input v-model="form.generalManagerOpinion" type="textarea" :rows="2" />
      </el-form-item>
      <el-divider>附件</el-divider>
      <el-form-item label="附件">
        <file-upload v-model="form.storageBlobDTOs" />
      </el-form-item>
    </el-form>
    <div class="text-center">
      <el-button type="primary" @click="handleSubmit">提交</el-button>
      <el-button @click="handleCancel">取消</el-button>
    </div>
  </div>
</template>
```
```js
data() {
  return {
    form: {
      storageBlobDTOs: [],
      storageBlobVOs: [],
      unqualifiedId: null,
      disposalMethod: null,
    },
    query: {
      pageNum: 1,
      pageSize: 10,
      status: null,
      projectName: '',
      orderNo: '',
    rules: {
      unqualifiedId: [{ required: true, message: '请选择关联不合格品', trigger: 'change' }],
      disposalMethod: [{ required: true, message: '请选择处置方式', trigger: 'change' }],
    },
    statusMap: { 0: '草稿', 1: '待审批', 2: '审批中', 3: '已完成', 4: '已驳回' },
    disposalMethodMap: { 1: '让步接收', 2: '厂内维修', 3: '返厂维修', 4: '换货', 5: '退货', 6: '报废' },
    unqualifiedProcessMap: { 1: '来料', 2: '制程', 3: '成品' },
    unqualifiedList: [],
  }
},
mounted() {
  // åŠ è½½å¾…å¤„ç†çš„ä¸åˆæ ¼å“åˆ—è¡¨
  loadUnqualifiedList({ inspectState: 0 }).then(res => this.unqualifiedList = res.rows);
  if (this.$route.query.id) {
    getDetail(this.$route.query.id).then(res => this.form = res.data);
  }
},
methods: {
  handleSubmit() {
    this.$refs.form.validate(valid => {
      if (!valid) return;
      const api = this.form.id ? updateOrder : saveOrder;
      api(this.form).then(() => {
        this.$message.success(this.form.id ? '修改成功' : '新增成功');
        this.$router.back();
      });
    });
  },
  handleCancel() { this.$router.back(); },
}
```
### 4. API è°ƒç”¨
## å¤„置方式说明
```js
import request from '@/utils/request'
| å€¼ | å«ä¹‰ | ç³»ç»Ÿè‡ªåŠ¨è¡Œä¸º |
|----|------|------------|
| 1 | è®©æ­¥æŽ¥æ”¶ | æ— è‡ªåŠ¨æ“ä½œï¼Œéœ€äººå·¥åŽç»­å¤„ç† |
| 2 | åŽ‚å†…ç»´ä¿® | **自动创建返修生产订单**(FG å¼€å¤´ NPS ç¼–号),克隆原工序路线 |
| 3 | è¿”厂维修 | **自动创建返修生产订单**(FG å¼€å¤´ NPS ç¼–号),克隆原工序路线 |
| 4 | æ¢è´§ | æ— è‡ªåŠ¨æ“ä½œ |
| 5 | é€€è´§ | æ— è‡ªåŠ¨æ“ä½œ |
| 6 | æŠ¥åºŸ | æ— è‡ªåŠ¨æ“ä½œï¼ˆåº“å­˜æ‰£å‡ç”±æ—§æ¨¡å— `/quality/qualityUnqualified/deal` å¤„理) |
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({ url: '/qualityUnqualifiedOrder/listPage', method: 'get', params: query })
}
## çŠ¶æ€æµè½¬
// è¯¦æƒ…
export function getDetail(id) {
  return request({ url: `/qualityUnqualifiedOrder/${id}`, method: 'get' })
}
// æ–°å¢ž
export function save(data) {
  return request({ url: '/qualityUnqualifiedOrder/save', method: 'post', data })
}
// ä¿®æ”¹
export function update(data) {
  return request({ url: '/qualityUnqualifiedOrder/update', method: 'put', data })
}
// åˆ é™¤
export function remove(ids) {
  return request({ url: '/qualityUnqualifiedOrder/delete', method: 'delete', data: ids })
}
```
质检自动创建 â”€â”€> 0(草稿) â”€â”€> è°ƒç”¨ /deal é€‰æ‹©å¤„置方式 â”€â”€> 3(已完成)
手动新增(没选处置方式) â”€â”€> 0(草稿) â”€â”€> è°ƒç”¨ /deal â”€â”€> 3(已完成)
手动新增(选了处置方式) â”€â”€> 3(已完成)
```
## æ³¨æ„äº‹é¡¹
- å¤„理单编号 `orderNo` ç”±åŽç«¯è‡ªåŠ¨ç”Ÿæˆï¼ˆå‰ç¼€ "BHG" + æ—¥æœŸ + 3位自增序号),前端无需传入
- æ–°å¢žæ—¶ `status` é»˜è®¤ä¸º 0(草稿),无需前端设置
- åˆ é™¤ä¸ºé€»è¾‘删除,通过 `deleted` å­—段标记
- é™„件上传使用系统已有的文件上传组件,将返回的临时文件ID通过 `storageBlobDTOs` ä¼ å…¥
- ä¸åˆæ ¼å“å¤„理单可关联 `quality_unqualified` è¡¨çš„记录(通过 `unqualifiedId` å­—段)
- æ£€éªŒå•提交时,不合格数量 > 0 ä¼šè‡ªåŠ¨åˆ›å»ºå¤„ç†å•ï¼ˆçŠ¶æ€ä¸ºè‰ç¨¿ï¼‰ï¼Œ**无需手动新增**
- æ‰‹åŠ¨æ–°å¢žå¤„ç†å•æ—¶ï¼Œé€‰äº†å¤„ç½®æ–¹å¼ç›´æŽ¥å®Œæˆï¼Œæ²¡é€‰åˆ™ä¿æŒè‰ç¨¿
- è‰ç¨¿çŠ¶æ€çš„å¤„ç†å•é€šè¿‡ `/qualityUnqualifiedOrder/deal` æŽ¥å£è¡¥å……处置方式
- æ—§çš„不合格管理(`/quality/qualityUnqualified`)继续用于不合格品首次记录和列表查看
- å¤„置操作统一使用本模块(`/qualityUnqualifiedOrder`)
- é€‰æ‹©"厂内维修"或"返厂维修"时,系统自动创建返修生产订单,无需手动操作
- è¿”修生产订单的 `disposalMethod` å­—段标记了处置方式,可在生产订单列表区分普通订单和返修订单
- å¤„理单编号自动生成,前缀为 `BHG`
- é™„件通过 `storageBlobDTOs` å­—段上传,查询时返回 `storageBlobVOs`
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java
@@ -421,6 +421,9 @@
        Map<String, BigDecimal> monthPayableMap = new HashMap<>();
        // æœˆåº¦åº”收(销售出库-退货)
        salesOutboundVos.forEach(item -> {
            if (item.getShippingDate() == null) {
                return;
            }
            String month = item.getShippingDate().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getOutboundAmount()).orElse(BigDecimal.ZERO)));
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
@@ -615,10 +615,10 @@
        map.put("id", item.getId());
        map.put("workOrderNo", safe(item.getWorkOrderNo()));
        map.put("productionOrderId", item.getProductionOrderId());
        map.put("planStartTime", formatDate(item.getPlanStartTime()));
        map.put("planEndTime", formatDate(item.getPlanEndTime()));
        map.put("actualStartTime", formatDate(item.getActualStartTime()));
        map.put("actualEndTime", formatDate(item.getActualEndTime()));
        map.put("planStartTime", formatDateTime(item.getPlanStartTime()));
        map.put("planEndTime", formatDateTime(item.getPlanEndTime()));
        map.put("actualStartTime", formatDateTime(item.getActualStartTime()));
        map.put("actualEndTime", formatDateTime(item.getActualEndTime()));
        map.put("planQuantity", item.getPlanQuantity());
        map.put("completeQuantity", item.getCompleteQuantity());
        map.put("status", item.getStatus());
src/main/java/com/ruoyi/basic/dto/ProductModelDto.java
@@ -1,14 +1,10 @@
package com.ruoyi.basic.dto;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.production.bean.dto.ProductStructureDto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class ProductModelDto extends ProductModel {
    private List<ProductStructureDto> productStructureList;
}
src/main/java/com/ruoyi/home/dto/ProductionTaskStatisticsDto.java
@@ -4,7 +4,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class ProductionTaskStatisticsDto {
@@ -16,16 +16,16 @@
    private String workOrderNo;
    @Schema(description = "计划开始时间")
    private LocalDate planStartTime;
    private LocalDateTime planStartTime;
    @Schema(description = "计划结束时间")
    private LocalDate planEndTime;
    private LocalDateTime planEndTime;
    @Schema(description = "实际开始时间")
    private LocalDate actualStartTime;
    private LocalDateTime actualStartTime;
    @Schema(description = "实际结束时间")
    private LocalDate actualEndTime;
    private LocalDateTime actualEndTime;
    @Schema(description = "计划数量")
    private BigDecimal planQuantity;
src/main/java/com/ruoyi/production/bean/dto/BomImportDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/dto/ProductStructureDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/dto/ProductionBomStructureDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -47,4 +47,10 @@
    @Schema(description = "是否生产")
    private Integer isProduction;
    @Schema(description = "是否按当前登录用户过滤权限")
    private Boolean filterByCurrentUser;
    @Schema(description = "当前用户ID,用于SQL权限过滤")
    private Long currentUserId;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickRecordDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java
@@ -1,5 +1,6 @@
package com.ruoyi.production.bean.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -71,4 +72,32 @@
    @Schema(description = "工序参数列表")
    private List<ProductionOrderRoutingOperationParam> productionOperationParamList;
    @Schema(description = "当前用户ID,用于权限过滤")
    private Long currentUserId;
    @Override
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    public void setId(Long id) {
        super.setId(id);
    }
    @Override
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    public Long getId() {
        return super.getId();
    }
    @Schema(description = "开始报工记录ID(结束报工时传入)")
    private Long startRecordId;
    /**
     * èŽ·å–ç»“æŸæŠ¥å·¥æ‰€éœ€çš„å¼€å§‹è®°å½•ID,优先取 startRecordId,其次取继承的 id
     */
    public Long resolveStartRecordId() {
        if (startRecordId != null) {
            return startRecordId;
        }
        return super.getId();
    }
}
src/main/java/com/ruoyi/production/bean/vo/ProductionBomStructureVo.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickVo.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java
@@ -35,9 +35,6 @@
    @Schema(description = "产品图片")
    private List<StorageBlobVO> productImages;
    @Schema(description = "bom编号")
    private String bomNo;
    @Schema(description = "完成进度")
    @Excel(name = "完成进度",sort = 7)
    private BigDecimal completionStatus;
src/main/java/com/ruoyi/production/controller/ProductionBomStructureController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java
@@ -1,6 +1,7 @@
package com.ruoyi.production.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
@@ -24,14 +25,24 @@
    @GetMapping("/page")
    @Operation(summary = "分页查询")
    public R page(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.pageProductionOperationTask(page, dto));
    public R page(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto,
                  @RequestParam(required = false) Boolean filterMine) {
        Long currentUserId = null;
        if (Boolean.TRUE.equals(filterMine)) {
            currentUserId = SecurityUtils.getUserId();
        }
        return R.ok(productionOperationTaskService.pageProductionOperationTask(page, dto, currentUserId));
    }
    @GetMapping("/list")
    @Operation(summary = "工单列表")
    public R<List<ProductionOperationTaskVo>> list(ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.listProductionOperationTask(dto));
    public R<List<ProductionOperationTaskVo>> list(ProductionOperationTaskDto dto,
                                                   @RequestParam(required = false) Boolean filterMine) {
        Long currentUserId = null;
        if (Boolean.TRUE.equals(filterMine)) {
            currentUserId = SecurityUtils.getUserId();
        }
        return R.ok(productionOperationTaskService.listProductionOperationTask(dto, currentUserId));
    }
    @GetMapping("/{id}")
src/main/java/com/ruoyi/production/controller/ProductionOrderBomController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
@@ -8,7 +8,6 @@
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
@@ -84,12 +83,6 @@
    @Operation(summary = "生产订单查询来源")
    public R<List<ProductionPlanVo>> getSource(@PathVariable Long id) {
        return R.ok(productionOrderService.getSource(id));
    }
    @GetMapping("/pick/{productionOrderId}")
    @Operation(summary = "根据订单id查询bom领料单")
    public R<List<ProductionOrderPickVo>> pick(@PathVariable Long productionOrderId) {
        return R.ok(productionOrderService.pick(productionOrderId));
    }
    @GetMapping("/ordeDetail")
src/main/java/com/ruoyi/production/controller/ProductionOrderPickController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java
@@ -56,6 +56,17 @@
        return R.ok(productionProductMainService.addProductMain(productionProductMainDto));
    }
    /**
     * å¼€å§‹æŠ¥å·¥
     * @param productionProductMainDto
     * @return
     */
    @PostMapping("/startWork")
    @PreAuthorize("@ss.hasPermi('productionProductMain:add')")
    public R startWork(@RequestBody ProductionProductMainDto productionProductMainDto) {
        return R.ok(productionProductMainService.startWork(productionProductMainDto));
    }
    @PostMapping
    @Operation(summary = "新增生产报工")
    public R add(@RequestBody ProductionProductMainDto productionProductMainDto) {
src/main/java/com/ruoyi/production/mapper/ProductionBomStructureMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/mapper/ProductionOrderBomMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/pojo/ProductionBomStructure.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/pojo/ProductionOperationTask.java
@@ -1,12 +1,13 @@
package com.ruoyi.production.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -43,16 +44,24 @@
    private String workOrderNo;
    @Schema(description = "计划开始时间")
    private LocalDate planStartTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime planStartTime;
    @Schema(description = "计划结束时间")
    private LocalDate planEndTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime planEndTime;
    @Schema(description = "实际开始时间")
    private LocalDate actualStartTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualStartTime;
    @Schema(description = "实际结束时间")
    private LocalDate actualEndTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualEndTime;
    @Schema(description = "状态 1 å¾…确认 2 å¾…生产 3生产中 4已生产")
    private Integer status;
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java
@@ -87,4 +87,7 @@
    @Schema(description = "是否结束)")
    @TableField("is_end_order")
    private Boolean endOrder;
    @Schema(description = "处置方式(不合格品处理单回传,2=厂内维修,3=返厂维修)")
    private Integer disposalMethod;
}
src/main/java/com/ruoyi/production/pojo/ProductionOrderBom.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/pojo/ProductionOrderRouting.java
@@ -48,12 +48,6 @@
    @Schema(description = "工艺路线编码")
    private String processRouteCode;
    @Schema(description = "基础bom的id")
    private Long bomId;
    @Schema(description = "订单bom的id")
    private Long orderBomId;
    @Schema(description = "创建人ID")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
src/main/java/com/ruoyi/production/pojo/ProductionProductMain.java
@@ -60,4 +60,14 @@
    @Schema(description = "工时")
    private BigDecimal workHour;
    @Schema(description = "实际开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualStartTime;
    @Schema(description = "实际结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualEndTime;
}
src/main/java/com/ruoyi/production/service/ProductionBomStructureService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java
@@ -13,9 +13,11 @@
public interface ProductionOperationTaskService extends IService<ProductionOperationTask> {
    IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page,
                                                                 ProductionOperationTaskDto productionOperationTaskDto);
                                                                 ProductionOperationTaskDto productionOperationTaskDto,
                                                                 Long currentUserId);
    List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto productionOperationTaskDto);
    List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto productionOperationTaskDto,
                                                                Long currentUserId);
    ProductionOperationTaskVo getProductionOperationTaskInfo(Long id);
src/main/java/com/ruoyi/production/service/ProductionOrderBomService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/ProductionOrderPickService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/ProductionOrderService.java
@@ -4,7 +4,6 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
@@ -29,8 +28,6 @@
    Object bindingRoute(ProductionOrderDto productionOrderDto);
    List<ProductionPlanVo> getSource(Long id);
    List<ProductionOrderPickVo> pick(Long productionOrderId);
    ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(ProductionOrderDto productionOrderDto);
src/main/java/com/ruoyi/production/service/ProductionProductMainService.java
@@ -18,6 +18,8 @@
    Boolean addProductMain(ProductionProductMainDto productionProductMainDto);
    Boolean startWork(ProductionProductMainDto productionProductMainDto);
    Boolean saveProductionProductMain(ProductionProductMainDto productionProductMainDto);
    Boolean removeProductMain(Long id);
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -60,9 +60,12 @@
    private String tempDir;
    @Override
    public IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
    public IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto, Long currentUserId) {
        // åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§å·¥åºä»»åŠ¡
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        if (dto != null && currentUserId != null) {
            dto.setCurrentUserId(currentUserId);
        }
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillOperationTypes(result.getRecords());
        fillUserNames(result.getRecords());
@@ -70,8 +73,11 @@
    }
    @Override
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto, Long currentUserId) {
        // æŸ¥è¯¢å·¥åºä»»åŠ¡åˆ—è¡¨
        if (dto != null && currentUserId != null) {
            dto.setCurrentUserId(currentUserId);
        }
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        fillOperationTypes(result);
        fillUserNames(result);
src/main/java/com/ruoyi/production/service/impl/ProductionOrderBomServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
@@ -7,7 +7,6 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.util.TaskPlanQuantityUtil;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductionOrderRoutingOperationService;
@@ -37,8 +36,6 @@
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingMapper productionOrderRoutingMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    @Override
@@ -137,37 +134,9 @@
                    throw new ServiceException("生产订单不存在");
                }
                // èŽ·å–è®¢å•BOM
                ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                        Wrappers.<ProductionOrderBom>lambdaQuery()
                                .eq(ProductionOrderBom::getProductionOrderId, productionOrder.getId()));
                // ç¡®å®šæ ¹äº§å“è§„æ ¼ID
                Long rootProductModelId = orderBom != null && orderBom.getProductModelId() != null
                        ? orderBom.getProductModelId()
                        : productionOrder.getProductModelId();
                // èŽ·å–BOM结构列表
                List<ProductionBomStructure> orderBomStructureList = orderBom == null || orderBom.getId() == null
                        ? Collections.emptyList()
                        : productionBomStructureMapper.selectList(
                        Wrappers.<ProductionBomStructure>lambdaQuery()
                                .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                                .orderByAsc(ProductionBomStructure::getId));
                // æž„建工序需求量映射
                Map<String, BigDecimal> operationDemandedQuantityMap =
                        TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
                // èŽ·å–å·¥è‰ºè·¯çº¿å·¥åºï¼ˆç”¨äºŽè®¡ç®—è®¡åˆ’æ•°é‡ï¼‰
                TechnologyRoutingOperation sourceOperation = technologyRoutingOperationMapper.selectById(
                        updatedOperation.getTechnologyRoutingOperationId());
                // å°†åŽŸæ¥çš„ç§æœ‰æ–¹æ³•æ›¿æ¢ä¸ºè°ƒç”¨å·¥å…·ç±»
                BigDecimal planQuantity = TaskPlanQuantityUtil.resolveTaskPlanQuantity(
                        sourceOperation,
                        operationDemandedQuantityMap,
                        productionOrder,
                        rootProductModelId);
                BigDecimal planQuantity = productionOrder.getQuantity() != null
                        ? productionOrder.getQuantity()
                        : BigDecimal.ZERO;
                task.setPlanQuantity(planQuantity);
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -16,9 +16,7 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
@@ -34,8 +32,6 @@
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.technology.pojo.*;
import lombok.RequiredArgsConstructor;
@@ -58,18 +54,13 @@
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductionOrderPickMapper productionOrderPickMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    private final QualityInspectFileMapper qualityInspectFileMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private final StorageBlobMapper storageBlobMapper;
    private final SalesLedgerMapper salesLedgerMapper;
@@ -78,8 +69,6 @@
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyRoutingOperationParamMapper technologyRoutingOperationParamMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final TechnologyBomMapper technologyBomMapper;
    private final TechnologyBomStructureMapper technologyBomStructureMapper;
    private final FileUtil fileUtil;
    @Override
@@ -212,9 +201,7 @@
    @Override
    public int syncProductionOrderSnapshot(Long productionOrderId) {
        // åŒæ­¥è®¢å•工艺、工序、参数和BOM快照
        ProductionOrder productionOrder = this.getById(productionOrderId);
        // å‚数与前置条件校验
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        }
@@ -225,42 +212,24 @@
        if (technologyRouting == null) {
            throw new ServiceException("工艺路线不存在");
        }
        // è®¢å•快照按“先清后建”处理,保证工艺路线、工序、参数、BOM å…¨éƒ¨æ¥è‡ªåŒä¸€ç‰ˆæœ¬ã€‚
        clearProductionSnapshot(productionOrderId);
        ProductionOrderBom orderBom = syncProductionOrderBomSnapshot(productionOrder, technologyRouting);
        //生产订单工艺路线表
        ProductionOrderRouting orderRouting = new ProductionOrderRouting();
        orderRouting.setProductionOrderId(productionOrder.getId());
        orderRouting.setTechnologyRoutingId(technologyRouting.getId());
        orderRouting.setProductModelId(technologyRouting.getProductModelId());
        orderRouting.setProcessRouteCode(technologyRouting.getProcessRouteCode());
        orderRouting.setDescription(technologyRouting.getDescription());
        orderRouting.setBomId(technologyRouting.getBomId());
        orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId());
        productionOrderRoutingMapper.insert(orderRouting);
        int syncedParamCount = 0;
        // æŸ¥è¯¢å¹¶å‡†å¤‡ä¸šåŠ¡æ•°æ®
        List<TechnologyRoutingOperation> routingOperations = technologyRoutingOperationMapper.selectList(
                Wrappers.<TechnologyRoutingOperation>lambdaQuery()
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
                        .orderByDesc(TechnologyRoutingOperation::getId));
        // Build task plan quantities from order BOM snapshot demand instead of recomputing from technology BOM units.
        Long rootProductModelId = orderBom != null && orderBom.getProductModelId() != null
                ? orderBom.getProductModelId()
                : productionOrder.getProductModelId();
        List<ProductionBomStructure> orderBomStructureList = orderBom == null || orderBom.getId() == null
                ? Collections.emptyList()
                : productionBomStructureMapper.selectList(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByAsc(ProductionBomStructure::getId));
        Map<String, BigDecimal> operationDemandedQuantityMap =
                buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
        BigDecimal orderQuantity = defaultDecimal(productionOrder.getQuantity());
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
                        routingOperations.stream()
                                .map(TechnologyRoutingOperation::getTechnologyOperationId)
                                .filter(Objects::nonNull)
@@ -273,7 +242,6 @@
                .max(Integer::compareTo)
                .orElse(null);
        for (TechnologyRoutingOperation sourceOperation : routingOperations) {
            // è®¢å•工序保存的是工艺工序快照,后续报工只依赖快照,不再直接引用工艺主数据。
            ProductionOrderRoutingOperation targetOperation = new ProductionOrderRoutingOperation();
            targetOperation.setProductionOrderId(productionOrder.getId());
            targetOperation.setTechnologyRoutingOperationId(sourceOperation.getId());
@@ -292,11 +260,7 @@
                ProductionOperationTask task = new ProductionOperationTask();
                task.setProductionOrderRoutingOperationId(targetOperation.getId());
                task.setProductionOrderId(productionOrder.getId());
                task.setPlanQuantity(resolveTaskPlanQuantity(
                        sourceOperation,
                        operationDemandedQuantityMap,
                        productionOrder,
                        rootProductModelId));
                task.setPlanQuantity(orderQuantity);
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
                task.setStatus(2);
@@ -308,7 +272,6 @@
                            .eq(TechnologyRoutingOperationParam::getTechnologyRoutingOperationId, sourceOperation.getId())
                            .orderByAsc(TechnologyRoutingOperationParam::getId));
            for (TechnologyRoutingOperationParam sourceParam : sourceParams) {
                // å·¥åºæ‰§è¡Œå‚数同样做快照,避免工艺参数调整影响已下达订单。
                ProductionOrderRoutingOperationParam targetParam = new ProductionOrderRoutingOperationParam();
                targetParam.setProductionOrderId(productionOrder.getId());
                targetParam.setProductionOrderRoutingOperationId(targetOperation.getId());
@@ -331,150 +294,12 @@
        return syncedParamCount;
    }
    private Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> bomStructures,
                                                                      Long rootProductModelId) {
        if (bomStructures == null || bomStructures.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProductionBomStructure> structureById = bomStructures.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
        Set<String> mergedOutputNodeKeySet = new HashSet<>();
        for (ProductionBomStructure bomStructure : bomStructures) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            // The BOM row points to the producing operation; task quantity should come from that operation's output node.
            ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
            Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
            if (outputProductModelId == null) {
                continue;
            }
            String mergedOutputNodeKey = buildOperationOutputNodeKey(
                    bomStructure.getTechnologyOperationId(),
                    outputNode == null ? null : outputNode.getId(),
                    outputProductModelId);
            if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
                continue;
            }
            // demandedQuantity is already the order-level required output quantity for the current output node.
            BigDecimal demandedQuantity = defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity());
            String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
            demandedQuantityMap.merge(key, demandedQuantity, BigDecimal::add);
        }
        return demandedQuantityMap;
    }
    private BigDecimal resolveTaskPlanQuantity(TechnologyRoutingOperation sourceOperation,
                                               Map<String, BigDecimal> operationDemandedQuantityMap,
                                               ProductionOrder productionOrder,
                                               Long rootProductModelId) {
        if (sourceOperation == null || operationDemandedQuantityMap == null || operationDemandedQuantityMap.isEmpty()) {
            return defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
        }
        Long outputProductModelId = sourceOperation.getProductModelId() != null
                ? sourceOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(sourceOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = operationDemandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
    }
    private String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
    private ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
                                                              Map<Long, ProductionBomStructure> structureById) {
        if (bomStructure == null) {
            return null;
        }
        // The root node is the first output node; child rows use their direct parent as the current operation output.
        if (bomStructure.getParentId() == null) {
            return bomStructure;
        }
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    private Long resolveOutputProductModelId(ProductionBomStructure outputNode,
                                             Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    private ProductionOrderBom syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
        // åŒæ­¥è®¢å•BOM快照结构
        if (technologyRouting.getBomId() == null) {
            return null;
        }
        TechnologyBom technologyBom = technologyBomMapper.selectById(technologyRouting.getBomId());
        if (technologyBom == null) {
            throw new ServiceException("工艺BOM不存在");
        }
        // æŸ¥è¯¢å¹¶å‡†å¤‡ä¸šåŠ¡æ•°æ®
        List<TechnologyBomStructure> structureList = technologyBomStructureMapper.selectList(
                Wrappers.<TechnologyBomStructure>lambdaQuery()
                        .eq(TechnologyBomStructure::getBomId, technologyBom.getId())
                        .orderByAsc(TechnologyBomStructure::getId));
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
        TechnologyBomStructure root = structureList.stream().filter(item -> item.getParentId() == null).findFirst().orElse(null);
        BigDecimal orderQuantity = defaultDecimal(productionOrder.getQuantity());
        ProductionOrderBom orderBom = new ProductionOrderBom();
        orderBom.setProductionOrderId(productionOrder.getId());
        orderBom.setBomId(Long.valueOf(technologyBom.getId()));
        orderBom.setProductModelId(root != null ? root.getProductModelId() : productionOrder.getProductModelId());
        orderBom.setRemark(technologyBom.getRemark());
        orderBom.setBomNo(technologyBom.getBomNo());
        orderBom.setVersion(technologyBom.getVersion());
        // æŒä¹…化或输出处理结果
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        for (TechnologyBomStructure source : structureList) {
            // å­èŠ‚ç‚¹ parentId éœ€è¦æ˜ å°„成新快照节点 id,才能保留原始 BOM å±‚级。
            ProductionBomStructure target = new ProductionBomStructure();
            target.setProductionOrderId(productionOrder.getId());
            target.setProductionOrderBomId(orderBom.getId());
            target.setParentId(source.getParentId() == null ? null : idMap.get(source.getParentId()));
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity()));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
            lastProcessDemandedQuantity = target.getDemandedQuantity();
        }
        return orderBom;
    }
    private void clearProductionSnapshot(Long productionOrderId) {
        // æ¸…理订单已生成的工艺与BOM快照数据
        boolean hasPickRecord = productionOrderPickRecordMapper.selectCount(
        // æŸ¥è¯¢å¹¶å‡†å¤‡ä¸šåŠ¡æ•°æ®
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)) > 0;
        // å‚数与前置条件校验
        if (hasPickRecord) {
            throw new ServiceException("生产订单已存在领料记录,不能重新生成快照");
        }
        List<Long> taskIds = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
                                .eq(ProductionOperationTask::getProductionOrderId, productionOrderId))
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
                .stream().map(ProductionOperationTask::getId).collect(Collectors.toList());
        if (!taskIds.isEmpty()) {
            // å·²æœ‰æŠ¥å·¥è®°å½•说明订单已开工,此时不允许再重建快照。
            boolean started = productionProductMainMapper.selectCount(
                    Wrappers.<ProductionProductMain>lambdaQuery()
                            .in(ProductionProductMain::getProductionOperationTaskId, taskIds)) > 0;
@@ -490,12 +315,6 @@
                .eq(ProductionOrderRoutingOperation::getProductionOrderId, productionOrderId));
        productionOrderRoutingMapper.delete(Wrappers.<ProductionOrderRouting>lambdaQuery()
                .eq(ProductionOrderRouting::getProductionOrderId, productionOrderId));
        productionBomStructureMapper.delete(Wrappers.<ProductionBomStructure>lambdaQuery()
                .eq(ProductionBomStructure::getProductionOrderId, productionOrderId));
        productionOrderBomMapper.delete(Wrappers.<ProductionOrderBom>lambdaQuery()
                .eq(ProductionOrderBom::getProductionOrderId, productionOrderId));
        productionOrderPickMapper.delete(Wrappers.<ProductionOrderPick>lambdaQuery()
                .eq(ProductionOrderPick::getProductionOrderId, productionOrderId));
    }
    private LambdaQueryWrapper<ProductionOrder> buildQueryWrapper(ProductionOrderDto dto) {
@@ -1013,122 +832,6 @@
            throw new ServiceException("生产订单不存在");
        }
        return productionOrder.getId();
    }
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId) {
        // æŸ¥è¯¢è®¢å•领料、投料与退料明细
        if (productionOrderId == null) {
            return Collections.emptyList();
        }
        // æŸ¥è¯¢å¹¶å‡†å¤‡ä¸šåŠ¡æ•°æ®
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderBom::getId)
                        .last("limit 1"));
        if (orderBom == null || orderBom.getId() == null) {
            return Collections.emptyList();
        }
        // æŸ¥è¯¢å®Œæ•´çš„BOM结构(包括根节点),用于计算层级需求数量
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.listByBomId(orderBom.getId());
        if (bomStructureList == null || bomStructureList.isEmpty()) {
            return Collections.emptyList();
        }
        // æŸ¥è¯¢ç”Ÿäº§è®¢å•获取订单数量
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
        BigDecimal orderQuantity = productionOrder != null ? defaultDecimal(productionOrder.getQuantity()) : BigDecimal.ZERO;
        // æž„建树形结构并计算层级需求数量
        Map<Long, ProductionBomStructureVo> structureByIdMap = bomStructureList.stream()
                .filter(s -> s != null && s.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructureVo::getId, s -> s));
        // æŒ‰å±‚级计算需求数量:子级需求数量 = çˆ¶çº§éœ€æ±‚数量 Ã— å­çº§å•位产出所需数量
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null) continue;
            if (structure.getParentId() == null || structure.getParentId() == 0) {
                // æ ¹èŠ‚ç‚¹ï¼šéœ€æ±‚æ•°é‡ = è®¢å•数量
                structure.setDemandedQuantity(orderQuantity);
            } else {
                // å­èŠ‚ç‚¹ï¼šéœ€æ±‚æ•°é‡ = çˆ¶çº§éœ€æ±‚数量 Ã— å­çº§å•位产出所需数量
                ProductionBomStructureVo parent = structureByIdMap.get(structure.getParentId());
                if (parent != null) {
                    BigDecimal parentDemandedQty = defaultDecimal(parent.getDemandedQuantity());
                    BigDecimal unitQuantity = defaultDecimal(structure.getUnitQuantity());
                    structure.setDemandedQuantity(parentDemandedQty.multiply(unitQuantity));
                }
            }
        }
        // è¿‡æ»¤å‡ºéžæ ¹èŠ‚ç‚¹ï¼ˆå®žé™…é¢†æ–™é¡¹ï¼‰
        // æŽ’除投入品与产出品相同且比例为1的情况(自身加工,不需要领料)
        List<ProductionBomStructureVo> childStructureList = bomStructureList.stream()
                .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                .filter(s -> {
                    ProductionBomStructureVo parent = structureByIdMap.get(s.getParentId());
                    if (parent == null) {
                        return true;
                    }
                    // æŠ•入品与产出品相同且比例为1时,不需要领料
                    boolean sameProduct = Objects.equals(s.getProductModelId(), parent.getProductModelId());
                    boolean unitRatio = BigDecimal.ONE.compareTo(defaultDecimal(s.getUnitQuantity())) == 0;
                    return !(sameProduct && unitRatio);
                })
                .collect(Collectors.toList());
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
        List<Long> productModelIds = childStructureList.stream()
                .map(ProductionBomStructureVo::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, BigDecimal> stockQuantityMap = new HashMap<>();
        Map<Long, LinkedHashSet<String>> stockBatchNoMap = new HashMap<>();
        if (!productModelIds.isEmpty()) {
            List<StockInventory> stockList = stockInventoryMapper.selectList(
                    Wrappers.<StockInventory>lambdaQuery()
                            .in(StockInventory::getProductModelId, productModelIds));
            for (StockInventory stockItem : stockList) {
                if (stockItem == null || stockItem.getProductModelId() == null) {
                    continue;
                }
                Long productModelId = stockItem.getProductModelId();
                stockQuantityMap.merge(productModelId, defaultDecimal(stockItem.getQualitity()), BigDecimal::add);
                String batchNo = stockItem.getBatchNo();
                if (batchNo != null && !batchNo.trim().isEmpty()) {
                    stockBatchNoMap.computeIfAbsent(productModelId, key -> new LinkedHashSet<>()).add(batchNo);
                }
            }
        }
        List<ProductionOrderPickVo> pickList = new ArrayList<>();
        for (ProductionBomStructureVo structure : childStructureList) {
            if (structure == null || structure.getProductModelId() == null) {
                continue;
            }
            Long productModelId = structure.getProductModelId();
            ProductionOrderPickVo vo = new ProductionOrderPickVo();
            vo.setProductModelId(productModelId);
            vo.setOperationName(structure.getOperationName());
            vo.setTechnologyOperationId(structure.getTechnologyOperationId());
            vo.setProductName(structure.getProductName());
            vo.setModel(structure.getModel());
            vo.setDemandedQuantity(defaultDecimal(structure.getDemandedQuantity()));
            vo.setUnit(structure.getUnit());
            List<String> batchNoList = stockBatchNoMap.get(productModelId) == null
                    ? Collections.emptyList()
                    : new ArrayList<>(stockBatchNoMap.get(productModelId));
            vo.setBatchNoList(batchNoList);
            vo.setStockQuantity(stockQuantityMap.getOrDefault(productModelId, BigDecimal.ZERO));
            vo.setBom(true);
            pickList.add(vo);
        }
        return pickList;
    }
    @Override
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -63,7 +63,7 @@
     * 1. ä»…允许同一产品型号的计划合并;
     * 2. å·²ä¸‹å‘或部分下发的计划不允许再次合并;
     * 3. ä¸‹å‘数量不能大于所选计划剩余需求总量;
     * 4. ä¸‹å‘时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     * 4. ä¸‹å‘时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺逻辑一致。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.production.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -14,9 +15,9 @@
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.bean.dto.ProductStructureDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
@@ -39,13 +40,14 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -72,8 +74,6 @@
    private final ProductionAccountMapper productionAccountMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
@@ -219,106 +219,179 @@
    @Override
    public Boolean addProductMain(ProductionProductMainDto dto) {
        // æ–°å¢žç”Ÿäº§æŠ¥å·¥ä¸»è®°å½•
        // å…¼å®¹æ—§æµç¨‹ï¼šå¦‚果没有开始报工记录ID,走一步报工(自动创建+结束)
        Long startRecordId = dto.resolveStartRecordId();
        if (startRecordId == null) {
            return oneStepWork(dto);
        }
        dto.setId(startRecordId);
        return finishWork(dto);
    }
    @Override
    public Boolean startWork(ProductionProductMainDto dto) {
        // å¼€å§‹æŠ¥å·¥ï¼šåˆ›å»ºæŠ¥å·¥è®°å½•,标记实际开始时间
        Long taskId = resolveTaskId(dto);
        if (taskId == null) {
            throw new ServiceException("请传入生产工单ID");
        }
        return addProductMainByProductionTask(dto);
    }
    @Override
    public Boolean saveProductionProductMain(ProductionProductMainDto productionProductMainDto) {
        // ä¿å­˜ç”Ÿäº§æŠ¥å·¥ä¸»è®°å½•
        return addProductMain(productionProductMainDto);
    }
    @Override
    public Boolean removeProductMain(Long id) {
        // åˆ é™¤ç”Ÿäº§æŠ¥å·¥ä¸»è®°å½•
        ProductionProductMain currentMain = productionProductMainMapper.selectById(id);
        if (currentMain == null) {
            return true;
        }
        return removeProductMainByProductionTask(currentMain);
    }
    private Boolean addProductMainByProductionTask(ProductionProductMainDto dto) {
        // æŒ‰ç”Ÿäº§ä»»åŠ¡æ–°å¢žæŠ¥å·¥ä¸»è®°å½•
        Long taskId = resolveTaskId(dto);
        if (taskId == null) {
            throw new ServiceException("生产工单ID不能为空");
        }
        SysUser user = userMapper.selectUserById(dto.getUserId());
        ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectById(taskId);
        if (productionOperationTask == null) {
        ProductionOperationTask task = productionOperationTaskMapper.selectById(taskId);
        if (task == null) {
            throw new ServiceException("生产工单不存在");
        }
        ProductionOrderRoutingOperation routingOperation = productionOrderRoutingOperationMapper.selectById(productionOperationTask.getProductionOrderRoutingOperationId());
        if (routingOperation == null) {
            throw new ServiceException("订单工艺路线工序不存在");
        // æƒé™æ ¡éªŒï¼šå·²æŒ‡æ´¾æ—¶ä»…被指派人可操作
        validateWorkerPermission(task);
        if (task.getStatus() != null && task.getStatus() != 2 && task.getStatus() != 3) {
            throw new ServiceException("当前工单状态不允许开始报工,仅待生产或生产中状态的工单可操作");
        }
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOperationTask.getProductionOrderId());
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        SysUser user = userMapper.selectUserById(dto.getUserId());
        ProductionProductMain main = new ProductionProductMain();
        main.setProductNo(generateProductNo());
        main.setUserId(user == null ? dto.getUserId() : user.getUserId());
        main.setUserName(user == null ? dto.getUserName() : user.getNickName());
        main.setProductionOperationTaskId(taskId);
        main.setStatus(0);
        main.setActualStartTime(LocalDateTime.now());
        productionProductMainMapper.insert(main);
        // å·¥å•状态 -> ç”Ÿäº§ä¸­
        task.setStatus(3);
        if (task.getActualStartTime() == null) {
            task.setActualStartTime(LocalDateTime.now());
        }
        TechnologyRoutingOperation technologyRoutingOperation = technologyRoutingOperationMapper.selectById(routingOperation.getTechnologyRoutingOperationId());
        TechnologyOperation technologyOperation = technologyRoutingOperation == null ? null
                : technologyOperationMapper.selectById(technologyRoutingOperation.getTechnologyOperationId());
        ProductModel productModel = productModelMapper.selectById(
                routingOperation.getProductModelId() != null ? routingOperation.getProductModelId() : productionOrder.getProductModelId());
        productionOperationTaskMapper.updateById(task);
        // ç”Ÿäº§è®¢å• -> ç”Ÿäº§è¿›è¡Œä¸­
        ProductionOrder productionOrder = productionOrderMapper.selectById(task.getProductionOrderId());
        if (productionOrder != null && productionOrder.getStartTime() == null) {
            productionOrder.setStartTime(LocalDateTime.now());
            productionOrder.setStatus(ProductOrderStatusEnum.RUNNING.getCode());
            productionOrderMapper.updateById(productionOrder);
        }
        return true;
    }
    private Boolean oneStepWork(ProductionProductMainDto dto) {
        // å…¼å®¹æ—§ä¸€æ­¥æŠ¥å·¥æµç¨‹ï¼šè‡ªåŠ¨åˆ›å»ºå¼€å§‹è®°å½•å¹¶ç«‹å³ç»“æŸ
        Long taskId = resolveTaskId(dto);
        if (taskId == null) {
            throw new ServiceException("请传入生产工单ID");
        }
        ProductionOperationTask task = productionOperationTaskMapper.selectById(taskId);
        if (task == null) {
            throw new ServiceException("生产工单不存在");
        }
        validateWorkerPermission(task);
        if (task.getStatus() != null && task.getStatus() != 2 && task.getStatus() != 3) {
            throw new ServiceException("当前工单状态不允许报工");
        }
        SysUser user = userMapper.selectUserById(dto.getUserId());
        ProductionProductMain main = new ProductionProductMain();
        main.setProductNo(generateProductNo());
        main.setUserId(user == null ? dto.getUserId() : user.getUserId());
        main.setUserName(user == null ? dto.getUserName() : user.getNickName());
        main.setProductionOperationTaskId(taskId);
        main.setStatus(0);
        main.setActualStartTime(LocalDateTime.now());
        productionProductMainMapper.insert(main);
        if (task.getActualStartTime() == null) {
            task.setActualStartTime(LocalDateTime.now());
        }
        task.setStatus(3);
        productionOperationTaskMapper.updateById(task);
        ProductionOrder productionOrder = productionOrderMapper.selectById(task.getProductionOrderId());
        if (productionOrder != null && productionOrder.getStartTime() == null) {
            productionOrder.setStartTime(LocalDateTime.now());
            productionOrder.setStatus(ProductOrderStatusEnum.RUNNING.getCode());
            productionOrderMapper.updateById(productionOrder);
        }
        dto.setId(main.getId());
        return finishWork(dto);
    }
    private Boolean finishWork(ProductionProductMainDto dto) {
        // ç»“束报工:更新开始报工记录,创建产出、投入品、核算记录
        ProductionProductMain currentMain = productionProductMainMapper.selectById(dto.getId());
        if (currentMain == null) {
            throw new ServiceException("开始报工记录不存在");
        }
        if (currentMain.getStatus() == null || currentMain.getStatus() != 0) {
            throw new ServiceException("该报工记录已结束或状态异常");
        }
        Long taskId = currentMain.getProductionOperationTaskId();
        ProductionOperationTask task = productionOperationTaskMapper.selectById(taskId);
        if (task == null) {
            throw new ServiceException("生产工单不存在");
        }
        validateWorkerPermission(task);
        // æ›´æ–°æŠ¥å·¥è®°å½•为已结束,记录结束时间
        currentMain.setActualEndTime(LocalDateTime.now());
        currentMain.setStatus(1);
        if (dto.getWorkHour() != null) {
            currentMain.setWorkHour(dto.getWorkHour());
        } else if (currentMain.getActualStartTime() != null) {
            // æ ¹æ®å¼€å§‹/结束时间计算实际工时(小时)
            long seconds = Duration.between(currentMain.getActualStartTime(), currentMain.getActualEndTime()).getSeconds();
            currentMain.setWorkHour(BigDecimal.valueOf(seconds).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP));
        }
        productionProductMainMapper.updateById(currentMain);
        // åŒæ­¥å·¥åºå‚æ•°
        ProductionOrderRoutingOperation routingOperation = productionOrderRoutingOperationMapper.selectById(task.getProductionOrderRoutingOperationId());
        if (routingOperation != null) {
            syncOperationParamInputValue(dto, routingOperation.getId(), currentMain.getId());
        }
        // èŽ·å–äº§å“è§„æ ¼
        ProductModel productModel = null;
        if (routingOperation != null) {
            productModel = productModelMapper.selectById(
                    routingOperation.getProductModelId() != null ? routingOperation.getProductModelId() : getProductionOrderProductModelId(task));
        }
        if (productModel == null) {
            throw new ServiceException("产品规格不存在");
        }
        ProductionProductMain productionProductMain = new ProductionProductMain();
        productionProductMain.setProductNo(generateProductNo());
        productionProductMain.setUserId(user == null ? dto.getUserId() : user.getUserId());
        productionProductMain.setUserName(user == null ? dto.getUserName() : user.getNickName());
        productionProductMain.setProductionOperationTaskId(taskId);
        productionProductMain.setStatus(0);
        productionProductMain.setWorkHour(dto.getWorkHour());
        productionProductMainMapper.insert(productionProductMain);
        syncOperationParamInputValue(dto, routingOperation.getId(), productionProductMain.getId());
        // æŠ•入品
        ProductionProductInput productionProductInput = new ProductionProductInput();
        productionProductInput.setProductionProductMainId(currentMain.getId());
        productionProductInput.setProductModelId(productModel.getId());
        productionProductInput.setInputQuantity(defaultDecimal(dto.getQuantity()));
        productionProductInput.setQuantity(productionProductInput.getInputQuantity());
        productionProductInputMapper.insert(productionProductInput);
        List<ProductStructureDto> productStructureDtos = resolveInputStructures(
                productionOrder.getId(), routingOperation, productModel.getId());
       // å¦‚果没有bom子节点了,那么投入就是他本身
        if (productStructureDtos.isEmpty()) {
            ProductStructureDto fallbackInput = new ProductStructureDto();
            fallbackInput.setProductModelId(productModel.getId());
            fallbackInput.setUnitQuantity(BigDecimal.ONE);
            productStructureDtos.add(fallbackInput);
        }
        for (ProductStructureDto item : productStructureDtos) {
            // å½“前实现按工序成品直接作为投入,后续若接入领料记录可在这里替换来源。
            ProductionProductInput productionProductInput = new ProductionProductInput();
            productionProductInput.setProductionProductMainId(productionProductMain.getId());
            productionProductInput.setProductModelId(item.getProductModelId());
            productionProductInput.setInputQuantity(item.getUnitQuantity().multiply(defaultDecimal(dto.getQuantity())));
            productionProductInput.setQuantity(productionProductInput.getInputQuantity());
            productionProductInputMapper.insert(productionProductInput);
        }
        // äº§å‡ºå“
        ProductionProductOutput productionProductOutput = new ProductionProductOutput();
        productionProductOutput.setProductionProductMainId(productionProductMain.getId());
        productionProductOutput.setProductionProductMainId(currentMain.getId());
        productionProductOutput.setProductModelId(productModel.getId());
        productionProductOutput.setQuantity(defaultDecimal(dto.getQuantity()));
        productionProductOutput.setScrapQty(defaultDecimal(dto.getScrapQty()));
        productionProductOutputMapper.insert(productionProductOutput);
        BigDecimal reportQty = defaultDecimal(productionProductOutput.getQuantity());
        BigDecimal scrapQty = defaultDecimal(productionProductOutput.getScrapQty());
        BigDecimal productQty = reportQty;
        String qualifiedBatchNo = null;
        List<ProductionOrderRoutingOperation> routingOperationList = productionOrderRoutingOperationMapper.selectList(
        TechnologyRoutingOperation technologyRoutingOperation = routingOperation != null
                ? technologyRoutingOperationMapper.selectById(routingOperation.getTechnologyRoutingOperationId()) : null;
        TechnologyOperation technologyOperation = technologyRoutingOperation == null ? null
                : technologyOperationMapper.selectById(technologyRoutingOperation.getTechnologyOperationId());
        List<ProductionOrderRoutingOperation> routingOperationList = routingOperation != null ? productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
                        .eq(ProductionOrderRoutingOperation::getOrderRoutingId, routingOperation.getOrderRoutingId())
                        .eq(ProductionOrderRoutingOperation::getProductionOrderId, routingOperation.getProductionOrderId()));
        boolean isLastOperation = routingOperation.getDragSort() != null && routingOperation.getDragSort().equals(routingOperationList.size());
                        .eq(ProductionOrderRoutingOperation::getProductionOrderId, routingOperation.getProductionOrderId())) : new ArrayList<>();
        boolean isLastOperation = routingOperation != null && routingOperation.getDragSort() != null
                && routingOperation.getDragSort().equals(routingOperationList.size());
        if (productQty.compareTo(BigDecimal.ZERO) > 0) {
            if (Boolean.TRUE.equals(routingOperation.getIsQuality())) {
                // è´¨æ£€å·¥åºå…ˆç”Ÿæˆæ£€éªŒå•,非质检工序直接入合格品库存。
            if (routingOperation != null && Boolean.TRUE.equals(routingOperation.getIsQuality())) {
                int inspectType = isLastOperation ? 2 : 1;
                String process = isLastOperation ? null : technologyOperation == null ? null : technologyOperation.getName();
                Product product = productMapper.selectById(productModel.getProductId());
@@ -331,7 +404,7 @@
                qualityInspect.setProcess(process);
                qualityInspect.setInspectState(0);
                qualityInspect.setInspectType(inspectType);
                qualityInspect.setProductMainId(productionProductMain.getId());
                qualityInspect.setProductMainId(currentMain.getId());
                qualityInspect.setProductModelId(productModel.getId());
                qualityInspectMapper.insert(qualityInspect);
                List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(product.getId(), inspectType, process);
@@ -350,45 +423,46 @@
                }
            } else {
                StockInventoryDto stockInventoryDto = new StockInventoryDto();
                stockInventoryDto.setRecordId(productionProductMain.getId());
                stockInventoryDto.setRecordId(currentMain.getId());
                stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode()));
                stockInventoryDto.setQualitity(productQty);
                stockInventoryDto.setProductModelId(productModel.getId());
                stockInventoryService.addStockInRecordOnly(stockInventoryDto);
                qualifiedBatchNo = resolveLatestStockInBatchNo(
                        productionProductMain.getId(),
                        currentMain.getId(),
                        StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(),
                        productModel.getId(),
                        "0");
            }
            productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).add(productQty));
            if (ObjectUtils.isNull(productionOperationTask.getActualStartTime())) {
                productionOperationTask.setActualStartTime(LocalDate.now());
            task.setCompleteQuantity(defaultDecimal(task.getCompleteQuantity()).add(productQty));
            if (task.getActualStartTime() == null) {
                task.setActualStartTime(LocalDateTime.now());
            }
            // æŠ¥å·¥é©±åŠ¨å·¥å•çŠ¶æ€æµè½¬ï¼šæœ‰äº§å‡ºå³è¿›è¡Œä¸­ï¼Œè¾¾åˆ°è®¡åˆ’é‡å³å®Œå·¥ã€‚
            productionOperationTask.setStatus(3);
            if (productionOperationTask.getPlanQuantity() != null
                    && productionOperationTask.getCompleteQuantity().compareTo(productionOperationTask.getPlanQuantity()) >= 0) {
                productionOperationTask.setActualEndTime(LocalDate.now());
                productionOperationTask.setStatus(4);
            task.setStatus(3);
            if (task.getPlanQuantity() != null
                    && task.getCompleteQuantity().compareTo(task.getPlanQuantity()) >= 0) {
                task.setActualEndTime(LocalDateTime.now());
                task.setStatus(4);
            }
            productionOperationTaskMapper.updateById(productionOperationTask);
            productionOperationTaskMapper.updateById(task);
            if (ObjectUtils.isNull(productionOrder.getStartTime())) {
                productionOrder.setStartTime(LocalDateTime.now());
            }
            // è®¢å•状态由最后一道工序的合格产出推动,避免中间工序提前完工。
            productionOrder.setStatus(ProductOrderStatusEnum.RUNNING.getCode());
            if (isLastOperation) {
                productionOrder.setCompleteQuantity(defaultDecimal(productionOrder.getCompleteQuantity()).add(productQty));
                if (productionOrder.getQuantity() != null
                        && productionOrder.getCompleteQuantity().compareTo(productionOrder.getQuantity()) >= 0) {
                    productionOrder.setEndTime(LocalDateTime.now());
                    productionOrder.setStatus(ProductOrderStatusEnum.FINISHED.getCode());
            ProductionOrder productionOrder = productionOrderMapper.selectById(task.getProductionOrderId());
            if (productionOrder != null) {
                if (productionOrder.getStartTime() == null) {
                    productionOrder.setStartTime(LocalDateTime.now());
                }
                productionOrder.setStatus(ProductOrderStatusEnum.RUNNING.getCode());
                if (isLastOperation) {
                    productionOrder.setCompleteQuantity(defaultDecimal(productionOrder.getCompleteQuantity()).add(productQty));
                    if (productionOrder.getQuantity() != null
                            && productionOrder.getCompleteQuantity().compareTo(productionOrder.getQuantity()) >= 0) {
                        productionOrder.setEndTime(LocalDateTime.now());
                        productionOrder.setStatus(ProductOrderStatusEnum.FINISHED.getCode());
                    }
                }
                productionOrderMapper.updateById(productionOrder);
            }
            productionOrderMapper.updateById(productionOrder);
            BigDecimal workHours = BigDecimal.ZERO;
            if (technologyOperation != null && technologyOperation.getSalaryQuota() != null) {
@@ -397,13 +471,14 @@
                        : technologyOperation.getSalaryQuota();
            }
            ProductionAccount productionAccount = new ProductionAccount();
            productionAccount.setProductionProductMainId(productionProductMain.getId());
            productionAccount.setSchedulingUserId(user == null ? null : user.getUserId());
            productionAccount.setSchedulingUserName(user == null ? dto.getUserName() : user.getNickName());
            productionAccount.setProductionProductMainId(currentMain.getId());
            SysUser user = userMapper.selectUserById(dto.getUserId());
            productionAccount.setSchedulingUserId(user == null ? currentMain.getUserId() : user.getUserId());
            productionAccount.setSchedulingUserName(user == null ? currentMain.getUserName() : user.getNickName());
            productionAccount.setFinishedNum(productQty);
            productionAccount.setWorkHours(technologyOperation != null ? technologyOperation.getSalaryQuota() : null);
            productionAccount.setTechnologyOperationName(technologyOperation == null ? null : technologyOperation.getName());
            productionAccount.setSchedulingDate(LocalDateTime.now());
            productionAccount.setSchedulingDate(currentMain.getActualEndTime() != null ? currentMain.getActualEndTime() : LocalDateTime.now());
            productionAccountMapper.insert(productionAccount);
        }
        if (scrapQty.compareTo(BigDecimal.ZERO) > 0) {
@@ -411,10 +486,54 @@
                    productModel.getId(),
                    scrapQty,
                    StockInQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(),
                    productionProductMain.getId(),
                    currentMain.getId(),
                    qualifiedBatchNo);
        }
        return true;
    }
    private Long getProductionOrderProductModelId(ProductionOperationTask task) {
        // ä»Žç”Ÿäº§è®¢å•获取产品规格ID
        if (task == null || task.getProductionOrderId() == null) {
            return null;
        }
        ProductionOrder order = productionOrderMapper.selectById(task.getProductionOrderId());
        return order == null ? null : order.getProductModelId();
    }
    private void validateWorkerPermission(ProductionOperationTask task) {
        // æ ¡éªŒå½“前用户是否有权操作此工单:未指派时人人可操作,已指派时仅被指派人可操作
        if (task == null) {
            return;
        }
        String userIds = task.getUserIds();
        if (userIds == null || userIds.isEmpty() || "[]".equals(userIds.trim())) {
            return;
        }
        Long currentUserId = SecurityUtils.getUserId();
        if (currentUserId == null) {
            return;
        }
        List<Long> assignedIds = JSON.parseArray(userIds, Long.class);
        if (assignedIds == null || !assignedIds.contains(currentUserId)) {
            throw new ServiceException("您未被指派到此工单,无法操作");
        }
    }
    @Override
    public Boolean saveProductionProductMain(ProductionProductMainDto productionProductMainDto) {
        // ä¿å­˜ç”Ÿäº§æŠ¥å·¥ä¸»è®°å½•
        return addProductMain(productionProductMainDto);
    }
    @Override
    public Boolean removeProductMain(Long id) {
        // åˆ é™¤ç”Ÿäº§æŠ¥å·¥ä¸»è®°å½•
        ProductionProductMain currentMain = productionProductMainMapper.selectById(id);
        if (currentMain == null) {
            return true;
        }
        return removeProductMainByProductionTask(currentMain);
    }
    private String resolveLatestStockInBatchNo(Long recordId,
@@ -502,64 +621,6 @@
        target.setProductionOrderRoutingOperationId(source.getProductionOrderRoutingOperationId());
        target.setProductionProductMainId(productionProductMainId);
        return target;
    }
    private List<ProductStructureDto> resolveInputStructures(Long productionOrderId,
                                                             ProductionOrderRoutingOperation routingOperation,
                                                             Long outputProductModelId) {
        if (productionOrderId == null || routingOperation == null || routingOperation.getTechnologyOperationId() == null) {
            return new ArrayList<>();
        }
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderBom::getId)
                        .last("limit 1"));
        if (orderBom == null || orderBom.getId() == null) {
            return new ArrayList<>();
        }
        List<ProductionBomStructure> bomNodeList = productionBomStructureMapper.selectList(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByAsc(ProductionBomStructure::getId));
        if (bomNodeList.isEmpty()) {
            return new ArrayList<>();
        }
        Map<Long, ProductionBomStructure> nodeMap = bomNodeList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Long currentOutputModelId = routingOperation.getProductModelId() != null
                ? routingOperation.getProductModelId()
                : outputProductModelId;
        Map<Long, BigDecimal> unitQtyByProductModel = new LinkedHashMap<>();
        for (ProductionBomStructure node : bomNodeList) {
            if (node == null || node.getParentId() == null || node.getProductModelId() == null) {
                continue;
            }
            if (!Objects.equals(node.getTechnologyOperationId(), routingOperation.getTechnologyOperationId())) {
                continue;
            }
            ProductionBomStructure parent = nodeMap.get(node.getParentId());
            if (parent == null || !Objects.equals(parent.getProductModelId(), currentOutputModelId)) {
                continue;
            }
            unitQtyByProductModel.merge(node.getProductModelId(), defaultDecimal(node.getUnitQuantity()), BigDecimal::add);
        }
        List<ProductStructureDto> result = new ArrayList<>();
        for (Map.Entry<Long, BigDecimal> entry : unitQtyByProductModel.entrySet()) {
            if (entry.getValue().compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            ProductStructureDto item = new ProductStructureDto();
            item.setProductModelId(entry.getKey());
            item.setUnitQuantity(entry.getValue());
            result.add(item);
        }
        return result;
    }
    private Boolean removeProductMainByProductionTask(ProductionProductMain productionProductMain) {
src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java
@@ -1,131 +1,14 @@
package com.ruoyi.production.util;
import com.ruoyi.production.pojo.ProductionBomStructure;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.technology.pojo.TechnologyRoutingOperation;
import lombok.experimental.UtilityClass;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
 * å·¥å•计划数量计算工具类
 */
@UtilityClass
public class TaskPlanQuantityUtil {
    /**
     * è®¡ç®—工单计划数量(使用 TechnologyRoutingOperation)
     */
    public BigDecimal resolveTaskPlanQuantity(TechnologyRoutingOperation sourceOperation,
                                              Map<String, BigDecimal> operationDemandedQuantityMap,
                                              ProductionOrder productionOrder,
                                              Long rootProductModelId) {
        if (sourceOperation == null || operationDemandedQuantityMap == null || operationDemandedQuantityMap.isEmpty()) {
            return defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
        }
        Long outputProductModelId = sourceOperation.getProductModelId() != null
                ? sourceOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(sourceOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = operationDemandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
    }
    /**
     * è®¡ç®—工单计划数量(使用 ProductionOrderRoutingOperation)
     */
    public BigDecimal resolveTaskPlanQuantity(ProductionOrderRoutingOperation routingOperation,
                                              Map<String, BigDecimal> demandedQuantityMap,
                                              BigDecimal orderQuantity,
                                              Long rootProductModelId) {
        if (routingOperation == null || demandedQuantityMap == null || demandedQuantityMap.isEmpty()) {
            return orderQuantity;
        }
        Long outputProductModelId = routingOperation.getProductModelId() != null
                ? routingOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(routingOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = demandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : orderQuantity;
    }
    /**
     * æž„建工序需求量映射表
     */
    public Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> bomStructures, Long rootProductModelId) {
        if (bomStructures == null || bomStructures.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProductionBomStructure> structureById = new HashMap<>();
        for (ProductionBomStructure item : bomStructures) {
            if (item != null && item.getId() != null) {
                structureById.put(item.getId(), item);
            }
        }
        Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
        Set<String> mergedOutputNodeKeySet = new HashSet<>();
        for (ProductionBomStructure bomStructure : bomStructures) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
            Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
            if (outputProductModelId == null) {
                continue;
            }
            String mergedOutputNodeKey = buildOperationOutputNodeKey(bomStructure.getTechnologyOperationId(),
                    outputNode == null ? null : outputNode.getId(), outputProductModelId);
            if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
                continue;
            }
            BigDecimal demandedQuantity = defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity());
            String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
            demandedQuantityMap.merge(key, demandedQuantity, BigDecimal::add);
        }
        return demandedQuantityMap;
    }
    /**
     * æž„建工序需求量key
     */
    public String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    /**
     * æž„建输出节点key
     */
    public String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
    /**
     * è§£æžå·¥åºè¾“出节点
     */
    public ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
                                                             Map<Long, ProductionBomStructure> structureById) {
        if (bomStructure == null) {
            return null;
        }
        if (bomStructure.getParentId() == null) {
            return bomStructure;
        }
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    /**
     * è§£æžè¾“出产品规格ID
     */
    public Long resolveOutputProductModelId(ProductionBomStructure outputNode, Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    /**
     * é»˜è®¤BigDecimal值
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedOrderController.java
@@ -8,10 +8,12 @@
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.mapper.QualityUnqualifiedOrderMapper;
import com.ruoyi.quality.pojo.QualityUnqualifiedOrder;
import com.ruoyi.quality.service.IQualityUnqualifiedOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
@@ -26,6 +28,8 @@
    @Resource
    private IQualityUnqualifiedOrderService orderService;
    @Resource
    private QualityUnqualifiedOrderMapper orderMapper;
    @Resource
    private FileUtil fileUtil;
    @PostMapping("/save")
@@ -33,29 +37,35 @@
    @Log(title = "新增不合格品处理单", businessType = BusinessType.INSERT)
    public R<?> save(@RequestBody QualityUnqualifiedOrder order) {
        String orderNo = OrderUtils.countTodayByCreateTime(
                orderService.getBaseMapper(), "BHG", "order_no",
                orderMapper, "BHG", "order_no",
                order.getCreateTime() != null ? order.getCreateTime() : LocalDateTime.now());
        order.setOrderNo(orderNo);
        order.setStatus(0);
        orderService.save(order);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.QUALITY_UNQUALIFIED_ORDER, order.getId(), order.getStorageBlobDTOs());
        return R.ok(true);
        if (order.getStatus() == null) {
            order.setStatus(order.getDisposalMethod() != null ? 3 : 0);
        }
        boolean result = orderService.save(order);
        if (result) {
            fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.QUALITY_UNQUALIFIED_ORDER, order.getId(), order.getStorageBlobDTOs());
        }
        return R.ok(result);
    }
    @PutMapping("/update")
    @Operation(summary = "修改不合格品处理单")
    @Log(title = "修改不合格品处理单", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityUnqualifiedOrder order) {
        orderService.updateById(order);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.QUALITY_UNQUALIFIED_ORDER, order.getId(), order.getStorageBlobDTOs());
        return R.ok(true);
        boolean result = orderService.updateWithRework(order);
        if (result) {
            fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.QUALITY_UNQUALIFIED_ORDER, order.getId(), order.getStorageBlobDTOs());
        }
        return R.ok(result);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除不合格品处理单")
    @Log(title = "删除不合格品处理单", businessType = BusinessType.DELETE)
    public R<?> delete(@RequestBody List<Long> ids) {
        return R.ok(orderService.removeBatchByIds(ids));
        return R.ok(orderMapper.deleteByIds(ids) > 0);
    }
    @GetMapping("/listPage")
@@ -71,4 +81,18 @@
    public R<?> detail(@PathVariable Long id) {
        return R.ok(orderService.getDetail(id));
    }
    @PostMapping("/deal")
    @Operation(summary = "不合格品处理")
    @Log(title = "不合格品处理", businessType = BusinessType.OTHER)
    public R<?> deal(@RequestBody QualityUnqualifiedOrder order) {
        return R.ok(orderService.deal(order));
    }
    @GetMapping("/export/{id}")
    @Operation(summary = "导出不合格品处理单")
    @Log(title = "导出不合格品处理单", businessType = BusinessType.EXPORT)
    public void export(@PathVariable Long id, HttpServletResponse response) {
        orderService.export(id, response);
    }
}
src/main/java/com/ruoyi/quality/mapper/QualityUnqualifiedOrderMapper.java
@@ -7,8 +7,12 @@
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface QualityUnqualifiedOrderMapper extends BaseMapper<QualityUnqualifiedOrder> {
    IPage<QualityUnqualifiedOrder> listPage(Page page, @Param("query") QualityUnqualifiedOrder query);
    int deleteByIds(@Param("ids") List<Long> ids);
}
src/main/java/com/ruoyi/quality/pojo/QualityUnqualifiedOrder.java
@@ -112,6 +112,15 @@
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "责任部门主管意见")
    private String deptOpinion;
    @Schema(description = "公司处理决定")
    private String companyDecision;
    @Schema(description = "总经理意见")
    private String generalManagerOpinion;
    @Schema(description = "创建用户")
    @TableField(value = "create_by", fill = FieldFill.INSERT)
    private Integer createBy;
@@ -136,7 +145,6 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableLogic
    @TableField(fill = FieldFill.INSERT)
    private Integer deleted;
src/main/java/com/ruoyi/quality/service/IQualityUnqualifiedOrderService.java
@@ -4,10 +4,19 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.quality.pojo.QualityUnqualifiedOrder;
import jakarta.servlet.http.HttpServletResponse;
public interface IQualityUnqualifiedOrderService extends IService<QualityUnqualifiedOrder> {
    IPage<QualityUnqualifiedOrder> listPage(Page page, QualityUnqualifiedOrder query);
    QualityUnqualifiedOrder getDetail(Long id);
    boolean save(QualityUnqualifiedOrder order);
    boolean deal(QualityUnqualifiedOrder order);
    boolean updateWithRework(QualityUnqualifiedOrder order);
    void export(Long id, HttpServletResponse response);
}
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -16,6 +16,7 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
@@ -24,9 +25,11 @@
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedOrderMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.quality.pojo.QualityUnqualifiedOrder;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.quality.service.IQualityInspectParamService;
@@ -67,6 +70,8 @@
    private QualityTestStandardMapper qualityTestStandardMapper;
    private QualityUnqualifiedMapper qualityUnqualifiedMapper;
    private QualityUnqualifiedOrderMapper qualityUnqualifiedOrderMapper;
    private SalesLedgerProductMapper salesLedgerProductMapper;
@@ -161,6 +166,26 @@
            qualityUnqualified.setDefectivePhenomena(text + "这些指标中存在不合格");//不合格现象
            qualityUnqualified.setInspectId(qualityInspect.getId());
            qualityUnqualifiedMapper.insert(qualityUnqualified);
            // è‡ªåŠ¨åˆ›å»ºä¸åˆæ ¼å“å¤„ç†å•ï¼Œæµå…¥æ–°æ¨¡å—
            QualityUnqualifiedOrder order = new QualityUnqualifiedOrder();
            order.setUnqualifiedId(qualityUnqualified.getId());
            order.setUnqualifiedProcess(qualityInspect.getInspectType() + 1);
            order.setUnqualifiedQuantity(qualityInspect.getUnqualifiedQuantity());
            order.setQuantity(qualityInspect.getQuantity());
            order.setProductModelId(qualityInspect.getProductModelId());
            order.setInspectorName(qualityInspect.getCheckName());
            if (qualityInspect.getCheckTime() != null) {
                order.setInspectDate(new java.util.Date(qualityInspect.getCheckTime().getTime()));
            }
            order.setSupplierName(qualityInspect.getSupplier());
            order.setSpecificationModel(qualityInspect.getModel());
            order.setProblemDescription(qualityInspect.getDefectivePhenomena());
            order.setStatus(0);
            String orderNo = OrderUtils.countTodayByCreateTime(
                    qualityUnqualifiedOrderMapper, "BHG", "order_no", LocalDateTime.now());
            order.setOrderNo(orderNo);
            qualityUnqualifiedOrderMapper.insert(order);
        }
        qualityInspect.setInspectState(1);//已提交
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedOrderServiceImpl.java
@@ -1,15 +1,48 @@
package com.ruoyi.quality.service.impl;
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;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOrderRouting;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedOrderMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.quality.pojo.QualityUnqualifiedOrder;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.quality.service.IQualityUnqualifiedOrderService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@AllArgsConstructor
@Service
@@ -17,6 +50,13 @@
    private final QualityUnqualifiedOrderMapper orderMapper;
    private final FileUtil fileUtil;
    private final QualityUnqualifiedMapper qualityUnqualifiedMapper;
    private final IQualityInspectService qualityInspectService;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingMapper productionOrderRoutingMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    @Override
    public IPage<QualityUnqualifiedOrder> listPage(Page page, QualityUnqualifiedOrder query) {
@@ -31,4 +71,277 @@
        }
        return order;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean save(QualityUnqualifiedOrder order) {
        boolean result = super.save(order);
        // å¤„置方式包含"ç»´ä¿®"(2=厂内维修, 3=返厂维修)时,自动创建返修生产订单
        if (order.getDisposalMethod() != null && (order.getDisposalMethod() == 2 || order.getDisposalMethod() == 3)) {
            createReworkProductionOrder(order);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deal(QualityUnqualifiedOrder order) {
        QualityUnqualifiedOrder existing = orderMapper.selectById(order.getId());
        if (existing == null) {
            return false;
        }
        existing.setDisposalMethod(order.getDisposalMethod());
        existing.setRepairEvaluation(order.getRepairEvaluation());
        existing.setReasonAnalysis(order.getReasonAnalysis());
        existing.setCorrectionAction(order.getCorrectionAction());
        existing.setPreventiveAction(order.getPreventiveAction());
        existing.setRemark(order.getRemark());
        existing.setDeptOpinion(order.getDeptOpinion());
        existing.setCompanyDecision(order.getCompanyDecision());
        existing.setGeneralManagerOpinion(order.getGeneralManagerOpinion());
        existing.setStatus(3);
        boolean result = updateById(existing);
        if (result && existing.getDisposalMethod() != null
                && (existing.getDisposalMethod() == 2 || existing.getDisposalMethod() == 3)) {
            createReworkProductionOrder(existing);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateWithRework(QualityUnqualifiedOrder order) {
        boolean result = updateById(order);
        if (result && order.getDisposalMethod() != null
                && (order.getDisposalMethod() == 2 || order.getDisposalMethod() == 3)) {
            createReworkProductionOrder(order);
        }
        return result;
    }
    @Override
    public void export(Long id, HttpServletResponse response) {
        QualityUnqualifiedOrder order = getDetail(id);
        if (order == null) {
            throw new ServiceException("处理单不存在");
        }
        try (InputStream is = this.getClass().getResourceAsStream("/static/不合格品处理单.xls");
             POIFSFileSystem fs = new POIFSFileSystem(is);
             HSSFWorkbook wb = new HSSFWorkbook(fs)) {
            Sheet sheet = wb.getSheetAt(0);
            setCellValue(sheet, 2, 1, order.getProjectName());
            setCellValue(sheet, 2, 5, order.getProjectNo());
            setCellValue(sheet, 3, 1, order.getEquipmentName());
            setCellValue(sheet, 3, 5, order.getEquipmentDrawingNo());
            setCellValue(sheet, 4, 1, order.getMaterialName());
            setCellValue(sheet, 4, 5, order.getMaterialDrawingNo());
            setCellValue(sheet, 5, 1, order.getSpecificationModel());
            setCellValue(sheet, 5, 3, order.getMaterialQuality());
            setCellValue(sheet, 5, 5, order.getQuantity());
            setCellValue(sheet, 5, 7, order.getUnqualifiedQuantity());
            // ä¸åˆæ ¼å·¥åº - åªæ›¿æ¢å¯¹åº”的□为√
            if (order.getUnqualifiedProcess() != null) {
                String origin = "□来料    â–¡åˆ¶ç¨‹   â–¡æˆå“";
                int idx = order.getUnqualifiedProcess() - 1;
                if (idx >= 0 && idx < 3) {
                    origin = origin.replaceFirst("□", "√");
                    if (idx >= 1) origin = origin.replaceFirst("□", idx == 1 ? "√" : "□");
                    if (idx >= 2) origin = origin.replaceFirst("□", "√");
                }
                // æŒ‰ç´¢å¼•依次替换
                StringBuilder sb = new StringBuilder();
                int found = 0;
                for (char c : "□来料    â–¡åˆ¶ç¨‹   â–¡æˆå“".toCharArray()) {
                    if (c == '□') {
                        sb.append(found == idx ? '☑' : '□');
                        found++;
                    } else {
                        sb.append(c);
                    }
                }
                setCellValue(sheet, 6, 1, sb.toString());
            }
            setCellValue(sheet, 6, 5, order.getSupplierName());
            setCellValue(sheet, 7, 1, order.getInspectorName());
            setCellValue(sheet, 7, 3, order.getInspectDate());
            setCellValue(sheet, 7, 5, order.getResponsiblePerson());
            setCellValue(sheet, 7, 7, order.getResponsibleDept());
            setCellValue(sheet, 8, 1, order.getProblemDescription());
            setCellValue(sheet, 9, 1, order.getReasonAnalysis());
            setCellValue(sheet, 10, 1, order.getCorrectionAction());
            // å¤„置方式 - åªæ›¿æ¢å¯¹åº”的□为√
            if (order.getDisposalMethod() != null) {
                String template = "□让步接收  â–¡åŽ‚å†…ç»´ä¿®  â–¡è¿”厂维修  â–¡æ¢è´§  â–¡é€€è´§  â–¡æŠ¥åºŸ";
                int idx = order.getDisposalMethod() - 1;
                StringBuilder sb = new StringBuilder();
                int found = 0;
                for (char c : template.toCharArray()) {
                    if (c == '□') {
                        sb.append(found == idx ? '☑' : '□');
                        found++;
                    } else {
                        sb.append(c);
                    }
                }
                setCellValue(sheet, 11, 1, sb.toString());
            }
            setCellValue(sheet, 12, 1, order.getRepairEvaluation());
            setCellValue(sheet, 13, 1, order.getPreventiveAction());
            setCellValue(sheet, 14, 1, order.getDeptOpinion());
            setCellValue(sheet, 15, 1, order.getCompanyDecision());
            setCellValue(sheet, 16, 1, order.getGeneralManagerOpinion());
            response.setContentType("application/vnd.ms-excel");
            String fileName = URLEncoder.encode("不合格品处理单_" + order.getOrderNo(), "UTF-8");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xls");
            OutputStream os = response.getOutputStream();
            wb.write(os);
            os.flush();
            os.close();
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    private void setCellValue(Sheet sheet, int rowIdx, int colIdx, Object value) {
        if (value == null) return;
        Row row = sheet.getRow(rowIdx);
        if (row == null) row = sheet.createRow(rowIdx);
        Cell cell = row.getCell(colIdx);
        if (cell == null) cell = row.createCell(colIdx);
        if (value instanceof Date) {
            cell.setCellValue((Date) value);
        } else if (value instanceof Number) {
            cell.setCellValue(((Number) value).doubleValue());
        } else {
            cell.setCellValue(value.toString());
        }
    }
    private void createReworkProductionOrder(QualityUnqualifiedOrder order) {
        if (order.getUnqualifiedId() == null) {
            return;
        }
        QualityUnqualified unqualified = qualityUnqualifiedMapper.selectById(order.getUnqualifiedId());
        if (unqualified == null || unqualified.getInspectId() == null) {
            return;
        }
        QualityInspect qualityInspect = qualityInspectService.getById(unqualified.getInspectId());
        if (qualityInspect == null || qualityInspect.getProductMainId() == null) {
            return;
        }
        ProductionProductMain sourceMain = productionProductMainMapper.selectById(qualityInspect.getProductMainId());
        if (sourceMain == null || sourceMain.getProductionOperationTaskId() == null) {
            return;
        }
        ProductionOperationTask sourceTask = productionOperationTaskMapper.selectById(sourceMain.getProductionOperationTaskId());
        if (sourceTask == null) {
            return;
        }
        ProductionOrder sourceOrder = productionOrderMapper.selectById(sourceTask.getProductionOrderId());
        if (sourceOrder == null) {
            return;
        }
        BigDecimal reworkQty = order.getUnqualifiedQuantity() != null ? order.getUnqualifiedQuantity()
                : unqualified.getQuantity() != null ? unqualified.getQuantity() : BigDecimal.ONE;
        ProductionOrder newOrder = new ProductionOrder();
        BeanUtils.copyProperties(sourceOrder, newOrder);
        newOrder.setId(null);
        newOrder.setNpsNo(generateNextProductionOrderNo("FG"));
        newOrder.setQuantity(reworkQty);
        newOrder.setCompleteQuantity(BigDecimal.ZERO);
        newOrder.setStartTime(null);
        newOrder.setEndTime(null);
        newOrder.setCreateTime(null);
        newOrder.setUpdateTime(null);
        newOrder.setDisposalMethod(order.getDisposalMethod());
        productionOrderMapper.insert(newOrder);
        Map<Long, Long> routingIdMap = new HashMap<>();
        List<ProductionOrderRouting> sourceRoutings = productionOrderRoutingMapper.selectList(
                Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, sourceOrder.getId())
                        .orderByAsc(ProductionOrderRouting::getId));
        for (ProductionOrderRouting sourceRouting : sourceRoutings) {
            ProductionOrderRouting newRouting = new ProductionOrderRouting();
            BeanUtils.copyProperties(sourceRouting, newRouting);
            newRouting.setId(null);
            newRouting.setProductionOrderId(newOrder.getId());
            newRouting.setCreateTime(null);
            newRouting.setUpdateTime(null);
            productionOrderRoutingMapper.insert(newRouting);
            routingIdMap.put(sourceRouting.getId(), newRouting.getId());
        }
        List<ProductionOrderRoutingOperation> sourceOperations = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
                        .eq(ProductionOrderRoutingOperation::getProductionOrderId, sourceOrder.getId())
                        .orderByAsc(ProductionOrderRoutingOperation::getDragSort)
                        .orderByAsc(ProductionOrderRoutingOperation::getId));
        for (ProductionOrderRoutingOperation sourceOperation : sourceOperations) {
            ProductionOrderRoutingOperation newOperation = new ProductionOrderRoutingOperation();
            BeanUtils.copyProperties(sourceOperation, newOperation);
            newOperation.setId(null);
            newOperation.setProductionOrderId(newOrder.getId());
            newOperation.setOrderRoutingId(routingIdMap.get(sourceOperation.getOrderRoutingId()));
            newOperation.setCreateTime(null);
            newOperation.setUpdateTime(null);
            productionOrderRoutingOperationMapper.insert(newOperation);
            ProductionOperationTask newTask = new ProductionOperationTask();
            newTask.setProductionOrderRoutingOperationId(newOperation.getId());
            newTask.setProductionOrderId(newOrder.getId());
            newTask.setPlanQuantity(newOrder.getQuantity());
            newTask.setCompleteQuantity(BigDecimal.ZERO);
            newTask.setWorkOrderNo(generateNextTaskNo("FG"));
            newTask.setStatus(1);
            productionOperationTaskMapper.insert(newTask);
        }
    }
    private String generateNextProductionOrderNo(String prefix) {
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String orderPrefix = prefix + datePrefix;
        ProductionOrder latestOrder = productionOrderMapper.selectOne(
                Wrappers.<ProductionOrder>lambdaQuery()
                        .likeRight(ProductionOrder::getNpsNo, orderPrefix)
                        .orderByDesc(ProductionOrder::getNpsNo)
                        .last("limit 1"));
        int sequence = 1;
        if (latestOrder != null && latestOrder.getNpsNo() != null && latestOrder.getNpsNo().startsWith(orderPrefix)) {
            try {
                sequence = Integer.parseInt(latestOrder.getNpsNo().substring(orderPrefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                sequence = 1;
            }
        }
        return orderPrefix + String.format("%04d", sequence);
    }
    private String generateNextTaskNo(String prefix) {
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String taskPrefix = prefix + datePrefix;
        ProductionOperationTask latestTask = productionOperationTaskMapper.selectOne(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .likeRight(ProductionOperationTask::getWorkOrderNo, taskPrefix)
                        .orderByDesc(ProductionOperationTask::getWorkOrderNo)
                        .last("limit 1"));
        int sequence = 1;
        if (latestTask != null && latestTask.getWorkOrderNo() != null && latestTask.getWorkOrderNo().startsWith(taskPrefix)) {
            try {
                sequence = Integer.parseInt(latestTask.getWorkOrderNo().substring(taskPrefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                sequence = 1;
            }
        }
        return taskPrefix + String.format("%03d", sequence);
    }
}
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -19,8 +19,6 @@
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.execl.StockInRecordExportData;
import com.ruoyi.production.mapper.ProductionOrderPickMapper;
import com.ruoyi.production.pojo.ProductionOrderPick;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockUninventoryMapper;
@@ -44,7 +42,6 @@
    private StockInRecordMapper stockInRecordMapper;
    private StockInventoryMapper stockInventoryMapper;
    private StockUninventoryMapper stockUninventoryMapper;
    private ProductionOrderPickMapper productionOrderPickMapper;
    @Override
    public IPage<StockInRecordDto> listPage(Page page, StockInRecordDto stockInRecordDto) {
@@ -158,32 +155,6 @@
        return stockUninventoryMapper.selectOne(eq);
    }
    /**
     * å›žæ»šç”Ÿäº§é€€æ–™å…¥åº“的领料记录退料数量
     * @param stockInRecord å…¥åº“记录
     */
    private void rollbackFeedReturnQty(StockInRecord stockInRecord) {
        ProductionOrderPick productionOrderPick = productionOrderPickMapper.selectById(stockInRecord.getRecordId());
        if (productionOrderPick != null) {
            BigDecimal returnQty = productionOrderPick.getReturnQty();
            if (returnQty == null) {
                returnQty = BigDecimal.ZERO;
            }
            BigDecimal newReturnQty = returnQty.subtract(stockInRecord.getStockInNum());
            if (newReturnQty.compareTo(BigDecimal.ZERO) < 0) {
                newReturnQty = BigDecimal.ZERO;
            }
            productionOrderPick.setReturnQty(newReturnQty);
            // é‡æ–°è®¡ç®—实际用量
            BigDecimal actualQty = productionOrderPick.getQuantity().add(
                productionOrderPick.getFeedingQty() != null ? productionOrderPick.getFeedingQty() : BigDecimal.ZERO)
                .subtract(newReturnQty);
            productionOrderPick.setActualQty(actualQty);
            productionOrderPick.setReturned(newReturnQty.compareTo(BigDecimal.ZERO) > 0);
            productionOrderPickMapper.updateById(productionOrderPick);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int batchDeletePending(List<Long> ids) {
@@ -194,11 +165,6 @@
            }
            if (stockInRecord.getApprovalStatus() != null && !ReviewStatusEnum.PENDING_REVIEW.getCode().equals(stockInRecord.getApprovalStatus())) {
                throw new BaseException("只有待审批状态的记录才能删除,入库批次:" + stockInRecord.getInboundBatches());
            }
            // å¦‚果是生产退料入库,删除时需要回滚领料记录的退料数量
            if (StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode().equals(stockInRecord.getRecordType())) {
                rollbackFeedReturnQty(stockInRecord);
            }
        }
        return stockInRecordMapper.deleteBatchIds(ids);
@@ -223,12 +189,6 @@
            }
            stockInRecord.setApprovalStatus(approvalStatus);
            stockInRecordMapper.updateById(stockInRecord);
            // å®¡æ‰¹é©³å›žæ—¶ï¼Œå¦‚果是生产退料入库,需要回滚领料记录的退料数量
            if (ReviewStatusEnum.REJECTED.getCode().equals(approvalStatus) &&
                StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode().equals(stockInRecord.getRecordType())) {
                rollbackFeedReturnQty(stockInRecord);
            }
            // å®¡æ‰¹é€šè¿‡æ—¶ï¼Œåº“存增加
            if (ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus)) {
@@ -292,27 +252,6 @@
            // åªæœ‰é©³å›žçŠ¶æ€æ‰èƒ½é‡æ–°å®¡æ ¸
            if (!ReviewStatusEnum.REJECTED.getCode().equals(stockInRecord.getApprovalStatus())) {
                throw new BaseException("只有驳回状态的记录才能重新审核,入库批次:" + stockInRecord.getInboundBatches());
            }
            // å¦‚果是生产退料入库,恢复退料数量(因为驳回时已扣减)
            if (StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode().equals(stockInRecord.getRecordType())) {
                ProductionOrderPick productionOrderPick = productionOrderPickMapper.selectById(stockInRecord.getRecordId());
                if (productionOrderPick != null) {
                    BigDecimal returnQty = productionOrderPick.getReturnQty();
                    if (returnQty == null) {
                        returnQty = BigDecimal.ZERO;
                    }
                    // é‡æ–°å®¡æ ¸æ—¶æ¢å¤é€€æ–™æ•°é‡
                    BigDecimal newReturnQty = returnQty.add(stockInRecord.getStockInNum());
                    productionOrderPick.setReturnQty(newReturnQty);
                    // é‡æ–°è®¡ç®—实际用量
                    BigDecimal actualQty = productionOrderPick.getQuantity().add(
                        productionOrderPick.getFeedingQty() != null ? productionOrderPick.getFeedingQty() : BigDecimal.ZERO)
                        .subtract(newReturnQty);
                    productionOrderPick.setActualQty(actualQty);
                    productionOrderPick.setReturned(newReturnQty.compareTo(BigDecimal.ZERO) > 0);
                    productionOrderPickMapper.updateById(productionOrderPick);
                }
            }
            // å°†çŠ¶æ€æ”¹ä¸ºå¾…å®¡æ ¸
src/main/resources/mapper/production/ProductionBomStructureMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -70,6 +70,9 @@
            <if test="c != null and c.workOrderNo != null and c.workOrderNo != ''">
                and pot.work_order_no like concat('%', #{c.workOrderNo}, '%')
            </if>
            <if test="c != null and c.currentUserId != null">
                and (pot.user_ids is null or pot.user_ids = '' or pot.user_ids = '[]' or json_contains(pot.user_ids, json_quote(cast(#{c.currentUserId} as char))))
            </if>
        </where>
        order by pot.production_order_id desc, poro.drag_sort
    </select>
src/main/resources/mapper/production/ProductionOrderBomMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -52,9 +52,7 @@
        pm.model as model,
        po.is_end_order as endOrder,
        tr.process_route_code as processRouteCode,
        ROUND(po.complete_quantity / po.quantity * 100, 2) AS completionStatus,
        tb.bom_no as bomNo,
        pop_return.returned as returned
        ROUND(po.complete_quantity / po.quantity * 100, 2) AS completionStatus
    </sql>
    <sql id="ProductionOrderVoFrom">
@@ -74,13 +72,6 @@
                 left join product_model pm on po.product_model_id = pm.id
                 left join product p on pm.product_id = p.id
                 left join technology_routing tr on po.technology_routing_id = tr.id
                 left join technology_bom tb on tb.id = tr.bom_id
                 left join (
            select production_order_id as productionOrderId,
                   if(max(case when ifnull(is_returned, 0) = 1 then 1 else 0 end) = 1, true, false) as returned
            from production_order_pick
            group by production_order_id
        ) pop_return on pop_return.productionOrderId = po.id
    </sql>
    <sql id="ProductionOrderWhere">
@@ -164,7 +155,6 @@
               po.quantity,
               ifnull(po.complete_quantity, 0) as completeQuantity,
               round(ifnull(po.complete_quantity, 0) / nullif(po.quantity, 0) * 100, 2) as completionStatus,
               tb.bom_no,
               datediff(po_sales.deliveryDate, curdate()) as deliveryDaysDiff,
               po_sales.deliveryDate as deliveryDate,
               false as isFh
@@ -184,7 +174,6 @@
                 left join product_model pm on po.product_model_id = pm.id
                 left join product p on pm.product_id = p.id
                 left join technology_routing tr on po.technology_routing_id = tr.id
                 left join technology_bom tb on tr.bom_id = tb.id
        where po.create_time between #{startTime} and #{endTime}
        order by po.create_time desc
    </select>
src/main/resources/mapper/production/ProductionOrderPickMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/production/ProductionOrderRoutingMapper.xml
@@ -12,7 +12,6 @@
        <result column="update_time" property="updateTime" />
        <result column="technology_routing_id" property="technologyRoutingId" />
        <result column="process_route_code" property="processRouteCode" />
        <result column="bom_id" property="bomId" />
        <result column="create_user" property="createUser" />
        <result column="dept_id" property="deptId" />
    </resultMap>
src/main/resources/mapper/production/ProductionProductMainMapper.xml
@@ -12,6 +12,8 @@
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="actual_start_time" property="actualStartTime" />
        <result column="actual_end_time" property="actualEndTime" />
    </resultMap>
    <select id="listPageProductionProductMainDto" resultType="com.ruoyi.production.bean.dto.ProductionProductMainDto">
@@ -81,6 +83,9 @@
            <if test="c.productMainId != null">
                and ppm.id = #{c.productMainId}
            </if>
            <if test="c.currentUserId != null">
                and (pot.user_ids is null or pot.user_ids = '' or pot.user_ids = '[]' or json_contains(pot.user_ids, json_quote(cast(#{c.currentUserId} as char))))
            </if>
        </where>
        order by ppm.create_time desc
    </select>
src/main/resources/mapper/quality/QualityUnqualifiedOrderMapper.xml
@@ -23,4 +23,11 @@
        </where>
        order by create_time desc
    </select>
    <delete id="deleteByIds">
        DELETE FROM quality_unqualified_order WHERE id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
</mapper>
src/main/resources/static/²»ºÏ¸ñÆ·´¦Àíµ¥.xls
Binary files differ