云
5 天以前 fd1efdba56dbf07943b82c971694579af034fc5a
feat: 新增废料管理并完善合同导出

新增废料入库、出售单、财务审批回调、库存过账及按生产订单统计,并提供业务表、权限菜单和审批流程模板。仓库设置支持仓库类型,库存台账展示物料技术参数。

合同 Word 导出改用中性模板,补充合同查询权限与数据校验,模板预留供方和交易条款。

废料审批模型需导入发布;Word 模板未完成渲染版式检查。
已添加14个文件
已修改14个文件
636 ■■■■■ 文件已修改
sql/mysql/20261103_warehouse_type.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261103_wm_scrap.sql 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261103_wm_scrap_menu.sql 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/wm_scrap_sale_finance.bpmn 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractDocumentRenderer.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/resources/templates/contract/sale-contract.docx 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractDocumentRendererTest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/scrap/MesWmScrapReceiptController.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/scrap/MesWmScrapSaleController.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehousePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/scrap/MesWmScrapReceiptDO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/scrap/MesWmScrapSaleDO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/scrap/MesWmScrapSaleLineDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/scrap/MesWmScrapReceiptMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/scrap/MesWmScrapSaleLineMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/scrap/MesWmScrapSaleMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/warehouse/MesWmWarehouseMapper.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/scrap/MesWmScrapSaleApprovalListener.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/scrap/MesWmScrapSaleService.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261103_warehouse_type.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- ä»“库类型为可选文本,保留历史仓库记录原值。
ALTER TABLE `mes_wm_warehouse`
    ADD COLUMN `warehouse_type` varchar(50) NULL DEFAULT NULL COMMENT '仓库类型' AFTER `name`;
sql/mysql/20261103_wm_scrap.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
-- ç‹¬ç«‹åºŸæ–™ä¸šåŠ¡ï¼šå…¥åº“æŒ‰ç”Ÿäº§è®¢å•ç™»è®°ï¼›å‡ºå”®å•åœ¨è´¢åŠ¡å®¡æ‰¹é€šè¿‡åŽè¿‡è´¦ã€‚
-- æ­¤è„šæœ¬ä»…åˆ›å»ºä¸šåŠ¡è¡¨ï¼Œä¸åˆ›å»ºå®¡æ‰¹æ¨¡åž‹ã€èœå•æˆ–ä»“åº“åŸºç¡€æ•°æ®ã€‚
CREATE TABLE IF NOT EXISTS `mes_wm_scrap_receipt` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `code` varchar(64) NOT NULL COMMENT '入库单号',
  `work_order_id` bigint NOT NULL COMMENT '生产工单编号',
  `work_order_code` varchar(64) NOT NULL COMMENT '生产工单号快照',
  `item_id` bigint NOT NULL COMMENT '废料物料编号',
  `quantity` decimal(20,4) NOT NULL COMMENT '入库数量',
  `warehouse_id` bigint NOT NULL,
  `location_id` bigint NOT NULL,
  `area_id` bigint NOT NULL,
  `batch_id` bigint DEFAULT NULL,
  `batch_code` varchar(100) DEFAULT NULL,
  `receipt_time` datetime NOT NULL,
  `transaction_id` bigint DEFAULT NULL COMMENT '入库库存流水编号',
  `remark` varchar(500) DEFAULT NULL,
  `creator` varchar(64) DEFAULT '',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updater` varchar(64) DEFAULT '',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted` bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_scrap_receipt_code` (`code`),
  KEY `idx_scrap_receipt_order_time` (`work_order_id`,`receipt_time`),
  KEY `idx_scrap_receipt_area` (`area_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='废料入库';
CREATE TABLE IF NOT EXISTS `mes_wm_scrap_sale` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `code` varchar(64) NOT NULL COMMENT '出售单号',
  `buyer` varchar(200) NOT NULL COMMENT '购买方',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '0草稿 1审批中 2已出库 3已驳回 4已取消',
  `process_instance_id` varchar(64) DEFAULT NULL COMMENT '审批流程实例',
  `remark` varchar(500) DEFAULT NULL,
  `creator` varchar(64) DEFAULT '',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updater` varchar(64) DEFAULT '',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted` bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_scrap_sale_code` (`code`),
  KEY `idx_scrap_sale_process` (`process_instance_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='废料出售单';
CREATE TABLE IF NOT EXISTS `mes_wm_scrap_sale_line` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `sale_id` bigint NOT NULL,
  `material_stock_id` bigint NOT NULL COMMENT '指定库存记录',
  `quantity` decimal(20,4) NOT NULL,
  `unit_price` decimal(20,4) NOT NULL,
  `transaction_id` bigint DEFAULT NULL COMMENT '审批通过后的出库流水编号',
  `creator` varchar(64) DEFAULT '',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updater` varchar(64) DEFAULT '',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted` bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (`id`),
  KEY `idx_scrap_sale_line_sale` (`sale_id`),
  KEY `idx_scrap_sale_line_stock` (`material_stock_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='废料出售明细';
sql/mysql/20261103_wm_scrap_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
-- åºŸæ–™ç®¡ç†èœå•和权限;业务表需先执行 20261103_wm_scrap.sql。
-- åˆå§‹æŽˆæƒè¶…级管理员,其他角色请在角色管理中按职责配置。
INSERT INTO system_menu(name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,updater,deleted)
SELECT '废料管理','',2,80,5780,'scrap','ep:box','wls/scrap/index','WlsScrap',0,b'1',b'0',b'1','1','1',b'0'
WHERE EXISTS (SELECT 1 FROM system_menu WHERE id=5780 AND deleted=b'0')
  AND NOT EXISTS (SELECT 1 FROM system_menu WHERE component='wls/scrap/index' AND deleted=b'0');
SET @scrap_menu_id = (SELECT id FROM system_menu WHERE component='wls/scrap/index' AND deleted=b'0' ORDER BY id LIMIT 1);
INSERT INTO system_menu(name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,updater,deleted)
SELECT p.name,p.permission,3,p.sort,@scrap_menu_id,'','','','',0,b'1',b'0',b'1','1','1',b'0'
FROM (
    SELECT '废料入库查询' name,'mes:wm-scrap-receipt:query' permission,1 sort UNION ALL
    SELECT '废料入库登记','mes:wm-scrap-receipt:create',2 UNION ALL
    SELECT '废料出售查询','mes:wm-scrap-sale:query',3 UNION ALL
    SELECT '废料出售创建','mes:wm-scrap-sale:create',4 UNION ALL
    SELECT '废料出售提交审批','mes:wm-scrap-sale:submit',5
) p
WHERE @scrap_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.permission=p.permission AND m.deleted=b'0');
INSERT INTO system_role_menu(role_id,menu_id,creator,updater,deleted)
SELECT r.id,m.id,'1','1',b'0' FROM system_role r JOIN system_menu m
  ON m.deleted=b'0' AND (m.id=@scrap_menu_id OR m.parent_id=@scrap_menu_id)
WHERE r.code='super_admin' AND r.deleted=b'0'
  AND NOT EXISTS (SELECT 1 FROM system_role_menu rm WHERE rm.role_id=r.id AND rm.menu_id=m.id AND rm.deleted=b'0');
sql/mysql/wm_scrap_sale_finance.bpmn
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:flowable="http://flowable.org/bpmn"
             xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
             xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
             xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
             targetNamespace="http://www.iocoder.cn/bpmn">
  <process id="wm_scrap_sale_finance" name="废料出售财务审批" isExecutable="true">
    <startEvent id="start" name="提交" />
    <sequenceFlow id="to_review" sourceRef="start" targetRef="finance_review" />
    <userTask id="finance_review" name="财务审核" flowable:candidateStrategy="10" flowable:candidateParam="1">
      <extensionElements><flowable:assignStartUserHandlerType>1</flowable:assignStartUserHandlerType></extensionElements>
    </userTask>
    <sequenceFlow id="to_end" sourceRef="finance_review" targetRef="end" />
    <endEvent id="end" name="完成" />
  </process>
  <bpmndi:BPMNDiagram id="diagram">
    <bpmndi:BPMNPlane id="plane" bpmnElement="wm_scrap_sale_finance">
      <bpmndi:BPMNShape id="start_shape" bpmnElement="start"><omgdc:Bounds x="100" y="100" width="36" height="36" /></bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="review_shape" bpmnElement="finance_review"><omgdc:Bounds x="200" y="78" width="120" height="80" /></bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="end_shape" bpmnElement="end"><omgdc:Bounds x="390" y="100" width="36" height="36" /></bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="to_review_edge" bpmnElement="to_review"><omgdi:waypoint x="136" y="118" /><omgdi:waypoint x="200" y="118" /></bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="to_end_edge" bpmnElement="to_end"><omgdi:waypoint x="320" y="118" /><omgdi:waypoint x="390" y="118" /></bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -147,10 +147,18 @@
    @GetMapping("/download-document")
    @Operation(summary = "按购销合同模板下载合同 Word æ–‡ä»¶")
    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
    public void downloadDocument(@RequestParam("id") Long id, HttpServletResponse response) throws IOException {
        // getContract åŒ…含合同数据权限校验;沿用详情页的产品、单位和客户信息。
        CrmContractRespVO contract = buildContractDetail(contractService.getContract(id));
        Assert.notNull(contract, "合同不存在");
        if (contract.getProducts() == null || contract.getProducts().isEmpty()) {
            throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "合同没有产品明细,请先维护产品后导出");
        }
        if (contract.getTotalPrice() == null || contract.getProducts().stream()
                .anyMatch(product -> product.getContractPrice() == null || product.getCount() == null)) {
            throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "合同金额、产品数量和单价不能为空,请先补全后导出");
        }
        byte[] content = cn.iocoder.yudao.module.crm.service.contract.CrmContractDocumentRenderer.render(contract);
        String filename = "购销合同-" + contract.getNo() + ".docx";
        response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractDocumentRenderer.java
@@ -78,9 +78,12 @@
            BigDecimal discount = productTotal.subtract(total);
            replace(document, Map.of("customerName", text(contract.getCustomerName()),
                    "contractNo", text(contract.getNo()),
                    "contractName", text(contract.getName()),
                    "contractDate", contract.getOrderDate() == null ? "____________" : contract.getOrderDate().format(DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日")),
                    "startDate", date(contract.getStartTime()),
                    "endDate", date(contract.getEndTime()),
                    "totalPrice", money(total), "totalUpper", Convert.digitToChinese(total),
                    "discountNote", discount.signum() == 0 ? "" : "(明细合计 " + money(productTotal) + " å…ƒï¼Œæ•´å•优惠 " + money(discount) + " å…ƒï¼‰"));
                    "discountNote", discount.signum() == 0 ? "" : "(明细合计 " + money(productTotal) + " å…ƒï¼Œä¸ŽåˆåŒé‡‘额的差额 " + money(discount) + " å…ƒï¼‰"));
            if (document.getDocumentElement().getTextContent().contains("${")) throw new IOException("合同模板存在未填写槽位");
            var transformerFactory = TransformerFactory.newInstance();
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
@@ -103,5 +106,6 @@
    }
    private static String money(BigDecimal value) { return value.setScale(2, RoundingMode.HALF_UP).toPlainString(); }
    private static String date(java.time.LocalDateTime value) { return value == null ? "____________" : value.format(DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日")); }
    private static String text(String value) { return value == null ? "" : value; }
}
yudao-module-crm/src/main/resources/templates/contract/sale-contract.docx
Binary files differ
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractDocumentRendererTest.java
@@ -36,7 +36,8 @@
        String xml = new String(parts.get("word/document.xml"), StandardCharsets.UTF_8);
        assertTrue(xml.contains("HT20261015001")); assertTrue(xml.contains("2026å¹´10月15日"));
        assertTrue(xml.contains("10000.00")); assertTrue(xml.contains("壹万元整"));
        assertTrue(xml.contains("冻干冬虫夏草(2500级一等)")); assertTrue(xml.contains("诚信合规"));
        assertTrue(xml.contains("冻干冬虫夏草(2500级一等)")); assertTrue(xml.contains("交付与验收"));
        assertFalse(xml.contains("北京同仁堂")); assertFalse(xml.contains("28504001040001454"));
        assertFalse(xml.contains("${"));
        try(var template = new ClassPathResource("templates/contract/sale-contract.docx").getInputStream()) {
            var original = unzip(template.readAllBytes()); assertEquals(original.keySet(),parts.keySet());
@@ -50,11 +51,11 @@
        contract.setProducts(Collections.nCopies(25,contract.getProducts().getFirst()));
        contract.setTotalPrice(new BigDecimal("249000"));
        String xml = new String(unzip(CrmContractDocumentRenderer.render(contract)).get("word/document.xml"),StandardCharsets.UTF_8);
        assertTrue(xml.contains("客户&lt;&amp;&gt;公司")); assertTrue(xml.contains("整单优惠 1000.00 å…ƒ"));
        assertTrue(xml.contains("客户&lt;&amp;&gt;公司")); assertTrue(xml.contains("与合同金额的差额 1000.00 å…ƒ"));
        assertEquals(25,xml.split("冻干冬虫夏草(2500级一等)",-1).length-1);
        assertEquals(25,xml.split("<w:cantSplit",-1).length-1);
        assertTrue(xml.contains("249000.00")); assertFalse(xml.contains("${"));
        assertFalse(xml.contains("乙方(需方):中国科学院西北高原生物研究所"));
        assertTrue(xml.contains("乙方(需方):客户&lt;&amp;&gt;公司"));
        String output = System.getProperty("contract.document.sample");
        if (output != null) Files.write(Path.of(output).resolveSibling("generated-contract-many.docx"), CrmContractDocumentRenderer.render(contract));
    }
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java
@@ -24,6 +24,9 @@
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "技术参数")
    private String technicalParameters;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -191,6 +191,7 @@
            MapUtils.findAndThen(batchMap, vo.getBatchId(), batch -> vo.setReelNo(batch.getReelNo()));
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification());
                vo.setTechnicalParameters(item.getTechnicalParameters());
                MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
                        unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
            });
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java
@@ -35,6 +35,10 @@
    @ExcelProperty("规格型号")
    private String specification;
    @Schema(description = "技术参数")
    @ExcelProperty("技术参数")
    private String technicalParameters;
    @Schema(description = "计量单位名称", example = "千克")
    @ExcelProperty("计量单位")
    private String unitMeasureName;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/scrap/MesWmScrapReceiptController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.scrap;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapReceiptDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapReceiptMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.List;
import java.util.Map;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/** ç‹¬ç«‹åºŸæ–™å…¥åº“:记录和库存流水在同一个事务中提交。 */
@RestController
@RequestMapping("/mes/wm/scrap-receipt")
@Validated
public class MesWmScrapReceiptController {
    @Resource private MesWmScrapReceiptMapper receiptMapper;
    @Resource private MesProWorkOrderMapper workOrderMapper;
    @Resource private MesWmTransactionService transactionService;
    @Resource private MesWmWarehouseAreaService areaService;
    @PostMapping("/create")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-receipt:create')")
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<Long> create(@Valid @RequestBody CreateReq req) {
        var order = workOrderMapper.selectById(req.getWorkOrderId());
        if (order == null) throw new ServiceException(400, "生产工单不存在");
        areaService.validateWarehouseAreaExists(req.getWarehouseId(), req.getLocationId(), req.getAreaId());
        String code = "SCR-" + req.getRequestId();
        if (receiptMapper.selectByCode(code) != null) throw new ServiceException(400, "请求已提交,请刷新入库记录");
        MesWmScrapReceiptDO receipt = new MesWmScrapReceiptDO();
        receipt.setCode(code);
        receipt.setWorkOrderId(order.getId());
        receipt.setWorkOrderCode(order.getCode());
        receipt.setItemId(req.getItemId());
        receipt.setQuantity(req.getQuantity());
        receipt.setWarehouseId(req.getWarehouseId());
        receipt.setLocationId(req.getLocationId());
        receipt.setAreaId(req.getAreaId());
        receipt.setBatchId(req.getBatchId());
        receipt.setBatchCode(req.getBatchCode());
        receipt.setReceiptTime(LocalDateTime.now());
        receipt.setRemark(req.getRemark());
        receiptMapper.insert(receipt);
        Long transactionId = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.IN.getType()).setBizType(cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants.WM_SCRAP_RECEIPT)
                .setBizId(receipt.getId()).setBizLineId(receipt.getId()).setBizCode(receipt.getCode())
                .setItemId(receipt.getItemId()).setQuantity(receipt.getQuantity())
                .setWarehouseId(receipt.getWarehouseId()).setLocationId(receipt.getLocationId())
                .setAreaId(receipt.getAreaId()).setBatchId(receipt.getBatchId()).setBatchCode(receipt.getBatchCode())
                .setReceiptTime(receipt.getReceiptTime()).setForceCreateNewStock(true));
        receipt.setTransactionId(transactionId);
        receiptMapper.updateById(receipt);
        return success(receipt.getId());
    }
    @GetMapping("/monthly-summary")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-receipt:query')")
    public CommonResult<List<Map<String, Object>>> monthlySummary(@RequestParam String month) {
        YearMonth period = YearMonth.parse(month);
        return success(receiptMapper.summarize(period.atDay(1).atStartOfDay(),
                period.plusMonths(1).atDay(1).atStartOfDay()));
    }
    @GetMapping("/page")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-receipt:query')")
    public CommonResult<PageResult<MesWmScrapReceiptDO>> page(@Valid PageParam page,
                                                              @RequestParam(required = false) Long workOrderId) {
        return success(receiptMapper.selectReceiptPage(page, workOrderId));
    }
    @Data
    public static class CreateReq {
        @NotBlank @Pattern(regexp = "[A-Za-z0-9-]{1,60}") private String requestId;
        @NotNull private Long workOrderId;
        @NotNull private Long itemId;
        @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 16, fraction = 4) private BigDecimal quantity;
        @NotNull private Long warehouseId;
        @NotNull private Long locationId;
        @NotNull private Long areaId;
        private Long batchId;
        @Size(max = 100) private String batchCode;
        @Size(max = 500) private String remark;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/scrap/MesWmScrapSaleController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.scrap;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapSaleDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapSaleLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapReceiptMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapSaleLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapSaleMapper;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.scrap.MesWmScrapSaleService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@RestController
@RequestMapping("/mes/wm/scrap-sale")
@Validated
public class MesWmScrapSaleController {
    @Resource private MesWmScrapSaleMapper saleMapper;
    @Resource private MesWmScrapSaleLineMapper lineMapper;
    @Resource private MesWmScrapReceiptMapper receiptMapper;
    @Resource private MesWmMaterialStockService stockService;
    @Resource private MesWmScrapSaleService saleService;
    @GetMapping("/page")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-sale:query')")
    public CommonResult<PageResult<MesWmScrapSaleDO>> page(@Valid PageParam page) {
        return success(saleMapper.selectPage(page, new LambdaQueryWrapperX<MesWmScrapSaleDO>()
                .orderByDesc(MesWmScrapSaleDO::getId)));
    }
    @GetMapping("/lines")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-sale:query')")
    public CommonResult<List<MesWmScrapSaleLineDO>> lines(@RequestParam Long id) {
        return success(lineMapper.selectBySaleId(id));
    }
    @PutMapping("/submit")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-sale:submit')")
    public CommonResult<Boolean> submit(@RequestParam Long id) {
        saleService.submit(id, getLoginUserId());
        return success(true);
    }
    /** requestId ç”±å®¢æˆ·ç«¯ä¸ºæ¯æ¬¡ä¸šåŠ¡æ“ä½œç”Ÿæˆå¹¶åœ¨é‡è¯•æ—¶å¤ç”¨ã€‚ */
    @PostMapping("/create")
    @PreAuthorize("@ss.hasPermission('mes:wm-scrap-sale:create')")
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<Long> create(@Valid @RequestBody CreateReq req) {
        String code = "SS-" + req.getRequestId();
        if (saleMapper.selectByCode(code) != null) {
            throw new ServiceException(400, "请求已提交,请刷新出售单列表");
        }
        Set<Long> stocks = new HashSet<>();
        for (LineReq line : req.getLines()) {
            if (!stocks.add(line.getMaterialStockId())) throw new ServiceException(400, "同一库存不能重复添加");
            if (stockService.getMaterialStock(line.getMaterialStockId()) == null
                    || receiptMapper.countByStockId(line.getMaterialStockId()) == 0) {
                throw new ServiceException(400, "请选择废料入库产生的库存");
            }
        }
        MesWmScrapSaleDO sale = new MesWmScrapSaleDO();
        sale.setCode(code);
        sale.setBuyer(req.getBuyer());
        sale.setRemark(req.getRemark());
        sale.setStatus(0);
        saleMapper.insert(sale);
        for (LineReq input : req.getLines()) {
            MesWmScrapSaleLineDO line = new MesWmScrapSaleLineDO();
            line.setSaleId(sale.getId());
            line.setMaterialStockId(input.getMaterialStockId());
            line.setQuantity(input.getQuantity());
            line.setUnitPrice(input.getUnitPrice());
            lineMapper.insert(line);
        }
        return success(sale.getId());
    }
    @Data public static class CreateReq {
        @NotBlank @Pattern(regexp = "[A-Za-z0-9-]{1,60}") private String requestId;
        @NotBlank @Size(max = 200) private String buyer;
        @Size(max = 500) private String remark;
        @NotEmpty @Size(max = 100) private List<@Valid LineReq> lines;
    }
    @Data public static class LineReq {
        @NotNull private Long materialStockId;
        @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 16, fraction = 4) private BigDecimal quantity;
        @NotNull @DecimalMin("0") @Digits(integer = 16, fraction = 4) private BigDecimal unitPrice;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehousePageReqVO.java
@@ -18,6 +18,9 @@
    @Schema(description = "仓库名称", example = "原料仓")
    private String name;
    @Schema(description = "仓库类型")
    private String warehouseType;
    @Schema(description = "是否冻结", example = "false")
    private Boolean frozen;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseRespVO.java
@@ -25,6 +25,10 @@
    @ExcelProperty("仓库名称")
    private String name;
    @Schema(description = "仓库类型", example = "原料仓")
    @ExcelProperty("仓库类型")
    private String warehouseType;
    @Schema(description = "仓库地址", example = "A åŒºä¸€å·æ¥¼")
    @ExcelProperty("仓库地址")
    private String address;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseSaveReqVO.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
@@ -22,6 +23,10 @@
    @NotEmpty(message = "仓库名称不能为空")
    private String name;
    @Schema(description = "仓库类型", example = "原料仓")
    @Size(max = 50, message = "仓库类型不能超过 50 ä¸ªå­—符")
    private String warehouseType;
    @Schema(description = "仓库地址", example = "A åŒºä¸€å·æ¥¼")
    private String address;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/scrap/MesWmScrapReceiptDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@TableName("mes_wm_scrap_receipt")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesWmScrapReceiptDO extends BaseDO {
    @TableId
    private Long id;
    private String code;
    private Long workOrderId;
    private String workOrderCode;
    private Long itemId;
    private BigDecimal quantity;
    private Long warehouseId;
    private Long locationId;
    private Long areaId;
    private Long batchId;
    private String batchCode;
    private LocalDateTime receiptTime;
    private Long transactionId;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/scrap/MesWmScrapSaleDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mes_wm_scrap_sale")
public class MesWmScrapSaleDO extends BaseDO {
    @TableId private Long id;
    private String code;
    private String buyer;
    private Integer status;
    private String processInstanceId;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/scrap/MesWmScrapSaleLineDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mes_wm_scrap_sale_line")
public class MesWmScrapSaleLineDO extends BaseDO {
    @TableId private Long id;
    private Long saleId;
    private Long materialStockId;
    private BigDecimal quantity;
    private BigDecimal unitPrice;
    private Long transactionId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseDO.java
@@ -43,6 +43,8 @@
     * ä»“库名称
     */
    private String name;
    /** ä»“库类型 */
    private String warehouseType;
    /**
     * ä»“库地址
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
@@ -20,6 +20,11 @@
@Mapper
public interface MesWmMaterialStockMapper extends BaseMapperX<MesWmMaterialStockDO> {
    default MesWmMaterialStockDO lockById(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getId, id).last("FOR UPDATE"));
    }
    default PageResult<MesWmMaterialStockDO> selectPage(MesWmMaterialStockPageReqVO reqVO,
                                                         Collection<Long> itemTypeIds,
                                                         Collection<Long> itemIds,
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/scrap/MesWmScrapReceiptMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapReceiptDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Mapper
public interface MesWmScrapReceiptMapper extends BaseMapperX<MesWmScrapReceiptDO> {
    default MesWmScrapReceiptDO selectByCode(String code) {
        return selectOne(MesWmScrapReceiptDO::getCode, code);
    }
    @Select("SELECT COUNT(*) FROM mes_wm_scrap_receipt r JOIN mes_wm_transaction t " +
            "ON t.id = r.transaction_id AND t.deleted = 0 " +
            "WHERE r.deleted = 0 AND t.material_stock_id = #{stockId}")
    long countByStockId(@Param("stockId") Long stockId);
    default PageResult<MesWmScrapReceiptDO> selectReceiptPage(PageParam page, Long workOrderId) {
        return selectPage(page, new LambdaQueryWrapperX<MesWmScrapReceiptDO>()
                .eqIfPresent(MesWmScrapReceiptDO::getWorkOrderId, workOrderId)
                .orderByDesc(MesWmScrapReceiptDO::getId));
    }
    @Select("SELECT work_order_id AS workOrderId, work_order_code AS workOrderCode, item_id AS itemId, " +
            "SUM(quantity) AS quantity FROM mes_wm_scrap_receipt " +
            "WHERE deleted = 0 AND receipt_time >= #{start} AND receipt_time < #{end} " +
            "GROUP BY work_order_id, work_order_code, item_id ORDER BY work_order_code, item_id")
    List<Map<String, Object>> summarize(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/scrap/MesWmScrapSaleLineMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapSaleLineDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MesWmScrapSaleLineMapper extends BaseMapperX<MesWmScrapSaleLineDO> {
    default List<MesWmScrapSaleLineDO> selectBySaleId(Long id) {
        return selectList(MesWmScrapSaleLineDO::getSaleId, id);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/scrap/MesWmScrapSaleMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapSaleDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MesWmScrapSaleMapper extends BaseMapperX<MesWmScrapSaleDO> {
    default MesWmScrapSaleDO lockById(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesWmScrapSaleDO>()
                .eq(MesWmScrapSaleDO::getId, id).last("FOR UPDATE"));
    }
    default MesWmScrapSaleDO selectByCode(String code) {
        return selectOne(MesWmScrapSaleDO::getCode, code);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/warehouse/MesWmWarehouseMapper.java
@@ -20,6 +20,7 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmWarehouseDO>()
                .likeIfPresent(MesWmWarehouseDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmWarehouseDO::getName, reqVO.getName())
                .likeIfPresent(MesWmWarehouseDO::getWarehouseType, reqVO.getWarehouseType())
                .eqIfPresent(MesWmWarehouseDO::getFrozen, reqVO.getFrozen())
                .orderByDesc(MesWmWarehouseDO::getId));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java
@@ -49,6 +49,8 @@
    public static final int WM_RETURN_VENDOR = 125; // ä¾›åº”商退货出库:MesWmReturnVendorDO
    public static final int WM_STOCK_ADJUST_IN = 126; // åº“存调整入库(自定义入库)
    public static final int WM_STOCK_ADJUST_OUT = 127; // åº“存调整出库(自定义出库)
    public static final int WM_SCRAP_RECEIPT = 128; // åºŸæ–™å…¥åº“
    public static final int WM_SCRAP_SALE = 129; // åºŸæ–™å‡ºå”®å‡ºåº“
    // ========== CAL æŽ’班模块 [200, 300) ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/scrap/MesWmScrapSaleApprovalListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.service.wm.scrap;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class MesWmScrapSaleApprovalListener extends BpmProcessInstanceStatusEventListener {
    @Resource private MesWmScrapSaleService service;
    @Override protected String getProcessDefinitionKey() { return MesWmScrapSaleService.PROCESS_KEY; }
    @Override protected void onEvent(BpmProcessInstanceStatusEvent event) {
        service.onApproval(Long.valueOf(event.getBusinessKey()), event.getId(), event.getStatus());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/scrap/MesWmScrapSaleService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package cn.iocoder.yudao.module.mes.service.wm.scrap;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapSaleDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapSaleLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapReceiptMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapSaleLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapSaleMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.Objects;
import java.util.Map;
@Service
public class MesWmScrapSaleService {
    public static final String PROCESS_KEY = "wm_scrap_sale_finance";
    @Resource private MesWmScrapSaleMapper saleMapper;
    @Resource private MesWmScrapSaleLineMapper lineMapper;
    @Resource private MesWmScrapReceiptMapper receiptMapper;
    @Resource private MesWmMaterialStockMapper stockMapper;
    @Resource private MesWmTransactionService transactionService;
    @Resource private BpmProcessInstanceApi processApi;
    @Transactional(rollbackFor = Exception.class)
    public void submit(Long id, Long userId) {
        MesWmScrapSaleDO sale = saleMapper.lockById(id);
        if (sale == null || !Objects.equals(sale.getStatus(), 0)) throw new ServiceException(400, "仅草稿可提交审批");
        var lines = lineMapper.selectBySaleId(id);
        if (lines.isEmpty()) throw new ServiceException(400, "出售单没有明细");
        // è´¢åŠ¡å®¡æ ¸ä¸å¯è¢«å…¨å±€å…å®¡æ‰¹ç»•è¿‡ã€‚
        if (processApi.isGlobalAutoApprove()) throw new ServiceException(400, "全局免审批已开启,废料出售必须经过财务审核");
        MesWmScrapSaleDO update = new MesWmScrapSaleDO();
        update.setId(id);
        update.setStatus(1);
        saleMapper.updateById(update);
        String processId = processApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(PROCESS_KEY).setBusinessKey(String.valueOf(id))
                .setVariables(Map.of("saleCode", sale.getCode(), "buyer", sale.getBuyer(),
                        "totalAmount", lines.stream().map(line -> line.getQuantity().multiply(line.getUnitPrice()))
                                .reduce(BigDecimal.ZERO, BigDecimal::add))));
        MesWmScrapSaleDO processUpdate = new MesWmScrapSaleDO();
        processUpdate.setId(id);
        processUpdate.setProcessInstanceId(processId);
        saleMapper.updateById(processUpdate);
    }
    @Transactional(rollbackFor = Exception.class)
    public void onApproval(Long id, String processId, Integer status) {
        MesWmScrapSaleDO sale = saleMapper.lockById(id);
        if (sale == null || !Objects.equals(sale.getStatus(), 1)) return;
        if (sale.getProcessInstanceId() != null && !sale.getProcessInstanceId().equals(processId)) return;
        if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
            var lines = lineMapper.selectBySaleId(id);
            lines.sort(Comparator.comparing(MesWmScrapSaleLineDO::getMaterialStockId));
            for (MesWmScrapSaleLineDO line : lines) {
                var stock = stockMapper.lockById(line.getMaterialStockId());
                if (stock == null || receiptMapper.countByStockId(line.getMaterialStockId()) == 0)
                    throw new ServiceException(400, "废料库存不存在或已变动");
                BigDecimal available = zero(stock.getQuantity()).subtract(zero(stock.getFrozenQuantity())).subtract(zero(stock.getReservedQuantity()));
                if (available.compareTo(line.getQuantity()) < 0) throw new ServiceException(400, "废料可用库存不足,无法出库");
                Long transactionId = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                        .setType(MesWmTransactionTypeEnum.OUT.getType()).setBizType(MesBizTypeConstants.WM_SCRAP_SALE)
                        .setBizId(id).setBizCode(sale.getCode()).setBizLineId(line.getId())
                        .setMaterialStockId(stock.getId()).setItemId(stock.getItemId()).setQuantity(line.getQuantity().negate())
                        .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId())
                        .setBatchId(stock.getBatchId()).setBatchCode(stock.getBatchCode()));
                MesWmScrapSaleLineDO updateLine = new MesWmScrapSaleLineDO();
                updateLine.setId(line.getId());
                updateLine.setTransactionId(transactionId);
                lineMapper.updateById(updateLine);
            }
            setStatus(id, 2);
        } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) {
            setStatus(id, 3);
        } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.CANCEL.getStatus())) {
            setStatus(id, 4);
        }
    }
    private void setStatus(Long id, int status) {
        MesWmScrapSaleDO update = new MesWmScrapSaleDO();
        update.setId(id);
        update.setStatus(status);
        saleMapper.updateById(update);
    }
    private BigDecimal zero(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
}