宁夏万通需求:商机状态组改为非必填;过程检验单生产订单、工位改为非必填
商机(/sale/business,代码 crm/business):
- 新增商机时「商机状态组」不再必填;未选状态组则不写默认阶段,也不参与状态组维度统计
- 变更商机阶段时先校验状态组:未设置状态组的商机给出精确提示(新增错误码 1_020_002_005,含商机名称与下一步动作),只允许改「结束状态」
- crm_business.status_type_id / status_id 放宽为可空(配置导出脚本同步)
过程检验单(/qc/ipqc):
- 生产订单、工位改为非必填:VO 去掉必填校验,服务层关联比对改为两边都有值才比
- Excel 导入:生产工单号/工位名称留空即不关联;去重键改用 eq+isNull 成对匹配,留空行也能去重
业务可视化文档(销售流程、质量流程/三列表导入说明)同步。
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | {"id": "sales", "name": "销售流程", "mermaid": "mermaid/02-sales-flow.mmd", "color": "#4f9cff", |
| | | "summary": "线索转化客户 → 商机推进 → 报价(可AI识别) → 合同 BPM 审批 → 生成 ERP 销售订单并审批 → 需生产行自动建 MPS → 发货通知 → 销售出库(OQC)回写 → 开票(BPM) → 回款(先开票校验)。", |
| | | "steps": [ |
| | | {"stage": "获客", "action": "线索转化客户、建联系人、立项商机", "roles": ["销售内勤"]}, |
| | | {"stage": "获客", "action": "线索转化客户、建联系人、立项商机(商机状态组/阶段选填,不选也能建单)", "roles": ["销售内勤"]}, |
| | | {"stage": "报价", "action": "商机赢单后建报价单(AI OCR 预填),转合同", "roles": ["销售内勤"]}, |
| | | {"stage": "签约", "action": "合同来源:手工新建 / 报价单转入 / Excel 导入(一合同多产品明细行);提交 BPM 审批(可选流程定义),通过=20", "roles": ["销售", "审批人"]}, |
| | | {"stage": "接单", "action": "合同一键生成销售订单(XSDD),销售订单再走 BPM;审批通过自动为 needProduction 明细创建 MPS", "roles": ["销售", "计划员"]}, |
| | |
| | | {"stage": "过程", "action": "报工质检工序产出行→IPQC,合格拆行入库、不合格生成 NCR", "roles": ["IPQC 质检员"]}, |
| | | {"stage": "出货", "action": "销售出库提交→OQC,完成后出库单进入待拣货", "roles": ["OQC 质检员"]}, |
| | | {"stage": "退货", "action": "退料/销售退货/售后退货→RQC,结论回调单据与售后", "roles": ["RQC 质检员"]}, |
| | | {"stage": "导入", "action": "IQC/IPQC/OQC 三列表各支持 Excel 批量导入(按钮在「新增」与「导出」之间,纯新增不做更新覆盖):一行一张单,编号留空自动生成,质检方案留空按物料+检验类型自动匹配,检测人员留空取当前登录用户;物料/供应商/客户/工单/工位按编码或名称精确匹配(IPQC 物料由工单推导,模板不填),合格+不合格=检测/接收数量、IPQC 工废+料废+其他废品≤不合格、OQC 客户须在本人数据权限内;逐行导入部分成功,失败行进 failureList 带行号与可执行动作,成功单一律待检验0;同一批数据不可重复导入:文件内重复行与库内已存在同业务键单据(IQC 物料+供应商+供应商批次+来料日期、IPQC 生产工单+检验类型+工位+检测日期、OQC 物料+客户+批次号+出货日期)均拒该行,提示含维度、双方实际值与「先删除该单据再重试」动作", "roles": ["质检员", "IQC 质检员", "IPQC 质检员", "OQC 质检员"]}, |
| | | {"stage": "导入", "action": "IQC/IPQC/OQC 三列表各支持 Excel 批量导入(按钮在「新增」与「导出」之间,纯新增不做更新覆盖):一行一张单,编号留空自动生成,质检方案留空按物料+检验类型自动匹配,检测人员留空取当前登录用户;物料/供应商/客户/工单/工位按编码或名称精确匹配(IPQC 物料由工单推导、生产工单号与工位名称可留空不关联,模板不填物料),合格+不合格=检测/接收数量、IPQC 工废+料废+其他废品≤不合格、OQC 客户须在本人数据权限内;逐行导入部分成功,失败行进 failureList 带行号与可执行动作,成功单一律待检验0;同一批数据不可重复导入:文件内重复行与库内已存在同业务键单据(IQC 物料+供应商+供应商批次+来料日期、IPQC 生产工单+检验类型+工位+检测日期(工单/工位留空按空值比对)、OQC 物料+客户+批次号+出货日期)均拒该行,提示含维度、双方实际值与「先删除该单据再重试」动作", "roles": ["质检员", "IQC 质检员", "IPQC 质检员", "OQC 质检员"]}, |
| | | {"stage": "不合格", "action": "NCR: 待评审→评审→处置(退货/报废/返工/让步接收/降级)→关闭", "roles": ["质量工程师"]}, |
| | | {"stage": "出件", "action": "两条出件入口:①通用 generate/preview(调用方自带上下文) 渲染 HTML 并冻结数据快照,编号 QR+日期+4位流水按天独立,preview 只渲染不落库不消耗编号,regenerate 用冻结快照重渲且数据与编号不变并作废已归档 PDF,导出 PDF 由后端调 Chromium 打印实例已存的 HTML 后归档附件中心;②generate-from-qc 由质检单直接出件(数据经 MesQcReportApi 从 MES 取,前端不拼上下文),出件前五道校验按业务优先级只报第一处:类型非法/单据不存在/未完成检验/未填判定/模板类型不符/无可用检验项,报告头另带单据原判定 qcResult+qcResultText 与引擎判定并存,响应回 undecidableCount 待判定项数与 warnings(分组标题跳过/指标已删/未录实测值/样品超3),同一张单据可反复出多份", "roles": ["质检员"]}, |
| | | {"stage": "建模板", "action": "AI 导入:上传既有检验报告文件(扫描件/照片、电子版 PDF、Word/Excel) → 识别成模板草稿(不落库) → 人工在设计器确认调整后保存为版本;.doc/.xls 旧格式不支持", "roles": ["质检员", "质量工程师"]}]}, |
| | |
| | | {"key": "crm", "name": "销售管理(客户关系)", "color": "#4f9cff", "tag": "CRM", "flow": "sales", |
| | | "positioning": "销售前端:从线索到回款的客户经营", |
| | | "problem": "客户资产沉淀、商机转化、合同审批与开票回款管控", |
| | | "features": ["线索转化客户", "公海池与领取上限", "联系人", "商机自定义阶段", "跟进记录", "报价单(AI OCR)", "合同(BPM审批→生成销售订单·支持Excel导入)", "开票(BPM审批·支持Excel导入·同一批数据不可重复导入)→回款(先开票校验)", "回款计划", "销售目标", "数据权限三级(负责人/只读/读写)"], |
| | | "features": ["线索转化客户", "公海池与领取上限", "联系人", "商机自定义阶段(状态组选填)", "跟进记录", "报价单(AI OCR)", "合同(BPM审批→生成销售订单·支持Excel导入)", "开票(BPM审批·支持Excel导入·同一批数据不可重复导入)→回款(先开票校验)", "回款计划", "销售目标", "数据权限三级(负责人/只读/读写)"], |
| | | "objects": ["线索", "客户", "联系人", "商机", "报价单", "合同", "发票", "回款", "销售目标"], |
| | | "roles": ["销售内勤", "销售经理", "财务(开票回款)"], |
| | | "upstream": ["主数据(物料)"], "downstream": ["进销存(销售订单)", "售后(客户)", "AI(报价OCR)"], |
| | |
| | | {"name": "客户", "module": "销售管理", "table": "crm_customer", "doc": "客户主档,公海池/锁定/成交状态,负责人数据权限", "upstream": ["线索"], "downstream": ["联系人", "商机", "报价单", "合同", "售后工单", "收款单"], "keywords": "客户 customer 公海"}, |
| | | {"name": "线索", "module": "销售管理", "table": "crm_clue", "doc": "原始销售线索,仅一次转化为客户", "upstream": [], "downstream": ["客户"], "keywords": "线索 clue"}, |
| | | {"name": "联系人", "module": "销售管理", "table": "crm_contact", "doc": "客户下联系人,与商机多对多", "upstream": ["客户"], "downstream": ["商机"], "keywords": "联系人 contact"}, |
| | | {"name": "商机", "module": "销售管理", "table": "crm_business", "doc": "销售机会,DB 配置式阶段推进,结束态: 赢/输/无效", "upstream": ["客户"], "downstream": ["报价单", "合同"], "keywords": "商机 business 赢单 输单 阶段"}, |
| | | {"name": "商机", "module": "销售管理", "table": "crm_business", "doc": "销售机会,DB 配置式阶段推进(商机状态组/阶段为选填:不选状态组也能建单,此时无默认阶段、不可变更阶段、不参与状态组统计),结束态: 赢/输/无效", "upstream": ["客户"], "downstream": ["报价单", "合同"], "keywords": "商机 business 赢单 输单 阶段 状态组"}, |
| | | {"name": "报价单", "module": "销售管理", "table": "crm_sale_quotation", "doc": "QT编号,选 MDM 物料报价,AI OCR 录入,草稿→已转合同", "upstream": ["商机"], "downstream": ["合同"], "keywords": "报价 quotation QT"}, |
| | | {"name": "合同", "module": "销售管理", "table": "crm_contract", "doc": "HT编号,BPM 审批(0草稿/10审批中/20通过/30驳回/40取消/50作废),通过后可生成销售订单;支持 Excel 导入(一合同多产品明细行、合并单元格自动拉平,客户与物料按名称/编码校验);产品明细带出物料名称/编码/条码/规格型号/计量单位;删除合同时连带删除产品明细,不留孤儿数据", "upstream": ["报价单"], "downstream": ["销售订单", "回款计划", "开票"], "keywords": "合同 contract 审批 导入"}, |
| | | {"name": "发票", "module": "销售管理", "table": "crm_invoice", "doc": "KP编号(可留空自动生成、系统内唯一),BPM 审批,累计开票≤合同额,附件必传;支持 Excel 导入(仅新增不更新,合同须审核通过且本次金额≤合同剩余可开票额;同一批数据不可重复导入——同合同下发票号码相同、或发票号码留空时开票金额+发票抬头+开票日期相同即判重复,文件内重复行与库内已有单都拒该行并回显维度与实际值)", "upstream": ["合同"], "downstream": ["回款"], "keywords": "开票 发票 invoice 导入 去重"}, |
| | |
| | | {"name": "库存台账", "module": "仓储", "table": "mes_wm_material_stock", "doc": "物料×仓库×库区×库位×批次×供应商六元组,数量/冻结/占用/在途四量", "upstream": ["全部入库单"], "downstream": ["全部出库单", "事务流水"], "keywords": "库存 台账 stock 可用量"}, |
| | | {"name": "批次", "module": "仓储", "table": "mes_wm_batch", "doc": "关联物料/工单/供应商/客户/质量状态;SN 码含批次 uuid;条码支持18种业务对象", "upstream": ["入库单据"], "downstream": ["库存台账"], "keywords": "批次 batch SN 条码 追溯"}, |
| | | {"name": "IQC来料检验", "module": "质量", "table": "mes_qc_iqc", "doc": "到货通知/外协入库触发,草稿0→完成4,结论回写来源单据;行操作「出报告」仅在已完成时可见;支持 Excel 批量导入(工具栏「批量导入」,纯新增、导入单为草稿、编号可留空自动生成);同一批数据不可重复导入(物料+供应商+供应商批次+来料日期,文件内重复行与库内已有单都拒该行)", "upstream": ["到货通知", "外协入库", "Excel 批量导入"], "downstream": ["采购入库", "供应商退货", "NCR", "报告实例(出报告)"], "keywords": "IQC 来料检验 出报告 批量导入 导入模板"}, |
| | | {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;行操作「出报告」仅在已完成时可见;支持 Excel 批量导入(产品物料由生产工单推导,模板不填物料;工废+料废+其他废品不得超过不合格品数量);同一批数据不可重复导入(生产工单+检验类型+工位+检测日期,文件内重复行与库内已有单都拒该行)", "upstream": ["报工", "Excel 批量导入"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告 批量导入 导入模板"}, |
| | | {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;行操作「出报告」仅在已完成时可见;新增/修改时生产订单、工位均非必填:留空则不做工单与工位的工序校验、产品物料不再由工单推导(记录可无产品物料);质检方案必填,未选生产订单/生产任务时按「过程检验」种类列出全部启用方案并校验方案本身(存在/启用/种类含过程检验),不再要求与产品物料绑定;有工单/任务时仍按产品物料已绑定方案匹配;支持 Excel 批量导入(生产工单号、工位名称留空即不关联,工废+料废+其他废品不得超过不合格品数量);同一批数据不可重复导入(生产工单+检验类型+工位+检测日期,留空字段按空值比对,文件内重复行与库内已有单都拒该行)", "upstream": ["报工", "Excel 批量导入"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告 批量导入 导入模板"}, |
| | | {"name": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,完成推进出库单待拣货;行操作「出报告」仅在已完成时可见;支持 Excel 批量导入(客户须在当前登录用户的客户数据权限范围内);同一批数据不可重复导入(物料+客户+批次号+出货日期,文件内重复行与库内已有单都拒该行)", "upstream": ["销售出库单", "Excel 批量导入"], "downstream": ["销售出库单(推进)", "报告实例(出报告)"], "keywords": "OQC 出货检验 出厂检验 出报告 批量导入 导入模板"}, |
| | | {"name": "RQC退货检验", "module": "质量", "table": "mes_qc_rqc", "doc": "生产退料/销售退货/售后退货触发,结论回调售后;行操作「出报告」仅在已完成时可见。措辞已统一为「退货检验」(原菜单父项与 mes_qc_type 字典写「退料检验」,与后端枚举/表注释/菜单权限子项不一致;RQC 三者都覆盖,故取「退货检验」)", "upstream": ["退料", "退货"], "downstream": ["退货入库", "售后回调", "报告实例(出报告)"], "keywords": "RQC 退货检验 退料检验 出报告"}, |
| | | {"name": "NCR不合格品", "module": "质量", "table": "mes_qc_ncr", "doc": "待评审0→已评审6→已处置7→已关闭8,处置:退货/报废/返工/让步接收/降级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["退货/报废/返工(台账)"], "keywords": "NCR 不合格 处置 特采 报废"}, |
| | |
| | | {"key": "crm", "name": "销售管理(客户关系)", "color": "#4f9cff", "tag": "CRM", "flow": "sales", |
| | | "positioning": "销售前端:从线索到回款的客户经营", |
| | | "problem": "客户资产沉淀、商机转化、合同审批与开票回款管控", |
| | | "features": ["线索转化客户", "公海池与领取上限", "联系人", "商机自定义阶段", "跟进记录", "报价单(AI OCR)", "合同(BPM审批→生成销售订单·支持Excel导入)", "开票(BPM审批·支持Excel导入·同一批数据不可重复导入)→回款(先开票校验)", "回款计划", "销售目标", "数据权限三级(负责人/只读/读写)"], |
| | | "features": ["线索转化客户", "公海池与领取上限", "联系人", "商机自定义阶段(状态组选填)", "跟进记录", "报价单(AI OCR)", "合同(BPM审批→生成销售订单·支持Excel导入)", "开票(BPM审批·支持Excel导入·同一批数据不可重复导入)→回款(先开票校验)", "回款计划", "销售目标", "数据权限三级(负责人/只读/读写)"], |
| | | "objects": ["线索", "客户", "联系人", "商机", "报价单", "合同", "发票", "回款", "销售目标"], |
| | | "roles": ["销售内勤", "销售经理", "财务(开票回款)"], |
| | | "upstream": ["主数据(物料)"], "downstream": ["进销存(销售订单)", "售后(客户)", "AI(报价OCR)"], |
| | |
| | | {"name": "客户", "module": "销售管理", "table": "crm_customer", "doc": "客户主档,公海池/锁定/成交状态,负责人数据权限", "upstream": ["线索"], "downstream": ["联系人", "商机", "报价单", "合同", "售后工单", "收款单"], "keywords": "客户 customer 公海"}, |
| | | {"name": "线索", "module": "销售管理", "table": "crm_clue", "doc": "原始销售线索,仅一次转化为客户", "upstream": [], "downstream": ["客户"], "keywords": "线索 clue"}, |
| | | {"name": "联系人", "module": "销售管理", "table": "crm_contact", "doc": "客户下联系人,与商机多对多", "upstream": ["客户"], "downstream": ["商机"], "keywords": "联系人 contact"}, |
| | | {"name": "商机", "module": "销售管理", "table": "crm_business", "doc": "销售机会,DB 配置式阶段推进,结束态: 赢/输/无效", "upstream": ["客户"], "downstream": ["报价单", "合同"], "keywords": "商机 business 赢单 输单 阶段"}, |
| | | {"name": "商机", "module": "销售管理", "table": "crm_business", "doc": "销售机会,DB 配置式阶段推进(商机状态组/阶段为选填:不选状态组也能建单,此时无默认阶段、不可变更阶段、不参与状态组统计),结束态: 赢/输/无效", "upstream": ["客户"], "downstream": ["报价单", "合同"], "keywords": "商机 business 赢单 输单 阶段 状态组"}, |
| | | {"name": "报价单", "module": "销售管理", "table": "crm_sale_quotation", "doc": "QT编号,选 MDM 物料报价,AI OCR 录入,草稿→已转合同", "upstream": ["商机"], "downstream": ["合同"], "keywords": "报价 quotation QT"}, |
| | | {"name": "合同", "module": "销售管理", "table": "crm_contract", "doc": "HT编号,BPM 审批(0草稿/10审批中/20通过/30驳回/40取消/50作废),通过后可生成销售订单;支持 Excel 导入(一合同多产品明细行、合并单元格自动拉平,客户与物料按名称/编码校验);产品明细带出物料名称/编码/条码/规格型号/计量单位;删除合同时连带删除产品明细,不留孤儿数据", "upstream": ["报价单"], "downstream": ["销售订单", "回款计划", "开票"], "keywords": "合同 contract 审批 导入"}, |
| | | {"name": "发票", "module": "销售管理", "table": "crm_invoice", "doc": "KP编号(可留空自动生成、系统内唯一),BPM 审批,累计开票≤合同额,附件必传;支持 Excel 导入(仅新增不更新,合同须审核通过且本次金额≤合同剩余可开票额;同一批数据不可重复导入——同合同下发票号码相同、或发票号码留空时开票金额+发票抬头+开票日期相同即判重复,文件内重复行与库内已有单都拒该行并回显维度与实际值)", "upstream": ["合同"], "downstream": ["回款"], "keywords": "开票 发票 invoice 导入 去重"}, |
| | |
| | | {"name": "库存台账", "module": "仓储", "table": "mes_wm_material_stock", "doc": "物料×仓库×库区×库位×批次×供应商六元组,数量/冻结/占用/在途四量", "upstream": ["全部入库单"], "downstream": ["全部出库单", "事务流水"], "keywords": "库存 台账 stock 可用量"}, |
| | | {"name": "批次", "module": "仓储", "table": "mes_wm_batch", "doc": "关联物料/工单/供应商/客户/质量状态;SN 码含批次 uuid;条码支持18种业务对象", "upstream": ["入库单据"], "downstream": ["库存台账"], "keywords": "批次 batch SN 条码 追溯"}, |
| | | {"name": "IQC来料检验", "module": "质量", "table": "mes_qc_iqc", "doc": "到货通知/外协入库触发,草稿0→完成4,结论回写来源单据;行操作「出报告」仅在已完成时可见;支持 Excel 批量导入(工具栏「批量导入」,纯新增、导入单为草稿、编号可留空自动生成);同一批数据不可重复导入(物料+供应商+供应商批次+来料日期,文件内重复行与库内已有单都拒该行)", "upstream": ["到货通知", "外协入库", "Excel 批量导入"], "downstream": ["采购入库", "供应商退货", "NCR", "报告实例(出报告)"], "keywords": "IQC 来料检验 出报告 批量导入 导入模板"}, |
| | | {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;行操作「出报告」仅在已完成时可见;支持 Excel 批量导入(产品物料由生产工单推导,模板不填物料;工废+料废+其他废品不得超过不合格品数量);同一批数据不可重复导入(生产工单+检验类型+工位+检测日期,文件内重复行与库内已有单都拒该行)", "upstream": ["报工", "Excel 批量导入"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告 批量导入 导入模板"}, |
| | | {"name": "IPQC过程检验", "module": "质量", "table": "mes_qc_ipqc", "doc": "报工质检工序产出行触发,完成回调推进报工与产出入库;行操作「出报告」仅在已完成时可见;新增/修改时生产订单、工位均非必填:留空则不做工单与工位的工序校验、产品物料不再由工单推导(记录可无产品物料);质检方案必填,未选生产订单/生产任务时按「过程检验」种类列出全部启用方案并校验方案本身(存在/启用/种类含过程检验),不再要求与产品物料绑定;有工单/任务时仍按产品物料已绑定方案匹配;支持 Excel 批量导入(生产工单号、工位名称留空即不关联,工废+料废+其他废品不得超过不合格品数量);同一批数据不可重复导入(生产工单+检验类型+工位+检测日期,留空字段按空值比对,文件内重复行与库内已有单都拒该行)", "upstream": ["报工", "Excel 批量导入"], "downstream": ["产出入库", "NCR", "报告实例(出报告)"], "keywords": "IPQC 过程检验 巡检 出报告 批量导入 导入模板"}, |
| | | {"name": "OQC出货检验", "module": "质量", "table": "mes_qc_oqc", "doc": "销售出库提交触发,完成推进出库单待拣货;行操作「出报告」仅在已完成时可见;支持 Excel 批量导入(客户须在当前登录用户的客户数据权限范围内);同一批数据不可重复导入(物料+客户+批次号+出货日期,文件内重复行与库内已有单都拒该行)", "upstream": ["销售出库单", "Excel 批量导入"], "downstream": ["销售出库单(推进)", "报告实例(出报告)"], "keywords": "OQC 出货检验 出厂检验 出报告 批量导入 导入模板"}, |
| | | {"name": "RQC退货检验", "module": "质量", "table": "mes_qc_rqc", "doc": "生产退料/销售退货/售后退货触发,结论回调售后;行操作「出报告」仅在已完成时可见。措辞已统一为「退货检验」(原菜单父项与 mes_qc_type 字典写「退料检验」,与后端枚举/表注释/菜单权限子项不一致;RQC 三者都覆盖,故取「退货检验」)", "upstream": ["退料", "退货"], "downstream": ["退货入库", "售后回调", "报告实例(出报告)"], "keywords": "RQC 退货检验 退料检验 出报告"}, |
| | | {"name": "NCR不合格品", "module": "质量", "table": "mes_qc_ncr", "doc": "待评审0→已评审6→已处置7→已关闭8,处置:退货/报废/返工/让步接收/降级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["退货/报废/返工(台账)"], "keywords": "NCR 不合格 处置 特采 报废"}, |
| | |
| | | {"id": "sales", "name": "销售流程", "mermaid": "mermaid/02-sales-flow.mmd", "color": "#4f9cff", |
| | | "summary": "线索转化客户 → 商机推进 → 报价(可AI识别) → 合同 BPM 审批 → 生成 ERP 销售订单并审批 → 需生产行自动建 MPS → 发货通知 → 销售出库(OQC)回写 → 开票(BPM) → 回款(先开票校验)。", |
| | | "steps": [ |
| | | {"stage": "获客", "action": "线索转化客户、建联系人、立项商机", "roles": ["销售内勤"]}, |
| | | {"stage": "获客", "action": "线索转化客户、建联系人、立项商机(商机状态组/阶段选填,不选也能建单)", "roles": ["销售内勤"]}, |
| | | {"stage": "报价", "action": "商机赢单后建报价单(AI OCR 预填),转合同", "roles": ["销售内勤"]}, |
| | | {"stage": "签约", "action": "合同来源:手工新建 / 报价单转入 / Excel 导入(一合同多产品明细行);提交 BPM 审批(可选流程定义),通过=20", "roles": ["销售", "审批人"]}, |
| | | {"stage": "接单", "action": "合同一键生成销售订单(XSDD),销售订单再走 BPM;审批通过自动为 needProduction 明细创建 MPS", "roles": ["销售", "计划员"]}, |
| | |
| | | {"stage": "过程", "action": "报工质检工序产出行→IPQC,合格拆行入库、不合格生成 NCR", "roles": ["IPQC 质检员"]}, |
| | | {"stage": "出货", "action": "销售出库提交→OQC,完成后出库单进入待拣货", "roles": ["OQC 质检员"]}, |
| | | {"stage": "退货", "action": "退料/销售退货/售后退货→RQC,结论回调单据与售后", "roles": ["RQC 质检员"]}, |
| | | {"stage": "导入", "action": "IQC/IPQC/OQC 三列表各支持 Excel 批量导入(按钮在「新增」与「导出」之间,纯新增不做更新覆盖):一行一张单,编号留空自动生成,质检方案留空按物料+检验类型自动匹配,检测人员留空取当前登录用户;物料/供应商/客户/工单/工位按编码或名称精确匹配(IPQC 物料由工单推导,模板不填),合格+不合格=检测/接收数量、IPQC 工废+料废+其他废品≤不合格、OQC 客户须在本人数据权限内;逐行导入部分成功,失败行进 failureList 带行号与可执行动作,成功单一律待检验0;同一批数据不可重复导入:文件内重复行与库内已存在同业务键单据(IQC 物料+供应商+供应商批次+来料日期、IPQC 生产工单+检验类型+工位+检测日期、OQC 物料+客户+批次号+出货日期)均拒该行,提示含维度、双方实际值与「先删除该单据再重试」动作", "roles": ["质检员", "IQC 质检员", "IPQC 质检员", "OQC 质检员"]}, |
| | | {"stage": "导入", "action": "IQC/IPQC/OQC 三列表各支持 Excel 批量导入(按钮在「新增」与「导出」之间,纯新增不做更新覆盖):一行一张单,编号留空自动生成,质检方案留空按物料+检验类型自动匹配,检测人员留空取当前登录用户;物料/供应商/客户/工单/工位按编码或名称精确匹配(IPQC 物料由工单推导、生产工单号与工位名称可留空不关联,模板不填物料),合格+不合格=检测/接收数量、IPQC 工废+料废+其他废品≤不合格、OQC 客户须在本人数据权限内;逐行导入部分成功,失败行进 failureList 带行号与可执行动作,成功单一律待检验0;同一批数据不可重复导入:文件内重复行与库内已存在同业务键单据(IQC 物料+供应商+供应商批次+来料日期、IPQC 生产工单+检验类型+工位+检测日期(工单/工位留空按空值比对)、OQC 物料+客户+批次号+出货日期)均拒该行,提示含维度、双方实际值与「先删除该单据再重试」动作", "roles": ["质检员", "IQC 质检员", "IPQC 质检员", "OQC 质检员"]}, |
| | | {"stage": "不合格", "action": "NCR: 待评审→评审→处置(退货/报废/返工/让步接收/降级)→关闭", "roles": ["质量工程师"]}, |
| | | {"stage": "出件", "action": "两条出件入口:①通用 generate/preview(调用方自带上下文) 渲染 HTML 并冻结数据快照,编号 QR+日期+4位流水按天独立,preview 只渲染不落库不消耗编号,regenerate 用冻结快照重渲且数据与编号不变并作废已归档 PDF,导出 PDF 由后端调 Chromium 打印实例已存的 HTML 后归档附件中心;②generate-from-qc 由质检单直接出件(数据经 MesQcReportApi 从 MES 取,前端不拼上下文),出件前五道校验按业务优先级只报第一处:类型非法/单据不存在/未完成检验/未填判定/模板类型不符/无可用检验项,报告头另带单据原判定 qcResult+qcResultText 与引擎判定并存,响应回 undecidableCount 待判定项数与 warnings(分组标题跳过/指标已删/未录实测值/样品超3),同一张单据可反复出多份", "roles": ["质检员"]}, |
| | | {"stage": "建模板", "action": "AI 导入:上传既有检验报告文件(扫描件/照片、电子版 PDF、Word/Excel) → 识别成模板草稿(不落库) → 人工在设计器确认调整后保存为版本;.doc/.xls 旧格式不支持", "roles": ["质检员", "质量工程师"]}]}, |
| | |
| | | A[线索 crm_clue] -->|转化·仅一次| B[客户 crm_customer] |
| | | B --> C[联系人 crm_contact] |
| | | B --> D[商机 crm_business] |
| | | D -->|自定义阶段推进| E{商机结束} |
| | | D -->|自定义阶段推进(状态组选填)| E{商机结束} |
| | | E -->|赢单| F[报价单 QT·草稿0→已转合同10] |
| | | E -->|输单或无效| X[结束] |
| | | F -->|转合同·带入客户产品折扣| G[合同 HT·草稿0→审批中10] |
| | |
| | | A[线索 crm_clue] -->|转化·仅一次| B[客户 crm_customer] |
| | | B --> C[联系人 crm_contact] |
| | | B --> D[商机 crm_business] |
| | | D -->|自定义阶段推进| E{商机结束} |
| | | D -->|自定义阶段推进(状态组选填)| E{商机结束} |
| | | E -->|赢单| F[报价单 QT·草稿0→已转合同10] |
| | | E -->|输单或无效| X[结束] |
| | | F -->|转合同·带入客户产品折扣| G[合同 HT·草稿0→审批中10] |
| | |
| | | `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间', |
| | | `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间', |
| | | `owner_user_id` bigint DEFAULT NULL COMMENT '负责人的用户编号', |
| | | `status_type_id` bigint NOT NULL COMMENT '商机状态组编号', |
| | | `status_id` bigint NOT NULL COMMENT '商机状态编号', |
| | | `status_type_id` bigint DEFAULT NULL COMMENT '商机状态组编号', |
| | | `status_id` bigint DEFAULT NULL COMMENT '商机状态编号', |
| | | `end_status` int DEFAULT NULL COMMENT '结束状态', |
| | | `end_remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '结束时的备注', |
| | | `deal_time` datetime DEFAULT NULL COMMENT '预计成交日期', |
| | |
| | | @ExcelProperty("负责人部门") |
| | | private String ownerUserDeptName; |
| | | |
| | | @Schema(description = "商机状态组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714") |
| | | @Schema(description = "商机状态组编号(未设置状态组时为空)", example = "25714") |
| | | private Long statusTypeId; |
| | | @Schema(description = "商机状组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中") |
| | | @Schema(description = "商机状组名字", example = "进行中") |
| | | @ExcelProperty("商机状态组") |
| | | private String statusTypeName; |
| | | |
| | | @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30320") |
| | | @Schema(description = "商机状态编号(未设置状态组时为空)", example = "30320") |
| | | private Long statusId; |
| | | @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "跟进中") |
| | | @Schema(description = "状态名称", example = "跟进中") |
| | | @ExcelProperty("商机状态") |
| | | private String statusName; |
| | | |
| | |
| | | @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME) |
| | | private Long ownerUserId; |
| | | |
| | | @Schema(description = "商机状态组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714") |
| | | @Schema(description = "商机状态组编号(非必填:未选择时不带默认阶段,也不参与状态组统计)", example = "25714") |
| | | @DiffLogField(name = "商机状态组") |
| | | @NotNull(message = "商机状态组不能为空") |
| | | private Long statusTypeId; |
| | | |
| | | @Schema(description = "预计成交日期") |
| | |
| | | ErrorCode BUSINESS_UPDATE_STATUS_FAIL_END_STATUS = new ErrorCode(1_020_002_002, "更新商机状态失败,原因:已经是结束状态"); |
| | | ErrorCode BUSINESS_UPDATE_STATUS_FAIL_STATUS_EQUALS = new ErrorCode(1_020_002_003, "更新商机状态失败,原因:已经是该状态"); |
| | | ErrorCode BUSINESS_CONTACT_CUSTOMER_NOT_MATCH = new ErrorCode(1_020_002_004, "商机关联的联系人不属于该客户"); |
| | | ErrorCode BUSINESS_UPDATE_STATUS_FAIL_NO_STATUS_TYPE = new ErrorCode(1_020_002_005, "商机【{}】未设置「商机状态组」,无法变更「商机阶段」。请在新增商机时选择「商机状态组」(需先在【营销管理 - 系统配置 - 商机状态配置】中创建状态组),或改为只变更「结束状态」"); |
| | | |
| | | // ========== 联系人管理 1-020-003-000 ========== |
| | | ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在"); |
| | |
| | | |
| | | // 2.1 插入商机 |
| | | CrmBusinessDO business = BeanUtils.toBean(createReqVO, CrmBusinessDO.class); |
| | | business.setStatusId(businessStatusService.getBusinessStatusListByTypeId(createReqVO.getStatusTypeId()).get(0).getId()); // 默认状态 |
| | | // 商机状态组为选填:未选择时不设置默认阶段,也不参与「商机状态组」维度的统计 |
| | | if (createReqVO.getStatusTypeId() != null) { |
| | | List<CrmBusinessStatusDO> statusList = businessStatusService.getBusinessStatusListByTypeId(createReqVO.getStatusTypeId()); |
| | | business.setStatusId(CollUtil.isNotEmpty(statusList) ? statusList.get(0).getId() : null); // 默认状态 |
| | | } |
| | | calculateTotalPrice(business, businessProducts); |
| | | businessMapper.insert(business); |
| | | // 2.2 插入商机关联商品 |
| | |
| | | // 1.3 校验商机状态 |
| | | CrmBusinessStatusDO status = null; |
| | | if (reqVO.getStatusId() != null) { |
| | | // 商机状态组为选填:未设置状态组的商机没有可选阶段,不允许变更「商机阶段」 |
| | | if (business.getStatusTypeId() == null) { |
| | | throw exception(BUSINESS_UPDATE_STATUS_FAIL_NO_STATUS_TYPE, business.getName()); |
| | | } |
| | | status = businessStatusService.validateBusinessStatus(business.getStatusTypeId(), reqVO.getStatusId()); |
| | | } |
| | | // 1.4 校验是不是状态没变更 |
| | |
| | | // 3. 记录操作日志上下文 |
| | | LogRecordContext.putVariable("businessName", business.getName()); |
| | | LogRecordContext.putVariable("oldStatusName", getBusinessStatusName(business.getEndStatus(), |
| | | businessStatusService.getBusinessStatus(business.getStatusId()))); |
| | | business.getStatusId() != null ? businessStatusService.getBusinessStatus(business.getStatusId()) : null)); |
| | | LogRecordContext.putVariable("newStatusName", getBusinessStatusName(reqVO.getEndStatus(), status)); |
| | | } |
| | | |
| | |
| | | |
| | | // ========== 生产关联 ========== |
| | | |
| | | @Schema(description = "生产工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") |
| | | @NotNull(message = "生产工单不能为空") |
| | | @Schema(description = "生产工单 ID(非必填:未选择时不校验工单、不按工单推导物料)", example = "10") |
| | | private Long workOrderId; |
| | | |
| | | @Schema(description = "生产任务 ID", example = "20") |
| | | private Long taskId; |
| | | |
| | | @Schema(description = "工位 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") |
| | | @NotNull(message = "工位不能为空") |
| | | @Schema(description = "工位 ID(非必填:未选择时不校验工位关联的工序)", example = "30") |
| | | private Long workstationId; |
| | | |
| | | @Schema(description = "工序 ID", example = "40") |
| | |
| | | default List<MesQcIpqcDO> selectListByImportKey(Long workOrderId, Integer type, Long workstationId, |
| | | LocalDateTime inspectDate) { |
| | | return selectList(new LambdaQueryWrapperX<MesQcIpqcDO>() |
| | | .eq(MesQcIpqcDO::getWorkOrderId, workOrderId) |
| | | // 生产工单、工位为选填:留空时比对其它同样留空的检验单 |
| | | .eq(workOrderId != null, MesQcIpqcDO::getWorkOrderId, workOrderId) |
| | | .isNull(workOrderId == null, MesQcIpqcDO::getWorkOrderId) |
| | | .eq(MesQcIpqcDO::getType, type) |
| | | .eq(MesQcIpqcDO::getWorkstationId, workstationId) |
| | | .eq(workstationId != null, MesQcIpqcDO::getWorkstationId, workstationId) |
| | | .isNull(workstationId == null, MesQcIpqcDO::getWorkstationId) |
| | | .eq(inspectDate != null, MesQcIpqcDO::getInspectDate, inspectDate) |
| | | .isNull(inspectDate == null, MesQcIpqcDO::getInspectDate) |
| | | .orderByAsc(MesQcIpqcDO::getId)); |
| | |
| | | ErrorCode QC_IPQC_SOURCE_LINE_NOT_BELONG = new ErrorCode(1_040_604_009, "来源单据行不属于该报工单"); |
| | | ErrorCode QC_IPQC_SOURCE_LINE_REQUIRED = new ErrorCode(1_040_604_010, "来源单据类型为报工时,来源产出行 ID 不能为空"); |
| | | ErrorCode QC_IPQC_SOURCE_LINE_NOT_PENDING = new ErrorCode(1_040_604_011, "来源产出行不是待检验状态"); |
| | | ErrorCode QC_IPQC_TEMPLATE_IS_DISABLE = new ErrorCode(1_040_604_012, "所选质检方案【{}】已禁用,请重新选择已启用的质检方案"); |
| | | ErrorCode QC_IPQC_TEMPLATE_TYPE_NOT_MATCH = new ErrorCode(1_040_604_013, "所选质检方案【{}】的检测种类不包含过程检验,请重新选择检测种类包含过程检验的质检方案"); |
| | | // ========== MES 质量管理-过程检验行(1-040-604-100) ========== |
| | | ErrorCode QC_IPQC_LINE_NOT_EXISTS = new ErrorCode(1_040_604_100, "过程检验行不存在"); |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 解析生产工单编号,基于工单号 |
| | | * 解析生产工单编号,基于工单号(选填:工单号留空时返回 null,表示该行不关联生产工单) |
| | | */ |
| | | public Long resolveWorkOrderId(String workOrderCode) { |
| | | if (StrUtil.isBlank(workOrderCode)) { |
| | | throw new IllegalArgumentException("生产工单号不能为空:请填写模板中该行的「生产工单号」列"); |
| | | return null; |
| | | } |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderCode.trim()); |
| | | if (workOrder == null) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 解析工位编号,基于工位名称 |
| | | * 解析工位编号,基于工位名称(选填:工位名称留空时返回 null,表示该行不关联工位) |
| | | */ |
| | | public Long resolveWorkstationId(String workstationName) { |
| | | if (StrUtil.isBlank(workstationName)) { |
| | | throw new IllegalArgumentException("工位名称不能为空:请填写模板中该行的「工位名称」列"); |
| | | return null; |
| | | } |
| | | MesMdWorkstationDO workstation = workstationService.getWorkstationByName(workstationName.trim()); |
| | | if (workstation == null) { |
| | |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.enums.DictTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum; |
| | | import cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum; |
| | |
| | | import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService; |
| | |
| | | /** |
| | | * IPQC 保存数据校验上下文 |
| | | * |
| | | * @param workstation 工位 |
| | | * @param workstation 工位(可选,未指定 workstationId 时为空) |
| | | * @param task 生产任务(可选,指定 taskId 时非空) |
| | | * @param workOrder 生产工单(可选,未指定 workOrderId 时为空) |
| | | */ |
| | | private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task) {} |
| | | private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task, MesProWorkOrderDO workOrder) {} |
| | | |
| | | @Resource |
| | | private MesQcIpqcMapper ipqcMapper; |
| | | |
| | | @Resource |
| | | private MesQcTemplateItemService templateItemService; |
| | | @Resource |
| | | private MesQcTemplateService templateService; |
| | | @Resource |
| | | private MesQcIpqcLineService ipqcLineService; |
| | | @Resource |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createIpqc(MesQcIpqcSaveReqVO createReqVO) { |
| | | // 1.1 校验数据 |
| | | // 1.1 校验数据(生产订单、工位均非必填) |
| | | SaveDataContext ctx = validateIpqcSaveData(createReqVO); |
| | | // 1.2 校验工单存在 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId()); |
| | | // 1.3 确定检验物料:有任务时取任务的物料,无任务时取工单的产品 |
| | | Long itemId = (ctx.task() != null) ? ctx.task().getItemId() : workOrder.getProductId(); |
| | | // 1.4 根据物料 + IPQC 类型查找可用质检方案(支持前端显式指定) |
| | | List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList( |
| | | itemId, MesQcTypeEnum.IPQC.getType(), null, null); |
| | | if (CollUtil.isEmpty(templateItems)) { |
| | | throw exception(QC_NO_TEMPLATE); |
| | | } |
| | | MesQcTemplateItemDO templateItem = templateItems.get(0); |
| | | if (createReqVO.getTemplateId() != null) { |
| | | templateItem = templateItems.stream() |
| | | .filter(available -> Objects.equals(available.getTemplateId(), createReqVO.getTemplateId())) |
| | | .findFirst().orElse(null); |
| | | if (templateItem == null) { |
| | | throw exception(QC_NO_TEMPLATE); // 指定了方案但不在匹配列表中,拒绝 |
| | | } |
| | | } |
| | | Long templateId = templateItem.getTemplateId(); |
| | | // 1.5 获取来源单据编号 |
| | | // 1.2 确定检验物料:有任务时取任务的物料,有工单时取工单的产品,都没有时取请求指定的产品物料 |
| | | Long itemId = resolveItemId(createReqVO, ctx); |
| | | // 1.3 确定质检方案:显式指定时校验方案本身可用,未指定时按产品物料已绑定方案兜底 |
| | | Long templateId = resolveTemplateId(itemId, createReqVO.getTemplateId()); |
| | | // 1.4 获取来源单据编号 |
| | | String sourceDocCode = validateAndGetSourceDocCode( |
| | | createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId()); |
| | | |
| | | // 2. 插入主表 |
| | | MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class) |
| | | .setItemId(itemId) |
| | | .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId())) |
| | | .setProcessId(getProcessId(itemId, |
| | | ctx.workstation() != null ? ctx.workstation().getProcessId() : null)) |
| | | .setTemplateId(templateId) |
| | | .setSourceDocCode(sourceDocCode) |
| | | .setStatus(MesQcStatusEnum.DRAFT.getStatus()); |
| | |
| | | updateObj.setTemplateId(null); // 不允许修改模板 |
| | | updateObj.setItemId(null); // 不允许修改物料 |
| | | updateObj.setWorkOrderId(null); // 不允许修改工单 |
| | | updateObj.setProcessId(getProcessId(existIpqc.getItemId(), ctx.workstation().getProcessId())); |
| | | updateObj.setProcessId(getProcessId(existIpqc.getItemId(), |
| | | ctx.workstation() != null ? ctx.workstation().getProcessId() : null)); |
| | | ipqcMapper.updateById(updateObj); |
| | | } |
| | | |
| | | private SaveDataContext validateIpqcSaveData(MesQcIpqcSaveReqVO reqVO) { |
| | | // 校验编号唯一 |
| | | validateIpqcCodeUnique(reqVO.getId(), reqVO.getCode()); |
| | | // 校验工位、检测人员存在 |
| | | MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId()); |
| | | // 校验工位存在(工位非必填,未选择时跳过) |
| | | MesMdWorkstationDO workstation = reqVO.getWorkstationId() != null |
| | | ? workstationService.validateWorkstationExists(reqVO.getWorkstationId()) : null; |
| | | adminUserApi.validateUser(reqVO.getInspectorUserId()); |
| | | // 校验工单存在 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()); |
| | | if (reqVO.getProcessId() != null && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) { |
| | | // 校验工单存在且已确认(生产订单非必填,未选择时跳过) |
| | | MesProWorkOrderDO workOrder = reqVO.getWorkOrderId() != null |
| | | ? workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()) : null; |
| | | if (workstation != null && reqVO.getProcessId() != null |
| | | && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) { |
| | | throw exception(PRO_WORKSTATION_PROCESS_MISMATCH); |
| | | } |
| | | // 校验任务关系(如果指定了任务) |
| | |
| | | if (reqVO.getTaskId() != null) { |
| | | task = taskService.validateTaskNotFinished(reqVO.getTaskId()); |
| | | validateTaskRelation(task, workstation, workOrder, reqVO); |
| | | } else { |
| | | } else if (workOrder != null && reqVO.getItemId() != null |
| | | && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { |
| | | // 无任务时,itemId 仍需与工单产品一致 |
| | | if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { |
| | | throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); |
| | | } |
| | | throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); |
| | | } |
| | | return new SaveDataContext(workstation, task); |
| | | return new SaveDataContext(workstation, task, workOrder); |
| | | } |
| | | |
| | | /** |
| | | * 确定检验物料:任务优先,其次工单产品,都没有时取请求指定的产品物料(都未指定则为空) |
| | | */ |
| | | private Long resolveItemId(MesQcIpqcSaveReqVO reqVO, SaveDataContext ctx) { |
| | | if (ctx.task() != null) { |
| | | return ctx.task().getItemId(); |
| | | } |
| | | if (ctx.workOrder() != null) { |
| | | return ctx.workOrder().getProductId(); |
| | | } |
| | | return reqVO.getItemId(); |
| | | } |
| | | |
| | | /** |
| | | * 确定质检方案 |
| | | * |
| | | * <p>产品物料已绑定 IPQC 方案时沿用原逻辑:未显式指定取绑定列表首个,显式指定则必须命中绑定列表; |
| | | * <p>产品物料未匹配到任何方案(未选生产订单/生产任务/产品物料)时,显式指定的方案改为校验方案本身: |
| | | * 存在、已启用、检测种类包含过程检验,不再要求与产品物料绑定。 |
| | | */ |
| | | private Long resolveTemplateId(Long itemId, Long explicitTemplateId) { |
| | | List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList( |
| | | itemId, MesQcTypeEnum.IPQC.getType(), null, null); |
| | | if (CollUtil.isNotEmpty(templateItems)) { |
| | | if (explicitTemplateId == null) { |
| | | return templateItems.get(0).getTemplateId(); |
| | | } |
| | | return templateItems.stream() |
| | | .filter(available -> Objects.equals(available.getTemplateId(), explicitTemplateId)) |
| | | .findFirst() |
| | | .orElseThrow(() -> exception(QC_NO_TEMPLATE)) // 指定了方案但不在匹配列表中,拒绝 |
| | | .getTemplateId(); |
| | | } |
| | | // 产品物料未匹配到任何 IPQC 方案 |
| | | if (explicitTemplateId != null) { |
| | | return validateTemplateUsable(explicitTemplateId); |
| | | } |
| | | throw new ServiceException(QC_IPQC_NO_TEMPLATE.getCode(), buildNoTemplateMessage(itemId)); |
| | | } |
| | | |
| | | /** |
| | | * 校验质检方案本身可用:存在、已启用、检测种类包含过程检验,返回方案编号 |
| | | */ |
| | | private Long validateTemplateUsable(Long templateId) { |
| | | MesQcTemplateDO template = templateService.getTemplate(templateId); |
| | | if (template == null) { |
| | | throw new ServiceException(QC_TEMPLATE_NOT_EXISTS.getCode(), StrUtil.format( |
| | | "所选质检方案不存在(ID={}),可能已被删除,请刷新后重新选择质检方案", templateId)); |
| | | } |
| | | if (!CommonStatusEnum.isEnable(template.getStatus())) { |
| | | throw exception(QC_IPQC_TEMPLATE_IS_DISABLE, template.getName()); |
| | | } |
| | | if (!CollUtil.contains(template.getTypes(), MesQcTypeEnum.IPQC.getType())) { |
| | | throw exception(QC_IPQC_TEMPLATE_TYPE_NOT_MATCH, template.getName()); |
| | | } |
| | | return template.getId(); |
| | | } |
| | | |
| | | /** |
| | | * 构建「无可用质检方案」的提示:区分「有产品物料但未绑定方案」与「未选工单/任务/物料」 |
| | | */ |
| | | private String buildNoTemplateMessage(Long itemId) { |
| | | if (itemId == null) { |
| | | return "未选择生产订单/生产任务/产品物料,无法自动匹配质检方案:" |
| | | + "请直接选择「质检方案」后重试,或先选择生产订单/生产任务以自动带出该产品已绑定的方案。"; |
| | | } |
| | | return StrUtil.format("产品物料[{}]未绑定「过程检验」质检方案:请在【质量管理-质检方案】中为该物料" |
| | | + "配置检测种类包含过程检验的方案,或在本单手动选择质检方案。", describeItem(itemId)); |
| | | } |
| | | |
| | | /** |
| | | * 描述产品物料,用于异常提示:优先给出「编码 名称」,查不到时回退为物料编号 |
| | | */ |
| | | private String describeItem(Long itemId) { |
| | | MesMdItemDO item = itemService.getItem(itemId); |
| | | return item != null ? item.getCode() + " " + item.getName() : "物料编号 ID=" + itemId; |
| | | } |
| | | |
| | | private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation, |
| | | MesProWorkOrderDO workOrder, MesQcIpqcSaveReqVO reqVO) { |
| | | if (ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) { |
| | | // 生产订单、工位均非必填,未选择时跳过对应比对 |
| | | if (workOrder != null && ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) { |
| | | throw exception(PRO_TASK_WORK_ORDER_MISMATCH); |
| | | } |
| | | if (ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) { |
| | | if (workstation != null && ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) { |
| | | throw exception(PRO_TASK_WORKSTATION_MISMATCH); |
| | | } |
| | | Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId() : workstation.getProcessId(); |
| | | Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId() |
| | | : (workstation != null ? workstation.getProcessId() : null); |
| | | if (expectedProcessId != null && ObjUtil.notEqual(task.getProcessId(), expectedProcessId)) { |
| | | throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH); |
| | | } |
| | |
| | | + "。请修改模板中的「检验单编号」,或先在系统中删除该编号的检验单后重新导入"); |
| | | } |
| | | reqVO.setCode(code); |
| | | // 2. 名称、检验类型、工单、工位、质检方案 |
| | | // 2. 名称、检验类型、工单、工位、质检方案(生产工单号、工位名称留空即不关联,不再拦截) |
| | | if (StrUtil.isBlank(row.getName())) { |
| | | throw new IllegalArgumentException("检验单名称不能为空:请填写模板中该行的「检验单名称」列"); |
| | | } |