采购申请批量提交、订单导入重复校验;修复考勤异常申请字段不匹配
- 采购订单导入:按采购单编号或订单指纹识别重复,新增 overwrite 参数与 duplicateList;
前端检测到重复弹框确认后才覆盖;覆盖前校验入库、退货、发票、采购申请、审批流程占用
- 采购申请:新增批量提交接口(逐张独立事务)与前端批量提交按钮,按 50 张分批调用;
审批通过自动生成订单前,优先复用已审核且未入库的同内容订单并回填关联,避免重复生成
- 考勤异常申请:前后端字段名对齐(exceptionDate/type),类型选项与后端枚举一致,
列表异常日期区间筛选生效;新增时自动带出当前员工与部门,无员工档案时给出明确提示
- 新增脚本:采购申请批量提交按钮权限、HRM 审批状态字典
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | {"stage": "交ä»", "action": "åè´§éç¥å®¡æ ¸(å ç¨)â éå®åºåº OQC â åè¿ â åºåºååé¨å/å
¨é¨åºåº", "roles": ["ä»ç®¡", "è´¨æ£"]}, |
| | | {"stage": "忬¾", "action": "å¼ç¥¨ BPM(累计â¤ååé¢) â 忬¾å¼ºå¶å
å¼ç¥¨(累计â¤ååå©ä½ä¸å¼ç¥¨å©ä½)", "roles": ["è´¢å¡", "éå®"]}]}, |
| | | {"id": "purchase", "name": "éè´æµç¨", "mermaid": "mermaid/03-purchase-flow.mmd", "color": "#5fc79e", |
| | | "summary": "SRM åå
¥/宿 便º â éè´ç³è¯· BPM â éè´è®¢å BPM â 确认æ¶è´§çæ MES å°è´§éç¥ â IQC(åæ ¼/ç¹éå
¥åºÂ·éè´§/æ¥åº) â éè´å
¥åºåå订åä¸ç³è¯· â æ¥ç¥¨(AI OCR) â 仿¬¾å BPMã", |
| | | "summary": "SRM åå
¥/宿 便º â éè´ç³è¯· BPM(æ¯æåå¼ /æ¹éæäº¤) â éè´è®¢å导å
¥(é夿£æµ+å¼¹æ¡è¦ç) æ BPM â 确认æ¶è´§çæ MES å°è´§éç¥ â IQC(åæ ¼/ç¹éå
¥åºÂ·éè´§/æ¥åº) â éè´å
¥åºåå订åä¸ç³è¯· â æ¥ç¥¨(AI OCR) â 仿¬¾å BPMã", |
| | | "steps": [ |
| | | {"stage": "寻æº", "action": "ä¾åºååå
¥(BPM)å»ºæ¡£ï¼ææ 宿´çæç®ææ¯ä»·ï¼å®æ 审æ¹éè¿èªå¨çæéè´è®¢å", "roles": ["éè´å¼å"]}, |
| | | {"stage": "ç³è¯·", "action": "éè´ç³è¯·(CGSQ)æäº¤ BPMï¼é©³åå¯éç½®åè稿ï¼éè¿ä¸æä¾åºåèªå¨çæè®¢å", "roles": ["éè´å", "审æ¹äºº"]}, |
| | | {"stage": "ä¸å", "action": "éè´è®¢å(CGDD)èªå¨æäº¤ BPM(15â20)", "roles": ["éè´å"]}, |
| | | {"stage": "ç³è¯·", "action": "éè´ç³è¯·(CGSQ)åå¼ æå¾éå¤å¼ æ¹éæäº¤ BPM(ä»
æªæäº¤/驳åå¯æäº¤ï¼éåç¬ç«äºå¡ï¼å¤±è´¥é¡¹åç¬æç¤º)ï¼é©³åå¯éç½®åè稿ï¼éè¿ä¸æä¾åºåæ¶ï¼å
æâä¾åºå+æç»(ç©æ/æ°é/åä»·/ç¨ç)âå¹é
å·²å®¡æ ¸è®¢åï¼å¹é
å°åå
³èç³è¯·ãä¸éå¤çæï¼æªå¹é
æèªå¨çæ", "roles": ["éè´å", "审æ¹äºº"]}, |
| | | {"stage": "ä¸å", "action": "éè´è®¢å(CGDD)å¯ Excel 导å
¥(å¾éå®¡æ ¸ç´æ¥ç½® 20)æèªå¨æäº¤ BPM(15â20)ï¼å¯¼å
¥æ¶æåå·æâä¾åºå+æç»âè¯å«éå¤ï¼éå¤é¡¹é»è®¤ä¸å¯¼å
¥å¹¶å¼¹æ¡æç¤ºï¼ç¨æ·ç¡®è®¤åæè¦çå·²æè®¢åï¼å·²å
¥åº/å·²éè´§/å·²å
³èå票æéè´ç³è¯·ç订åç¦æ¢è¦ç", "roles": ["éè´å"]}, |
| | | {"stage": "æ¶è´§", "action": "确认æ¶è´§çæå°è´§éç¥(0âå¾
è´¨æ£2/å¾
å
¥åº3â4)ï¼éæ£ç©æèªå¨å建 IQC", "roles": ["ä»ç®¡", "IQC è´¨æ£å"]}, |
| | | {"stage": "å
¥åº", "action": "IQC åæ ¼/ç¹éâéè´å
¥åº(å¾
䏿¶âå¾
æ§è¡â宿)åºå+ï¼åå订å inCount/inStatus ä¸ç³è¯·æç»", "roles": ["ä»ç®¡"]}, |
| | | {"stage": "éè´§", "action": "IQC ä¸åæ ¼éè´§âä¾åºåéè´§åºåº(éæéå®å°è®¢åè¡)ï¼æ¥åºèµ° NCR", "roles": ["ä»ç®¡"]}, |
| | |
| | | {"key": "erp", "name": "è¿éåä¸è´¢å¡", "color": "#5fc79e", "tag": "ERP", "flow": "purchase", |
| | | "positioning": "åè´¸ä¸èµé忮䏿¢ï¼å MES åèµ·å®ç©æ¶å", |
| | | "problem": "éè´éå®åæ®å®¡æ¹ãæ¥ç¥¨ä»æ¬¾æ§å¶ãè®¢åæ§è¡è¿åº¦åå", |
| | | "features": ["éè´ç³è¯·(BPM)â订å(èªå¨/æå¨çæ)", "确认æ¶è´§âMESå°è´§éç¥", "å
¥åº/éè´§æ°éç¶æåå", "éè´æ¥ç¥¨(éé¢â¤è®¢å)", "仿¬¾å(å¿
é¡»å
³èæ¥ç¥¨Â·BPM)", "éå®è®¢å(BPM·审æ¹éè¿èªå¨å»ºMPS)", "åºåºç¶æåå", "æ¶æ¬¾å(å
³èCRM客æ·)", "ç»ç®è´¦æ·", "AIå票OCR"], |
| | | "features": ["éè´ç³è¯·(BPMÂ·æ¯ææ¹éæäº¤)â订å(èªå¨/æå¨çæÂ·ç¸åæç»ä¸éå¤çæ)", "éè´è®¢å导å
¥(é夿£æµÂ·å¼¹æ¡ç¡®è®¤è¦ç)", "确认æ¶è´§âMESå°è´§éç¥", "å
¥åº/éè´§æ°éç¶æåå", "éè´æ¥ç¥¨(éé¢â¤è®¢å)", "仿¬¾å(å¿
é¡»å
³èæ¥ç¥¨Â·BPM)", "éå®è®¢å(BPM·审æ¹éè¿èªå¨å»ºMPS)", "åºåºç¶æåå", "æ¶æ¬¾å(å
³èCRM客æ·)", "ç»ç®è´¦æ·", "AIå票OCR"], |
| | | "objects": ["éè´ç³è¯·", "éè´è®¢å", "éè´æ¥ç¥¨", "仿¬¾å", "éå®è®¢å", "æ¶æ¬¾å", "ç»ç®è´¦æ·"], |
| | | "roles": ["éè´å", "éå®å", "åºçº³", "ä¼è®¡"], |
| | | "upstream": ["CRM(åå)", "SRM(宿 )", "ä¸»æ°æ®"], "downstream": ["ç产(MPS)", "ä»å¨(æ¶å)"], |
| | |
| | | {"name": "ä¾åºå", "module": "ä¾åºå管ç", "table": "srm_supplier", "doc": "ä¾åºåæ¡£æ¡ï¼æ§ç
§/ç¨å·/é¶è¡/è系人ï¼ERP ä¸ MES å
±ç¨", "upstream": ["åå
¥ç³è¯·"], "downstream": ["éè´è®¢å", "å°è´§éç¥", "ä¾åºåéè´§", "绩æè¯ä»·"], "keywords": "ä¾åºå supplier vendor"}, |
| | | {"name": "åå
¥ç³è¯·", "module": "ä¾åºå管ç", "table": "srm_supplier_apply", "doc": "APç¼å·ï¼BPM 审æ¹(éè´/è´¨é/è´¢å¡)ï¼éè¿èªå¨å»ºä¾åºåæ¡£æ¡", "upstream": [], "downstream": ["ä¾åºå"], "keywords": "åå
¥ ä¾åºåç³è¯· apply"}, |
| | | {"name": "ææ é¡¹ç®", "module": "ä¾åºå管ç", "table": "srm_tender_project", "doc": "宿´ç(åå¸âææ â弿 âè¯æ â宿 )ä¸ç®ææ¯ä»·ä¸¤ç§æ¨¡å¼", "upstream": [], "downstream": ["宿 âéè´è®¢å"], "keywords": "ææ ææ æ¯ä»· 宿 tender 寻æº"}, |
| | | {"name": "éè´ç³è¯·", "module": "è¿éå", "table": "erp_purchase_request", "doc": "CGSQç¼å·ï¼BPM 审æ¹ï¼éè¿åæä¾åºåèªå¨çæè®¢å", "upstream": ["ç产缺å£(人工)"], "downstream": ["éè´è®¢å"], "keywords": "éè´ç³è¯· è¯·è´ request"}, |
| | | {"name": "éè´è®¢å", "module": "è¿éå", "table": "erp_purchase_order", "doc": "CGDDç¼å·ï¼BPM 审æ¹(15审æ¹ä¸/20å·²å®¡æ ¸)ï¼å
¥åºç¶æ:æª0é¨å1å
¨é¨2", "upstream": ["éè´ç³è¯·", "宿 "], "downstream": ["å°è´§éç¥", "éè´æ¥ç¥¨", "ä¾åºåéè´§(约æç©æ)"], "keywords": "éè´è®¢å purchase"}, |
| | | {"name": "éè´ç³è¯·", "module": "è¿éå", "table": "erp_purchase_request", "doc": "CGSQç¼å·ï¼BPM 审æ¹(æ¯ææ¹éæäº¤)ï¼éè¿åæä¾åºåæ¶å
å¹é
ç¸åæç»çå·²å®¡æ ¸è®¢åï¼å¹é
ä¸å°æèªå¨çæ", "upstream": ["ç产缺å£(人工)"], "downstream": ["éè´è®¢å"], "keywords": "éè´ç³è¯· è¯·è´ request"}, |
| | | {"name": "éè´è®¢å", "module": "è¿éå", "table": "erp_purchase_order", "doc": "CGDDç¼å·ï¼BPM 审æ¹(15审æ¹ä¸/20å·²å®¡æ ¸)æ Excel 导å
¥ï¼å¯¼å
¥é夿¶å¼¹æ¡ç¡®è®¤è¦çï¼å
¥åºç¶æ:æª0é¨å1å
¨é¨2", "upstream": ["éè´ç³è¯·", "宿 "], "downstream": ["å°è´§éç¥", "éè´æ¥ç¥¨", "ä¾åºåéè´§(约æç©æ)"], "keywords": "éè´è®¢å purchase"}, |
| | | {"name": "éè´æ¥ç¥¨", "module": "è¿éå", "table": "erp_purchase_invoice", "doc": "FPDç¼å·ï¼AI OCR é¢å¡«ï¼ç´¯è®¡â¤è®¢åæ»ä»·ï¼ä»æ¬¾åçåç½®", "upstream": ["éè´è®¢å"], "downstream": ["仿¬¾å"], "keywords": "æ¥ç¥¨ å票 éè´å票"}, |
| | | {"name": "仿¬¾å", "module": "è´¢å¡", "table": "erp_finance_payment", "doc": "FKDç¼å·ï¼å¿
é¡»å
³èæ¥ç¥¨+éä»¶ï¼ç´¯è®¡â¤æ¥ç¥¨éé¢ï¼BPM 审æ¹", "upstream": ["éè´æ¥ç¥¨"], "downstream": ["ç»ç®è´¦æ·"], "keywords": "仿¬¾ åºä» payment"}, |
| | | {"name": "æ¶æ¬¾å", "module": "è´¢å¡", "table": "erp_finance_receipt", "doc": "SKDç¼å·ï¼å
³è CRM å®¢æ·æ ¡éªï¼æ BPM(æå¨å®¡æ ¸)", "upstream": ["éå®(å¼±å
³è)"], "downstream": ["ç»ç®è´¦æ·"], "keywords": "æ¶æ¬¾ åºæ¶ receipt"}, |
| | |
| | | {"stage": "交ä»", "action": "åè´§éç¥å®¡æ ¸(å ç¨)â éå®åºåº OQC â åè¿ â åºåºååé¨å/å
¨é¨åºåº", "roles": ["ä»ç®¡", "è´¨æ£"]}, |
| | | {"stage": "忬¾", "action": "å¼ç¥¨ BPM(累计â¤ååé¢) â 忬¾å¼ºå¶å
å¼ç¥¨(累计â¤ååå©ä½ä¸å¼ç¥¨å©ä½)", "roles": ["è´¢å¡", "éå®"]}]}, |
| | | {"id": "purchase", "name": "éè´æµç¨", "mermaid": "mermaid/03-purchase-flow.mmd", "color": "#5fc79e", |
| | | "summary": "SRM åå
¥/宿 便º â éè´ç³è¯· BPM â éè´è®¢å BPM â 确认æ¶è´§çæ MES å°è´§éç¥ â IQC(åæ ¼/ç¹éå
¥åºÂ·éè´§/æ¥åº) â éè´å
¥åºåå订åä¸ç³è¯· â æ¥ç¥¨(AI OCR) â 仿¬¾å BPMã", |
| | | "summary": "SRM åå
¥/宿 便º â éè´ç³è¯· BPM(æ¯æåå¼ /æ¹éæäº¤) â éè´è®¢å导å
¥(é夿£æµ+å¼¹æ¡è¦ç) æ BPM â 确认æ¶è´§çæ MES å°è´§éç¥ â IQC(åæ ¼/ç¹éå
¥åºÂ·éè´§/æ¥åº) â éè´å
¥åºåå订åä¸ç³è¯· â æ¥ç¥¨(AI OCR) â 仿¬¾å BPMã", |
| | | "steps": [ |
| | | {"stage": "寻æº", "action": "ä¾åºååå
¥(BPM)å»ºæ¡£ï¼ææ 宿´çæç®ææ¯ä»·ï¼å®æ 审æ¹éè¿èªå¨çæéè´è®¢å", "roles": ["éè´å¼å"]}, |
| | | {"stage": "ç³è¯·", "action": "éè´ç³è¯·(CGSQ)æäº¤ BPMï¼é©³åå¯éç½®åè稿ï¼éè¿ä¸æä¾åºåèªå¨çæè®¢å", "roles": ["éè´å", "审æ¹äºº"]}, |
| | | {"stage": "ä¸å", "action": "éè´è®¢å(CGDD)èªå¨æäº¤ BPM(15â20)", "roles": ["éè´å"]}, |
| | | {"stage": "ç³è¯·", "action": "éè´ç³è¯·(CGSQ)åå¼ æå¾éå¤å¼ æ¹éæäº¤ BPM(ä»
æªæäº¤/驳åå¯æäº¤ï¼éåç¬ç«äºå¡ï¼å¤±è´¥é¡¹åç¬æç¤º)ï¼é©³åå¯éç½®åè稿ï¼éè¿ä¸æä¾åºåæ¶ï¼å
æâä¾åºå+æç»(ç©æ/æ°é/åä»·/ç¨ç)âå¹é
å·²å®¡æ ¸è®¢åï¼å¹é
å°åå
³èç³è¯·ãä¸éå¤çæï¼æªå¹é
æèªå¨çæ", "roles": ["éè´å", "审æ¹äºº"]}, |
| | | {"stage": "ä¸å", "action": "éè´è®¢å(CGDD)å¯ Excel 导å
¥(å¾éå®¡æ ¸ç´æ¥ç½® 20)æèªå¨æäº¤ BPM(15â20)ï¼å¯¼å
¥æ¶æåå·æâä¾åºå+æç»âè¯å«éå¤ï¼éå¤é¡¹é»è®¤ä¸å¯¼å
¥å¹¶å¼¹æ¡æç¤ºï¼ç¨æ·ç¡®è®¤åæè¦çå·²æè®¢åï¼å·²å
¥åº/å·²éè´§/å·²å
³èå票æéè´ç³è¯·ç订åç¦æ¢è¦ç", "roles": ["éè´å"]}, |
| | | {"stage": "æ¶è´§", "action": "确认æ¶è´§çæå°è´§éç¥(0âå¾
è´¨æ£2/å¾
å
¥åº3â4)ï¼éæ£ç©æèªå¨å建 IQC", "roles": ["ä»ç®¡", "IQC è´¨æ£å"]}, |
| | | {"stage": "å
¥åº", "action": "IQC åæ ¼/ç¹éâéè´å
¥åº(å¾
䏿¶âå¾
æ§è¡â宿)åºå+ï¼åå订å inCount/inStatus ä¸ç³è¯·æç»", "roles": ["ä»ç®¡"]}, |
| | | {"stage": "éè´§", "action": "IQC ä¸åæ ¼éè´§âä¾åºåéè´§åºåº(éæéå®å°è®¢åè¡)ï¼æ¥åºèµ° NCR", "roles": ["ä»ç®¡"]}, |
| | |
| | | S1 --> S2[èµè´¨è¯ä¹¦ç®¡ç] |
| | | S1 --> S3[绩æè¯ä»·Â·è´¨é交ä»ä»·æ ¼æå¡åç»´] |
| | | S4[SRM ææ æ¯ä»·Â·åå¸âææ â弿 âè¯æ â宿 ] -->|宿 审æ¹éè¿çæè®¢å| C3 |
| | | C1[éè´ç³è¯· CGSQ·è稿0â审æ¹ä¸10] -->|æäº¤BPM| C2{审æ¹} |
| | | C1[éè´ç³è¯· CGSQ·è稿0â审æ¹ä¸10] -->|åå¼ æäº¤ææ¹éæäº¤BPM| C2{审æ¹} |
| | | C2 -->|驳å30âå¯éç½®åè稿| C1 |
| | | C2 -->|éè¿20䏿ä¾åºå·èªå¨çæ| C3[éè´è®¢å CGDD·审æ¹ä¸15âå·²å®¡æ ¸20] |
| | | C2 -->|éè¿20䏿ä¾åºå·å
æä¾åºå+æç»å¹é
å·²å®¡æ ¸è®¢å| C2a{æ¯å¦å·²æç¸å订å} |
| | | C2a -->|æÂ·å
³èç³è¯·ä¸éå¤çæ| C3[éè´è®¢å CGDD·审æ¹ä¸15âå·²å®¡æ ¸20] |
| | | C2a -->|æ ·èªå¨çæ| C3 |
| | | C0[éè´è®¢å导å
¥ Excel·å¾éå®¡æ ¸ç´æ¥20] -->|åå·æä¾åºå+æç»é夿¶å¼¹æ¡ç¡®è®¤è¦ç| C3 |
| | | C2 -->|éè¿20æ ä¾åºå| C4[æå¨æå®ä¾åºåçæè®¢å] |
| | | C4 --> C3 |
| | | C3 -->|确认æ¶è´§| C5[å°è´§éç¥å·è稿0âå¾
è´¨æ£2âå¾
å
¥åº3â宿4] |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # éè´è®¢å导å
¥é夿£æµä¸éè´ç³è¯·æ¹éæäº¤ - å端èè°æ¹æ¡ |
| | | |
| | | ## æ¶åé¡µé¢ |
| | | |
| | | - ERP â éè´ç®¡ç â éè´è®¢å â 导å
¥å¼¹çª |
| | | - ERP â éè´ç®¡ç â éè´ç³è¯· â å表工å
·æ ãæäº¤å®¡æ¹æµç¨å¼¹çª |
| | | |
| | | ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å
¥ |
| | | |
| | | 1. éè´ç³è¯·å表å¾éå¤å¼ åæ® â ç¹å»ãæ¹éæäº¤ãâ åç«¯è¿æ»¤åºâæªæäº¤(0)/审æ¹ä¸éè¿(30)âçç³è¯· â éæ©å®¡æ¹æµç¨ â ææ¯æ¹ 50 å¼ è°ç¨æ¹éæäº¤æ¥å£ã |
| | | 2. æ¹éæäº¤éå¼ å建 BPM æµç¨å®ä¾å¹¶ååç³è¯·ç¶æä¸ºâ审æ¹ä¸(10)âï¼åå¼ å¤±è´¥ä¸å½±åå
¶å®ç³è¯·ï¼å¤±è´¥åå æç³è¯·åå·è¿åã |
| | | 3. éè´ç³è¯·å®¡æ¹éè¿ä¸å·²å¡«ä¾åºåæ¶ï¼å端å
æâä¾åºå + æç»ï¼ç©æãæ°éãåä»·ãç¨çï¼âå¹é
ç³»ç»å
å·²å®¡æ ¸ä¸å°æªå
¥åº/éè´§çéè´è®¢åï¼ |
| | | - å¹é
å°ï¼æç³è¯·å
³èå°è¯¥è®¢åï¼å¹¶å填订åæç»çç³è¯·æç»ç¼å·ï¼ä¸åçææ°è®¢åï¼ |
| | | - å¹é
ä¸å°ï¼ä¿æåæé»è¾èªå¨çæä¸å¼ å·²å®¡æ ¸éè´è®¢åã |
| | | 4. éè´è®¢å导å
¥æ¶ï¼å端æä»¥ä¸é¡ºåºè¯å«éå¤ï¼ |
| | | - æä»¶ä¸çâéè´åç¼å·âå¨ç³»ç»å
å·²åå¨ï¼ |
| | | - æä»¶ä¸çâéè´åç¼å·âç空ï¼ç±ç³»ç»èªå¨çæç¼å·ï¼ä¸ä¾åºåãæç»ãæ°éãåä»·ãç¨çä¸ç³»ç»å
å·²æè®¢åå®å
¨ä¸è´ã |
| | | 5. åç°éå¤ä¸æªå¼å¯è¦çæ¶ï¼éå¤è®¢åä¸è½åºï¼è¿å `duplicateList`ï¼åç«¯å¼¹æ¡æç¤ºï¼ç¨æ·ç¡®è®¤è¦çå以 `overwrite=true` 鿰坼å
¥ã |
| | | 6. å·²å
¥åºãå·²éè´§ãå·²å
³èå票æå·²å
³èéè´ç³è¯·ãå·²æäº¤å®¡æ¹æµç¨ç订åç¦æ¢è¦çï¼è¦ç失败åå è¿å
¥ `failureList`ã |
| | | |
| | | ## API |
| | | |
| | | | æ¹æ³ | è·¯å¾ | 说æ | |
| | | |------|------|------| |
| | | | POST | /erp/purchase-order/import | 导å
¥éè´è®¢åï¼æ°å¢ `overwrite` åæ°ãååºæ°å¢ `duplicateList`ï¼ | |
| | | | PUT | /erp/purchase-request/submit-batch | æ¹éæäº¤éè´ç³è¯·å®¡æ¹ï¼æ°å¢ï¼ | |
| | | |
| | | ### 1. 导å
¥éè´è®¢å |
| | | |
| | | **请æ±åæ°ï¼multipart/form-dataï¼ï¼** |
| | | |
| | | | åæ° | ç±»å | å¿
å¡« | 说æ | |
| | | |------|------|------|------| |
| | | | file | File | æ¯ | xls/xlsx æä»¶ | |
| | | | directApprove | Boolean | å¦ | æ¯å¦ç´æ¥å°å¯¼å
¥è®¢å置为âå·²å®¡æ ¸(20)âï¼é»è®¤ false | |
| | | | overwrite | Boolean | å¦ | æ¯å¦è¦çéå¤è®¢åï¼é»è®¤ falseï¼false æ¶éå¤é¡¹åªè¿åä¸è½åº | |
| | | |
| | | **ååºæ°å¢å段ï¼** |
| | | |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | duplicateList | Array | æ£æµå°çéå¤è®¢åï¼æªå¼å¯è¦çæ¶è¿äºè®¢åæªå¯¼å
¥ | |
| | | | duplicateList[].key | String | 导å
¥æä»¶ä¸çéè´åç¼å·ï¼ç¼å·ä¸ºç©ºæ¶ä¸ºâ第 N è¡â | |
| | | | duplicateList[].existingNo | String | ç³»ç»å
å·²åå¨çéè´è®¢åç¼å· | |
| | | | duplicateList[].existingId | Number | ç³»ç»å
å·²åå¨çéè´è®¢åç¼å· | |
| | | | duplicateList[].reason | String | éå¤åå ï¼éè´åç¼å·å·²åå¨ / ä¾åºåãæç»ãæ°éãåä»·ãéé¢ä¸å·²æè®¢åä¸è´ | |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "code": 0, |
| | | "data": { |
| | | "createList": ["CGDD20261105000001"], |
| | | "updateList": [], |
| | | "failureList": {}, |
| | | "duplicateList": [ |
| | | { |
| | | "key": "第 2 è¡", |
| | | "existingNo": "CGDD20261014000006", |
| | | "existingId": 2106, |
| | | "reason": "ä¾åºåãæç»ãæ°éãåä»·ãéé¢ä¸å·²æè®¢åä¸è´" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ### 2. æ¹éæäº¤éè´ç³è¯· |
| | | |
| | | **请æ±åæ°ï¼queryï¼ï¼** |
| | | |
| | | | åæ° | ç±»å | å¿
å¡« | 说æ | |
| | | |------|------|------|------| |
| | | | ids | String | æ¯ | éè´ç³è¯·ç¼å·ï¼å¤ä¸ªç¨è±æéå·åé | |
| | | | processDefinitionKey | String | æ¯ | BPM å®¡æ¹æµç¨å®ä¹ Key | |
| | | |
| | | **ååºå段ï¼** |
| | | |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | successIds | Number[] | æäº¤æåçéè´ç³è¯·ç¼å· | |
| | | | failureMap | Object | æäº¤å¤±è´¥éåï¼key 为éè´ç³è¯·åå·ï¼åä¸å°åå·æ¶ä¸ºâç¼å· xxxâï¼ï¼value 为失败åå | |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "code": 0, |
| | | "data": { |
| | | "successIds": [2195, 2196], |
| | | "failureMap": { |
| | | "CGSQ20261105000003": "æäº¤å¤±è´¥ï¼éè´ç³è¯·å(CGSQ20261105000003)没ææç»ï¼è¯·å
æ·»å æç»ååæäº¤" |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ## åæ®µå±ç¤ºè§å |
| | | |
| | | | åæ®µ | å±ç¤ºä½ç½® | 说æ | |
| | | |------|----------|------| |
| | | | duplicateList | 导å
¥ç»æåºãéå¤ç¡®è®¤å¼¹æ¡ | å±ç¤ºå¯¼å
¥æä»¶æ è¯ â å·²åå¨è®¢åç¼å· + éå¤åå | |
| | | | updateList | 导å
¥ç»æåº | è¦ç模å¼ä¸å±ç¤ºè¢«æ´æ°è®¢åç¼å· | |
| | | | failureMap | æ¹éæäº¤ç»æå¼¹æ¡ | éæ¡å±ç¤ºâç³è¯·åå·ï¼å¤±è´¥åå â | |
| | | |
| | | ## ä¸å¡è§å说æ |
| | | |
| | | | åºæ¯ | è§å | |
| | | |------|------| |
| | | | æ¹éæäº¤èå´ | ä»
âæªæäº¤(0)âåâ审æ¹ä¸éè¿(30)âçç³è¯·å¯æäº¤ï¼å
¶å®ç¶æèªå¨è·³è¿å¹¶æç¤º | |
| | | | æ¹éæäº¤äºå¡ | æ¯å¼ ç³è¯·ç¬ç«äºå¡ï¼åå¼ å¤±è´¥ä¸åæ»å
¶å®ç³è¯· | |
| | | | 导å
¥éå¤-åå· | æä»¶å¡«åäºâéè´åç¼å·âä¸ç³»ç»å·²åå¨ï¼å¤å®éå¤ | |
| | | | 导å
¥éå¤-æ åå· | æä»¶æªå¡«âéè´åç¼å·âæ¶ï¼ææâä¾åºå + æç»âæçº¹å¤å®éå¤ï¼å¡«åäºæç¡®åå·çæ£å¸¸éå¤éè´ä¸ä¼è¢«æçº¹æ¦æª | |
| | | | è¦çéå¶ | å·²å
¥åºãå·²éè´§ãå·²å
³èå票ãå·²å
³èéè´ç³è¯·ãå·²æäº¤å®¡æ¹æµç¨ç订åä¸å¯è¦ç | |
| | | | 审æ¹èªå¨çæ | å¹é
å°ç¸åå·²å®¡æ ¸è®¢åæ¶åªå»ºç«å
³èï¼ä¸éå¤çæè®¢åï¼å¹é
ä¸å°ææ°å¢ | |
| | | |
| | | ## 注æäºé¡¹ |
| | | |
| | | - 导å
¥éå¤å¼¹æ¡æä¾âè¦çéå¤è®¢åâåâä¸è¦çâ两个é项ï¼éæ©ä¸è¦çæ¶ï¼éå¤é¡¹ä¿çå¨ç»æåºï¼å¯å次ç¹å»âè¦çéå¤è®¢åâã |
| | | - è¥ç¡®è®¤æ¯æ£å¸¸çæ°éè´èééå¤ä¸ä¼ ï¼é卿件ä¸å¡«åä¸ä¸ªç³»ç»ä¸ä¸åå¨çéè´åç¼å·å鿰坼å
¥ã |
| | | - æ¹éæäº¤å»ºè®®æ 50 å¼ ä¸æ¹è°ç¨ï¼é¿å
䏿¬¡æäº¤è¿å¤å¯¼è´è¯·æ±è¶
æ¶ã |
| | | - `overwrite=true` ä¼éæ°å¤çæ´ä¸ªæä»¶ï¼å
å«é¦æ¬¡å·²æåæ°å¢çééå¤é¡¹ï¼å æ¤ `updateList` å¯è½å
å«è¿äºè®¢åç¼å·ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- éè´ç³è¯·ãæ¹éæäº¤ãæé®æé |
| | | -- å½±å页é¢ï¼ERP -> éè´ç®¡ç -> éè´ç³è¯· |
| | | -- |
| | | -- èæ¯ï¼éè´ç³è¯·æäº¤æ¥å£ï¼PUT /erp/purchase-request/submitã/submit-batchï¼ä½¿ç¨ |
| | | -- æéç erp:purchase-request:submitï¼ä½ system_menu ä¸ä»æªç»´æ¤è¯¥æéæé®ï¼ |
| | | -- 导è´å端工å
·æ ãæ¹éæäº¤ãæé®è¢«æéè¿æ»¤éèï¼æ®éè§è²è°ç¨æ¥å£ä¼ 403ã |
| | | -- |
| | | -- æ¬èæ¬å¹çï¼å¯é夿§è¡ãæ§è¡å鿏
çæéç¼åï¼éå¯å端æå é¤ |
| | | -- redis ä¸ç permission_menu_ids:erp:purchase-request:submit ä¸ç¸å
³ role_menu_ids:*ï¼ï¼ |
| | | -- åç«¯éæ°ç»å½/å·æ°åçæã |
| | | |
| | | -- 1. è¡¥æé®èåï¼æå¨âéè´ç³è¯·âèåä¸ï¼ |
| | | INSERT INTO system_menu (name, permission, type, sort, parent_id, status, icon, creator, updater) |
| | | SELECT 'æ¹éæäº¤å®¡æ¹', 'erp:purchase-request:submit', 3, 9, p.id, 0, '#', '1', '1' |
| | | FROM system_menu p |
| | | WHERE p.component = 'erp/purchase/request/index' |
| | | AND p.type = 2 |
| | | AND p.deleted = b'0' |
| | | AND NOT EXISTS ( |
| | | SELECT 1 FROM system_menu m |
| | | WHERE m.permission = 'erp:purchase-request:submit' AND m.deleted = b'0' |
| | | ); |
| | | |
| | | -- 2. æäºå·²æ¥æâéè´ç³è¯·âèåçè§è² |
| | | INSERT INTO system_role_menu (role_id, menu_id, creator, updater) |
| | | SELECT DISTINCT rm.role_id, m.id, '1', '1' |
| | | FROM system_role_menu rm |
| | | JOIN system_menu p ON p.component = 'erp/purchase/request/index' AND p.type = 2 AND p.deleted = b'0' |
| | | JOIN system_menu m ON m.permission = 'erp:purchase-request:submit' AND m.deleted = b'0' |
| | | WHERE rm.menu_id = p.id |
| | | AND rm.deleted = b'0' |
| | | AND NOT EXISTS ( |
| | | SELECT 1 FROM system_role_menu x |
| | | WHERE x.role_id = rm.role_id AND x.menu_id = m.id AND x.deleted = b'0' |
| | | ); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- HRM 审æ¹ç¶æåå
¸ |
| | | -- å½±å页é¢ï¼äººåèµæºæ¨¡å䏿æä½¿ç¨ hrm_audit_status åå
¸çåæ®å表/详æ
ï¼èå¤å¼å¸¸ç³è¯·ã请åã离èãè°å²çï¼ |
| | | -- |
| | | -- èæ¯ï¼ä»£ç ä¸å¤§éä½¿ç¨ @DictFormat("hrm_audit_status") ä¸å端 DICT_TYPE.HRM_AUDIT_STATUSï¼ |
| | | -- ä½ system_dict_type / system_dict_data ä¸ç¼ºå°è¯¥åå
¸ï¼å¯¼è´ç¶æåæ¾ç¤ºä¸ºç©ºãå¯¼åºæ 䏿åã |
| | | -- |
| | | -- æ¬èæ¬å¹çï¼å¯é夿§è¡ãæ§è¡åæ¸
çåå
¸ç¼åï¼éå¯åç«¯ï¼æå é¤ Redis ä¸ç dict_data:* ç¸å
³é®ï¼ï¼ |
| | | -- åç«¯éæ°ç»å½/å·æ°åçæã |
| | | |
| | | -- 1. åå
¸ç±»å |
| | | INSERT INTO system_dict_type (name, type, status, remark, creator, updater) |
| | | SELECT 'HRM 审æ¹ç¶æ', 'hrm_audit_status', 0, 'äººåæ¨¡å忮审æ¹ç¶æ', '1', '1' |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 FROM system_dict_type WHERE type = 'hrm_audit_status' AND deleted = b'0' |
| | | ); |
| | | |
| | | -- 2. åå
¸æ°æ®ï¼ä¸ HrmAuditStatusEnum ä¿æä¸è´ï¼ |
| | | INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, updater) |
| | | SELECT s.sort, s.label, s.value, 'hrm_audit_status', 0, s.color_type, '', '', '1', '1' |
| | | FROM ( |
| | | SELECT 0 AS sort, 'è稿' AS label, '0' AS value, 'info' AS color_type |
| | | UNION ALL SELECT 10, '审æ¹ä¸', '10', 'default' |
| | | UNION ALL SELECT 20, '审æ¹éè¿', '20', 'success' |
| | | UNION ALL SELECT 30, '审æ¹é©³å', '30', 'danger' |
| | | UNION ALL SELECT 40, 'å·²æ¤é', '40', 'warning' |
| | | ) s |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 FROM system_dict_data d |
| | | WHERE d.dict_type = 'hrm_audit_status' AND d.value = s.value AND d.deleted = b'0' |
| | | ); |
| | |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-order:import')") |
| | | public CommonResult<ErpPurchaseOrderImportRespVO> importPurchaseOrderExcel( |
| | | @RequestParam("file") MultipartFile file, |
| | | @RequestParam(value = "directApprove", required = false, defaultValue = "false") Boolean directApprove) throws Exception { |
| | | @RequestParam(value = "directApprove", required = false, defaultValue = "false") Boolean directApprove, |
| | | @RequestParam(value = "overwrite", required = false, defaultValue = "false") Boolean overwrite) throws Exception { |
| | | List<ErpPurchaseOrderImportExcelVO> list = ExcelUtils.read(file, ErpPurchaseOrderImportExcelVO.class); |
| | | return success(purchaseOrderService.importPurchaseOrderList(list, directApprove)); |
| | | return success(purchaseOrderService.importPurchaseOrderList(list, directApprove, overwrite)); |
| | | } |
| | | |
| | | @PostMapping("/confirm-receipt") |
| | |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSubmitBatchRespVO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; |
| | |
| | | return success(true); |
| | | } |
| | | |
| | | @PutMapping("/submit-batch") |
| | | @Operation(summary = "æ¹éæäº¤éè´ç³è¯·å®¡æ¹") |
| | | @Parameter(name = "ids", description = "éè´ç³è¯·ç¼å·æ°ç»", required = true) |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-request:submit')") |
| | | public CommonResult<ErpPurchaseRequestSubmitBatchRespVO> submitPurchaseRequestBatch( |
| | | @RequestParam("ids") List<Long> ids, |
| | | @RequestParam("processDefinitionKey") String processDefinitionKey) { |
| | | Long userId = getLoginUserId(); |
| | | return success(purchaseRequestService.submitPurchaseRequestBatch(ids, processDefinitionKey, userId)); |
| | | } |
| | | |
| | | @PutMapping("/reset-status") |
| | | @Operation(summary = "éç½®éè´ç³è¯·ç¶æï¼é©³åâè稿ï¼") |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-request:update')") |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * 管çåå° - éè´è®¢å导å
¥éå¤é¡¹ VO |
| | | * |
| | | * 导å
¥æ¶å¦æåç°ç³»ç»å
å·²æç¸åéè´åç¼å·ï¼æä¾åºåãæç»ãéé¢å®å
¨ä¸è´ç订åï¼ |
| | | * ä¸ç´æ¥å建æ°è®¢åï¼èæ¯è¿åç»åç«¯å¼¹æ¡æç¤ºï¼ç±ç¨æ·å³å®æ¯å¦è¦çã |
| | | */ |
| | | @Schema(description = "管çåå° - éè´è®¢å导å
¥éå¤é¡¹ VO") |
| | | @Data |
| | | @Builder |
| | | public class ErpPurchaseOrderImportDuplicateVO { |
| | | |
| | | @Schema(description = "导å
¥æä»¶ä¸çéè´åç¼å·ï¼ç¼å·ä¸ºç©ºæ¶ä½¿ç¨è¡å·", example = "CGDD20260101000001") |
| | | private String key; |
| | | |
| | | @Schema(description = "ç³»ç»å
å·²åå¨çéè´è®¢åç¼å·", example = "CGDD20260101000001") |
| | | private String existingNo; |
| | | |
| | | @Schema(description = "ç³»ç»å
å·²åå¨çéè´è®¢åç¼å·") |
| | | private Long existingId; |
| | | |
| | | @Schema(description = "éå¤åå ", example = "éè´åç¼å·å·²åå¨") |
| | | private String reason; |
| | | |
| | | } |
| | |
| | | @Schema(description = "导å
¥å¤±è´¥éåï¼key 为éè´åç¼å·æè¡å·ï¼value 为失败åå ") |
| | | private Map<String, String> failureList; |
| | | |
| | | @Schema(description = "æ£æµå°çéå¤éè´è®¢åï¼æªå¼å¯è¦çæ¶è¿äºè®¢åä¸ä¼å¯¼å
¥ï¼ç±åç«¯å¼¹æ¡æç¤ºç¨æ·") |
| | | private List<ErpPurchaseOrderImportDuplicateVO> duplicateList; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 管çåå° - éè´ç³è¯·æ¹éæäº¤ Response VO |
| | | */ |
| | | @Schema(description = "管çåå° - éè´ç³è¯·æ¹éæäº¤ Response VO") |
| | | @Data |
| | | @Builder |
| | | public class ErpPurchaseRequestSubmitBatchRespVO { |
| | | |
| | | @Schema(description = "æäº¤æåçéè´ç³è¯·ç¼å·æ°ç»") |
| | | private List<Long> successIds; |
| | | |
| | | @Schema(description = "æäº¤å¤±è´¥çéè´ç³è¯·éåï¼key 为éè´ç³è¯·åå·ï¼åä¸å°åå·æ¶ä¸ºç¼å·ï¼ï¼value 为失败åå ") |
| | | private Map<String, String> failureMap; |
| | | |
| | | } |
| | |
| | | return selectOne(ErpPurchaseOrderDO::getNo, no); |
| | | } |
| | | |
| | | /** |
| | | * æç¼å·æ¥è¯¢å¹¶å è¡éï¼ç¨äºéè´ç³è¯·å®¡æ¹éè¿æ¶å¤ç¨å·²æè®¢åï¼é¿å
å¹¶åéå¤å ç¨ |
| | | */ |
| | | default ErpPurchaseOrderDO selectByIdForUpdate(Long id) { |
| | | return selectOne(new LambdaQueryWrapperX<ErpPurchaseOrderDO>() |
| | | .eq(ErpPurchaseOrderDO::getId, id) |
| | | .last("FOR UPDATE")); |
| | | } |
| | | |
| | | default ErpPurchaseOrderDO selectByProcessInstanceId(String processInstanceId) { |
| | | return selectOne(ErpPurchaseOrderDO::getProcessInstanceId, processInstanceId); |
| | | } |
| | |
| | | ErrorCode PURCHASE_REQUEST_NOT_EXISTS = new ErrorCode(1_030_104_000, "éè´ç³è¯·åä¸åå¨"); |
| | | ErrorCode PURCHASE_REQUEST_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_001, "éè´ç³è¯·å({})䏿¯èç¨¿ç¶æï¼æ æ³å é¤"); |
| | | ErrorCode PURCHASE_REQUEST_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_002, "éè´ç³è¯·å({})䏿¯èç¨¿ç¶æï¼æ æ³ä¿®æ¹"); |
| | | ErrorCode PURCHASE_REQUEST_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_003, "æäº¤å¤±è´¥ï¼åªæèç¨¿ç¶æçç³è¯·åæè½æäº¤"); |
| | | ErrorCode PURCHASE_REQUEST_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_003, "æäº¤å¤±è´¥ï¼éè´ç³è¯·å({})䏿¯è稿æé©³åç¶æï¼æ æ³æäº¤"); |
| | | ErrorCode PURCHASE_REQUEST_NO_EXISTS = new ErrorCode(1_030_104_004, "çæç³è¯·åå·å¤±è´¥ï¼è¯·éæ°æäº¤"); |
| | | ErrorCode PURCHASE_REQUEST_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_104_005, "æ´æ°å®¡æ¹ç¶æå¤±è´¥ï¼åªæå®¡æ¹ä¸çç³è¯·åæè½æ´æ°"); |
| | | ErrorCode PURCHASE_REQUEST_NOT_APPROVE = new ErrorCode(1_030_104_006, "éè´ç³è¯·åæªå®¡æ ¸éè¿ï¼æ æ³æä½"); |
| | |
| | | ErrorCode PURCHASE_REQUEST_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_104_008, "éè´ç³è¯·å®¡æ¹åç±»ä¸åå¨ï¼è¯·å
å¨BPM模åå建éè´ç³è¯·å®¡æ¹æµç¨åç±»"); |
| | | ErrorCode PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_104_009, "éè´ç³è¯·å®¡æ¹åç±»ä¸æ²¡æå¯ç¨çæµç¨æ¨¡åï¼è¯·å
å建并å叿µç¨"); |
| | | ErrorCode PURCHASE_REQUEST_RESET_FAIL_NOT_REJECT = new ErrorCode(1_030_104_010, "é置失败ï¼åªæé©³åç¶æçéè´ç³è¯·åæè½éç½®"); |
| | | ErrorCode PURCHASE_REQUEST_ITEM_EMPTY = new ErrorCode(1_030_104_011, "æäº¤å¤±è´¥ï¼éè´ç³è¯·å({})没ææç»ï¼è¯·å
æ·»å æç»ååæäº¤"); |
| | | |
| | | // ========== ERP éè´æ¥ç¥¨ï¼1-030-105-000ï¼ ========== |
| | | ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "æ¥ç¥¨ä¸åå¨"); |
| | |
| | | * |
| | | * @param importList 导å
¥æ°æ®ï¼æ¯ä¸è¡ = 䏿¡æç»ï¼ç¸åéè´åç¼å·å并为ä¸å¼ 订åï¼ |
| | | * @param directApprove æ¯å¦å°å¯¼å
¥ç订åç´æ¥ç½®ä¸ºâå·²å®¡æ ¸âï¼true=å·²å®¡æ ¸(20)ï¼false=æªå®¡æ ¸(10) |
| | | * @param overwrite æ¯å¦è¦çç³»ç»å
å·²æçéå¤è®¢åï¼false æ¶åªè¿åéå¤é¡¹ä¾å端æç¤ºï¼true æ¶æ´æ°å·²æè®¢å |
| | | * @return 导å
¥ç»æ |
| | | */ |
| | | ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList, Boolean directApprove); |
| | | ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList, |
| | | Boolean directApprove, Boolean overwrite); |
| | | |
| | | /** |
| | | * æ´æ°éè´è®¢å |
| | |
| | | import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportDuplicateVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportExcelVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportRespVO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceOrderMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderItemMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper; |
| | |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| | |
| | | private ErpPurchaseOrderMapper purchaseOrderMapper; |
| | | @Resource |
| | | private ErpPurchaseOrderItemMapper purchaseOrderItemMapper; |
| | | @Resource |
| | | private ErpPurchaseInvoiceOrderMapper invoiceOrderMapper; |
| | | |
| | | @Resource |
| | | private ErpNoRedisDAO noRedisDAO; |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList, |
| | | Boolean directApprove) { |
| | | Boolean directApprove, Boolean overwrite) { |
| | | ErpPurchaseOrderImportRespVO respVO = ErpPurchaseOrderImportRespVO.builder() |
| | | .createList(new ArrayList<>()) |
| | | .updateList(new ArrayList<>()) |
| | | .failureList(new LinkedHashMap<>()) |
| | | .duplicateList(new ArrayList<>()) |
| | | .build(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | return respVO; |
| | |
| | | }); |
| | | } |
| | | |
| | | // 2. æâéè´åç¼å·âåç»ï¼åç¼å·å¤è¡ = ä¸å¼ 订åç夿¡æç»ï¼ç¼å·ä¸ºç©ºåæ¯è¡ç¬ç«æå |
| | | // 2. é¢çç³»ç»å
ææéè´è®¢åï¼æåå·ãæâä¾åºå + æç»âæçº¹å»ºç´¢å¼ï¼ç¨äºè¯å«éå¤å¯¼å
¥ |
| | | List<ErpPurchaseOrderDO> existingOrders = purchaseOrderMapper.selectList(); |
| | | Map<Long, List<ErpPurchaseOrderItemDO>> existingItemsMap = new HashMap<>(); |
| | | if (CollUtil.isNotEmpty(existingOrders)) { |
| | | purchaseOrderItemMapper.selectListByOrderIds( |
| | | existingOrders.stream().map(ErpPurchaseOrderDO::getId).toList()) |
| | | .forEach(item -> existingItemsMap.computeIfAbsent(item.getOrderId(), k -> new ArrayList<>()).add(item)); |
| | | } |
| | | Map<String, ErpPurchaseOrderDO> existingNoMap = new HashMap<>(); |
| | | Map<String, List<ErpPurchaseOrderDO>> existingFingerprintMap = new HashMap<>(); |
| | | for (ErpPurchaseOrderDO order : existingOrders) { |
| | | if (StrUtil.isNotBlank(order.getNo())) { |
| | | existingNoMap.put(order.getNo(), order); |
| | | } |
| | | String fingerprint = buildOrderFingerprint(order.getSupplierId(), existingItemsMap.get(order.getId())); |
| | | if (fingerprint != null) { |
| | | existingFingerprintMap.computeIfAbsent(fingerprint, k -> new ArrayList<>()).add(order); |
| | | } |
| | | } |
| | | |
| | | // 3. æâéè´åç¼å·âåç»ï¼åç¼å·å¤è¡ = ä¸å¼ 订åç夿¡æç»ï¼ç¼å·ä¸ºç©ºåæ¯è¡ç¬ç«æå |
| | | Map<String, List<ErpPurchaseOrderImportExcelVO>> orderGroups = new LinkedHashMap<>(); |
| | | for (int i = 0; i < importList.size(); i++) { |
| | | ErpPurchaseOrderImportExcelVO row = importList.get(i); |
| | |
| | | orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row); |
| | | } |
| | | |
| | | // 3. é个订å导å
¥ï¼åå¼ è®¢å失败ä¸å½±åå
¶å®ï¼ |
| | | // 4. é个订å导å
¥ï¼åå¼ è®¢å失败ä¸å½±åå
¶å®ï¼ |
| | | for (Map.Entry<String, List<ErpPurchaseOrderImportExcelVO>> entry : orderGroups.entrySet()) { |
| | | List<ErpPurchaseOrderImportExcelVO> rows = entry.getValue(); |
| | | String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim() |
| | | : "第 " + entry.getKey().substring("__row_".length()) + " è¡"; |
| | | try { |
| | | ErpPurchaseOrderDO purchaseOrder = createPurchaseOrderByImport(rows, supplierIdMap, directApprove); |
| | | respVO.getCreateList().add(purchaseOrder.getNo()); |
| | | ImportOrderData importData = prepareImportOrder(rows, supplierIdMap); |
| | | |
| | | // 4.1 å
æåå·æ¾ï¼åªæâåå·ä¸ºç©ºãç±ç³»ç»çæç¼å·âçè¡ææâä¾åºå + 宿´æç»âæçº¹æ¾ã |
| | | // æä»¶éåäºæç¡®åå·çï¼è§ä¸ºç¨æ·æç¡®è¦æ°å¢è¯¥åå·ï¼æçº¹ç¸åä¹ä¸æ¦æªï¼é¿å
æ£å¸¸çéå¤éè´è¢«è¯¯å¤ã |
| | | ErpPurchaseOrderDO existing = importData.order.getNo() == null ? null |
| | | : existingNoMap.get(importData.order.getNo()); |
| | | String duplicateReason = existing != null ? "éè´åç¼å·å·²åå¨" : null; |
| | | boolean matchedByFingerprint = false; |
| | | List<ErpPurchaseOrderDO> fingerprintCandidates = null; |
| | | if (existing == null && importData.generatedNo && importData.fingerprint != null) { |
| | | fingerprintCandidates = existingFingerprintMap.get(importData.fingerprint); |
| | | if (CollUtil.isNotEmpty(fingerprintCandidates)) { |
| | | existing = fingerprintCandidates.get(0); |
| | | matchedByFingerprint = true; |
| | | duplicateReason = "ä¾åºåãæç»ãæ°éãåä»·ãéé¢ä¸å·²æè®¢åä¸è´"; |
| | | } |
| | | } |
| | | |
| | | // 4.2 åç°éå¤ï¼é»è®¤ä¸å¯¼å
¥ï¼è¿åç»åç«¯å¼¹æ¡æç¤ºï¼å¾éè¦çåææ´æ°å·²æè®¢å |
| | | if (existing != null) { |
| | | if (!Boolean.TRUE.equals(overwrite)) { |
| | | respVO.getDuplicateList().add(buildImportDuplicate(label, existing, duplicateReason)); |
| | | continue; |
| | | } |
| | | if (matchedByFingerprint && fingerprintCandidates.size() > 1) { |
| | | String nos = fingerprintCandidates.stream().map(ErpPurchaseOrderDO::getNo) |
| | | .collect(Collectors.joining("ã")); |
| | | throw new IllegalArgumentException("æ¾å°å¤å¼ ç¸å订åï¼" + nos |
| | | + "ï¼ï¼æ æ³èªå¨è¦çï¼è¯·å
ä¿çä¸å¼ åå导å
¥"); |
| | | } |
| | | updatePurchaseOrderByImport(existing, importData, directApprove); |
| | | respVO.getUpdateList().add(existing.getNo()); |
| | | continue; |
| | | } |
| | | |
| | | // 4.3 没æéå¤ï¼æ£å¸¸æ°å¢ |
| | | createPurchaseOrderByImport(importData, directApprove); |
| | | respVO.getCreateList().add(importData.order.getNo()); |
| | | } catch (Exception ex) { |
| | | respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 便®å¯¼å
¥æ°æ®å建ä¸å¼ éè´è®¢åï¼å«æç»ï¼ã |
| | | * |
| | | * 注æï¼æ¤å¤ä¸è°ç¨ submitPurchaseOrderï¼å æ¤ä¸ä¼å建 BPM 工使µï¼è®¢åç¶æç± directApprove ç´æ¥å³å®ï¼ |
| | | * å¾éâæ¯å¦å®¡æ ¸â => å·²å®¡æ ¸(20)ï¼æªå¾é => æªå®¡æ ¸(10)ã |
| | | * æ ¡éªå¹¶ç»è£
ä¸å¼ 导å
¥è®¢åï¼ä¸è½åºï¼ã |
| | | */ |
| | | private ErpPurchaseOrderDO createPurchaseOrderByImport(List<ErpPurchaseOrderImportExcelVO> rows, |
| | | Map<String, Long> supplierIdMap, Boolean directApprove) { |
| | | private ImportOrderData prepareImportOrder(List<ErpPurchaseOrderImportExcelVO> rows, |
| | | Map<String, Long> supplierIdMap) { |
| | | // 1. ä¾åºåï¼å该ç»é¦è¡ï¼ |
| | | ErpPurchaseOrderImportExcelVO head = rows.get(0); |
| | | if (StrUtil.isBlank(head.getSupplierName())) { |
| | |
| | | items.add(itemVO); |
| | | } |
| | | |
| | | // 3. åå·ï¼å¡«äºå°±ç¨ï¼æ ¡éªå¯ä¸ï¼ï¼æ²¡å¡«èªå¨çæ |
| | | String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim() |
| | | : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX); |
| | | if (purchaseOrderMapper.selectByNo(no) != null) { |
| | | throw new IllegalArgumentException("éè´åç¼å·å·²åå¨ï¼" + no); |
| | | } |
| | | // 3. åå·ï¼å¡«äºå°±ç¨ï¼ç空æ¶å
ä¸çæï¼çç¡®è®¤è¦æ°å¢æ¶åçæï¼é¿å
éå¤è¡è¢«è·³è¿ä¹æ¶èæµæ°´å· |
| | | boolean generatedNo = StrUtil.isBlank(head.getNo()); |
| | | |
| | | // 4. ç»è£
主表ï¼å¾éâæ¯å¦å®¡æ ¸â => å·²å®¡æ ¸(20)ï¼å¦åæªå®¡æ ¸(10) |
| | | // 4. ç»è£
主表ï¼ç¶æç±è°ç¨æ¹å³å®ï¼ |
| | | ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO(); |
| | | purchaseOrder.setNo(no); |
| | | purchaseOrder.setNo(generatedNo ? null : head.getNo().trim()); |
| | | purchaseOrder.setSupplierId(supplierId); |
| | | purchaseOrder.setOrderTime(parseImportOrderTime(head.getOrderTime())); |
| | | purchaseOrder.setRemark(head.getRemark()); |
| | | purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove) |
| | | ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus()); |
| | | purchaseOrder.setDepositPrice(BigDecimal.ZERO); // deposit_price 为 NOT NULL 䏿 é»è®¤å¼ï¼å¿
é¡»æ¾å¼èµå¼ |
| | | purchaseOrder.setInCount(BigDecimal.ZERO); |
| | | purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()); |
| | | |
| | | List<ErpPurchaseOrderItemDO> itemDOs = validatePurchaseOrderItems(items); |
| | | calculateTotalPrice(purchaseOrder, itemDOs); |
| | | return new ImportOrderData(purchaseOrder, itemDOs, |
| | | buildOrderFingerprint(supplierId, itemDOs), generatedNo, |
| | | StrUtil.isNotBlank(head.getOrderTime())); |
| | | } |
| | | |
| | | /** |
| | | * æ°å¢å¯¼å
¥è®¢åï¼å«æç»ï¼ã |
| | | */ |
| | | private void createPurchaseOrderByImport(ImportOrderData importData, Boolean directApprove) { |
| | | ErpPurchaseOrderDO purchaseOrder = importData.order; |
| | | if (StrUtil.isBlank(purchaseOrder.getNo())) { |
| | | purchaseOrder.setNo(noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX)); |
| | | } |
| | | purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove) |
| | | ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus()); |
| | | purchaseOrder.setInCount(BigDecimal.ZERO); |
| | | purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()); |
| | | purchaseOrderMapper.insert(purchaseOrder); |
| | | itemDOs.forEach(item -> item.setOrderId(purchaseOrder.getId())); |
| | | purchaseOrderItemMapper.insertBatch(itemDOs); |
| | | return purchaseOrder; |
| | | importData.items.forEach(item -> item.setOrderId(purchaseOrder.getId())); |
| | | purchaseOrderItemMapper.insertBatch(importData.items); |
| | | } |
| | | |
| | | /** |
| | | * è¦çå·²æå¯¼å
¥è®¢åï¼å«æç»ï¼ã |
| | | */ |
| | | private void updatePurchaseOrderByImport(ErpPurchaseOrderDO existing, ImportOrderData importData, |
| | | Boolean directApprove) { |
| | | // 1. æä¸æ¸¸åæ®ç订åä¸å
许è¦çï¼é¿å
æå·²å
¥åº/å·²å¼ç¥¨çæ°æ®æ¹æ |
| | | if (existing.getInCount() != null && existing.getInCount().compareTo(BigDecimal.ZERO) > 0) { |
| | | throw new IllegalArgumentException("éè´è®¢å[" + existing.getNo() + "]å·²å
¥åºï¼å·²å
¥åºæ°é " |
| | | + normalizeNumber(existing.getInCount()) + "ï¼ï¼ä¸è½è¦çï¼è¯·å
å é¤å¯¹åºå
¥åºè®°å½åå导å
¥"); |
| | | } |
| | | if (existing.getReturnCount() != null && existing.getReturnCount().compareTo(BigDecimal.ZERO) > 0) { |
| | | throw new IllegalArgumentException("éè´è®¢å[" + existing.getNo() + "]å·²éè´§ï¼å·²éè´§æ°é " |
| | | + normalizeNumber(existing.getReturnCount()) + "ï¼ï¼ä¸è½è¦çï¼è¯·å
å é¤å¯¹åºéè´§è®°å½åå导å
¥"); |
| | | } |
| | | if (CollUtil.isNotEmpty(invoiceOrderMapper.selectListByPurchaseOrderId(existing.getId()))) { |
| | | throw new IllegalArgumentException("éè´è®¢å[" + existing.getNo() |
| | | + "]å·²å
³èå票ï¼ä¸è½è¦çï¼è¯·å
è§£é¤å票å
³èåå导å
¥"); |
| | | } |
| | | if (StrUtil.isNotBlank(existing.getProcessInstanceId())) { |
| | | throw new IllegalArgumentException("éè´è®¢å[" + existing.getNo() |
| | | + "]å·²æäº¤å®¡æ¹æµç¨ï¼ä¸è½è¦çï¼è¯·å
å¨åååå
¬æ¤åæä½åºè¯¥å®¡æ¹åå导å
¥"); |
| | | } |
| | | List<ErpPurchaseOrderItemDO> oldItems = purchaseOrderItemMapper.selectListByOrderId(existing.getId()); |
| | | boolean linkedRequest = oldItems.stream().anyMatch(item -> item.getRequestItemId() != null); |
| | | if (linkedRequest) { |
| | | throw new IllegalArgumentException("éè´è®¢å[" + existing.getNo() |
| | | + "]å·²å
³èéè´ç³è¯·ï¼ä¸è½è¦çï¼è¯·å
å¨éè´ç³è¯·ä¸æ¤éæå é¤å¯¹åºç³è¯·åå导å
¥"); |
| | | } |
| | | |
| | | // 2. æ´æ°ä¸»è¡¨ï¼ä¿çååå·ãåå
¥åºç¶æï¼ |
| | | ErpPurchaseOrderDO updateObj = importData.order; |
| | | updateObj.setId(existing.getId()); |
| | | updateObj.setNo(existing.getNo()); |
| | | // æä»¶æ²¡å¡«éè´æ¶é´æ¶ä¿çåè®¢åæ¶é´ï¼é¿å
è¦çæ¶ææ¥ææ¹æå½åæ¶é´ |
| | | if (!importData.orderTimeProvided) { |
| | | updateObj.setOrderTime(existing.getOrderTime()); |
| | | } |
| | | updateObj.setStatus(Boolean.TRUE.equals(directApprove) |
| | | ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus()); |
| | | updateObj.setInCount(existing.getInCount() == null ? BigDecimal.ZERO : existing.getInCount()); |
| | | updateObj.setInStatus(existing.getInStatus()); |
| | | purchaseOrderMapper.updateById(updateObj); |
| | | |
| | | // 3. æç»æ´ç»æ¿æ¢ |
| | | purchaseOrderItemMapper.deleteByOrderId(existing.getId()); |
| | | importData.items.forEach(item -> item.setOrderId(existing.getId())); |
| | | purchaseOrderItemMapper.insertBatch(importData.items); |
| | | } |
| | | |
| | | private ErpPurchaseOrderImportDuplicateVO buildImportDuplicate(String label, ErpPurchaseOrderDO existing, |
| | | String reason) { |
| | | return ErpPurchaseOrderImportDuplicateVO.builder() |
| | | .key(label) |
| | | .existingNo(existing.getNo()) |
| | | .existingId(existing.getId()) |
| | | .reason(reason) |
| | | .build(); |
| | | } |
| | | |
| | | /** |
| | | * çæâä¾åºå + 宿´æç»âç订åæçº¹ï¼ç¨äºè¯å«éå¤å¯¼å
¥ã |
| | | * æç»æç©æãæ°éãåä»·ãç¨çæåºåæ¯è¾ï¼ä¸åè¡é¡ºåºå½±åã |
| | | */ |
| | | private String buildOrderFingerprint(Long supplierId, List<ErpPurchaseOrderItemDO> items) { |
| | | if (supplierId == null || CollUtil.isEmpty(items)) { |
| | | return null; |
| | | } |
| | | List<String> parts = items.stream() |
| | | .map(item -> String.join(":", |
| | | String.valueOf(item.getProductId()), |
| | | normalizeNumber(item.getCount()), |
| | | normalizeNumber(item.getProductPrice()), |
| | | normalizeNumber(item.getTaxPercent()))) |
| | | .sorted() |
| | | .toList(); |
| | | return supplierId + "|" + String.join(",", parts); |
| | | } |
| | | |
| | | private String normalizeNumber(BigDecimal value) { |
| | | return value == null ? "" : value.stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | private static class ImportOrderData { |
| | | |
| | | private final ErpPurchaseOrderDO order; |
| | | private final List<ErpPurchaseOrderItemDO> items; |
| | | private final String fingerprint; |
| | | /** |
| | | * éè´åç¼å·æ¯å¦ç±ç³»ç»èªå¨çæï¼æä»¶éçç©ºï¼ |
| | | */ |
| | | private final boolean generatedNo; |
| | | /** |
| | | * æä»¶éæ¯å¦å¡«åäºéè´æ¶é´ |
| | | */ |
| | | private final boolean orderTimeProvided; |
| | | |
| | | private ImportOrderData(ErpPurchaseOrderDO order, List<ErpPurchaseOrderItemDO> items, String fingerprint, |
| | | boolean generatedNo, boolean orderTimeProvided) { |
| | | this.order = order; |
| | | this.items = items; |
| | | this.fingerprint = fingerprint; |
| | | this.generatedNo = generatedNo; |
| | | this.orderTimeProvided = orderTimeProvided; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSubmitBatchRespVO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO; |
| | | import jakarta.validation.Valid; |
| | |
| | | void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId); |
| | | |
| | | /** |
| | | * æ¹éæäº¤éè´ç³è¯·å®¡æ¹ |
| | | * |
| | | * éåæäº¤ï¼åå¼ å¤±è´¥ä¸å½±åå
¶å®ç³è¯·ï¼æ¯å¼ ç³è¯·ç¬ç«äºå¡ã |
| | | * |
| | | * @param ids éè´ç³è¯·ç¼å·æ°ç» |
| | | * @param processDefinitionKey æµç¨å®ä¹ Key |
| | | * @param userId ç¨æ·ç¼å· |
| | | * @return æ¹éæäº¤ç»æï¼æåç¼å· + 失败åå ï¼ |
| | | */ |
| | | ErpPurchaseRequestSubmitBatchRespVO submitPurchaseRequestBatch(List<Long> ids, String processDefinitionKey, |
| | | Long userId); |
| | | |
| | | /** |
| | | * è·åéè´ç³è¯·å®¡æ¹æµç¨å表 |
| | | * |
| | | * @return æµç¨å®ä¹å表 |
| | |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSubmitBatchRespVO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO; |
| | |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.flowable.engine.repository.ProcessDefinition; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.context.annotation.Lazy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.validation.annotation.Validated; |
| | |
| | | private DeptApi deptApi; |
| | | @Resource |
| | | private StorageAttachmentApi storageAttachmentApi; |
| | | |
| | | /** |
| | | * èªèº«ä»£çï¼æ¹éæäº¤æ¶éåè°ç¨ {@link #submitPurchaseRequest}ï¼ä¿è¯æ¯å¼ ç³è¯·ç¬ç«äºå¡ |
| | | */ |
| | | @Autowired |
| | | @Lazy |
| | | private ErpPurchaseRequestService self; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId) { |
| | | // 1. æ ¡éªåå¨ + èç¨¿ç¶æ |
| | | ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestCanModify(id); |
| | | // 1. æ ¡éªåå¨ + è稿/驳åç¶æ |
| | | ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExists(id); |
| | | if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(purchaseRequest.getStatus()) |
| | | && !ErpPurchaseRequestStatusEnum.REJECT.getStatus().equals(purchaseRequest.getStatus())) { |
| | | throw exception(PURCHASE_REQUEST_SUBMIT_FAIL_NOT_DRAFT, purchaseRequest.getNo()); |
| | | } |
| | | |
| | | // 2. æ ¡éªæ¯å¦ææç» |
| | | List<ErpPurchaseRequestItemDO> items = purchaseRequestItemMapper.selectListByRequestId(id); |
| | | if (CollUtil.isEmpty(items)) { |
| | | throw exception(PURCHASE_REQUEST_NOT_EXISTS); // TODO: æ·»å æç»ä¸ºç©ºçé误ç |
| | | throw exception(PURCHASE_REQUEST_ITEM_EMPTY, purchaseRequest.getNo()); |
| | | } |
| | | |
| | | // 3. å建 BPM æµç¨å®ä¾ |
| | |
| | | .setId(id) |
| | | .setStatus(ErpPurchaseRequestStatusEnum.PROCESS.getStatus()) |
| | | .setProcessInstanceId(processInstanceId)); |
| | | } |
| | | |
| | | @Override |
| | | public ErpPurchaseRequestSubmitBatchRespVO submitPurchaseRequestBatch(List<Long> ids, |
| | | String processDefinitionKey, |
| | | Long userId) { |
| | | ErpPurchaseRequestSubmitBatchRespVO respVO = ErpPurchaseRequestSubmitBatchRespVO.builder() |
| | | .successIds(new ArrayList<>()) |
| | | .failureMap(new LinkedHashMap<>()) |
| | | .build(); |
| | | if (CollUtil.isEmpty(ids)) { |
| | | return respVO; |
| | | } |
| | | List<Long> distinctIds = ids.stream().filter(Objects::nonNull).distinct().toList(); |
| | | if (CollUtil.isEmpty(distinctIds)) { |
| | | return respVO; |
| | | } |
| | | |
| | | // 1. é¢çç³è¯·åå·ï¼å¤±è´¥æ¶å端å¯ç´æ¥çå°æ¯åªå¼ ç³è¯·å |
| | | Map<Long, String> noMap = purchaseRequestMapper.selectRequestListByIds(distinctIds).stream() |
| | | .filter(request -> request.getId() != null && StrUtil.isNotBlank(request.getNo())) |
| | | .collect(java.util.stream.Collectors.toMap(ErpPurchaseRequestDO::getId, ErpPurchaseRequestDO::getNo, |
| | | (a, b) -> a)); |
| | | |
| | | // 2. éåæäº¤ï¼åå¼ å¤±è´¥ä¸å½±åå
¶å®ç³è¯·ï¼éè¿ self 代çä¿è¯æ¯å¼ ç¬ç«äºå¡ |
| | | for (Long id : distinctIds) { |
| | | String label = noMap.getOrDefault(id, "ç¼å· " + id); |
| | | try { |
| | | self.submitPurchaseRequest(id, processDefinitionKey, userId); |
| | | respVO.getSuccessIds().add(id); |
| | | } catch (Exception ex) { |
| | | String reason = resolveSubmitErrorMessage(ex); |
| | | if (respVO.getFailureMap().containsKey(label)) { |
| | | label = label + "ï¼ç¼å· " + id + "ï¼"; |
| | | } |
| | | respVO.getFailureMap().put(label, reason); |
| | | } |
| | | } |
| | | return respVO; |
| | | } |
| | | |
| | | private String resolveSubmitErrorMessage(Exception ex) { |
| | | if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) { |
| | | return serviceException.getMessage(); |
| | | } |
| | | return StrUtil.blankToDefault(ex.getMessage(), "æäº¤å¤±è´¥"); |
| | | } |
| | | |
| | | @Override |
| | |
| | | // 1. è·åç³è¯·æç» |
| | | List<ErpPurchaseRequestItemDO> requestItems = purchaseRequestItemMapper.selectListByRequestId(id); |
| | | if (CollUtil.isEmpty(requestItems)) { |
| | | throw exception(PURCHASE_REQUEST_NOT_EXISTS); |
| | | throw exception(PURCHASE_REQUEST_ITEM_EMPTY, purchaseRequest.getNo()); |
| | | } |
| | | |
| | | // 2. çæéè´è®¢åå· |
| | | // 2. å
å°è¯å¤ç¨ç³»ç»å
å·²åå¨ä¸ä¾åºåãæç»å®å
¨ä¸è´çå·²å®¡æ ¸éè´è®¢åï¼é常æ¥èªéè´è®¢å导å
¥ï¼ï¼ |
| | | // é¿å
审æ¹éè¿ååçæä¸å¼ éå¤è®¢å |
| | | ErpPurchaseOrderDO reusableOrder = findReusablePurchaseOrder(purchaseRequest, requestItems); |
| | | if (reusableOrder != null) { |
| | | linkPurchaseRequestToOrder(purchaseRequest, requestItems, reusableOrder); |
| | | log.info("[doGeneratePurchaseOrder] éè´ç³è¯·({}) å¤ç¨å·²æéè´è®¢å({})ï¼æªéå¤çæ", |
| | | id, reusableOrder.getNo()); |
| | | return reusableOrder.getId(); |
| | | } |
| | | |
| | | // 3. çæéè´è®¢åå· |
| | | String orderNo = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX); |
| | | if (purchaseOrderMapper.selectByNo(orderNo) != null) { |
| | | throw exception(PURCHASE_ORDER_NO_EXISTS); |
| | |
| | | } |
| | | |
| | | /** |
| | | * æ¥æ¾å¯å¤ç¨çéè´è®¢åï¼ä¾åºåç¸åï¼ä¸æç»ï¼ç©æãæ°éãåä»·ãç¨çï¼ä¸ç³è¯·å®å
¨ä¸è´ã |
| | | * |
| | | * åªå¹é
å·²å®¡æ ¸è®¢åï¼é¿å
æç³è¯·æå°å°æªå®¡æ ¸ç订åä¸ï¼ä¼å
鿩尿ªå
³èéè´ç³è¯·ç导å
¥è®¢åï¼ |
| | | * å
¶æ¬¡éæ©ç¼å·æå°ç订åãå¹é
å°ç订åä¼å è¡éï¼é²æ¢å¹¶åå®¡æ¹æ¶ä¸¤å¼ ç³è¯·åæ¶å ç¨åä¸å¼ 订åã |
| | | */ |
| | | private ErpPurchaseOrderDO findReusablePurchaseOrder(ErpPurchaseRequestDO purchaseRequest, |
| | | List<ErpPurchaseRequestItemDO> requestItems) { |
| | | if (purchaseRequest.getSupplierId() == null || CollUtil.isEmpty(requestItems)) { |
| | | return null; |
| | | } |
| | | String targetFingerprint = buildRequestFingerprint(purchaseRequest.getSupplierId(), requestItems); |
| | | if (targetFingerprint == null) { |
| | | return null; |
| | | } |
| | | |
| | | // 1. æ¥è¯¢åä¾åºåçå·²å®¡æ ¸è®¢åï¼ææçº¹çéå®å
¨ä¸è´ç |
| | | List<ErpPurchaseOrderDO> orders = purchaseOrderMapper.selectList( |
| | | purchaseRequest.getSupplierId(), ErpAuditStatus.APPROVE.getStatus()); |
| | | if (CollUtil.isEmpty(orders)) { |
| | | return null; |
| | | } |
| | | Map<Long, List<ErpPurchaseOrderItemDO>> orderItemMap = purchaseOrderItemMapper.selectListByOrderIds( |
| | | orders.stream().map(ErpPurchaseOrderDO::getId).toList()).stream() |
| | | .collect(java.util.stream.Collectors.groupingBy(ErpPurchaseOrderItemDO::getOrderId)); |
| | | List<ErpPurchaseOrderDO> candidates = orders.stream() |
| | | .filter(order -> targetFingerprint.equals( |
| | | buildOrderFingerprint(order.getSupplierId(), orderItemMap.get(order.getId())))) |
| | | // å·²å
¥åº/å·²éè´§ç订å说æè¿ç¬éè´å·²ç»æ§è¡è¿ï¼ä¸è½æ¿æ¥é¡¶æ°ç³è¯·ï¼å¦åæ°éæ±ä¼è¢«âåæâ |
| | | .filter(order -> order.getInCount() == null |
| | | || order.getInCount().compareTo(BigDecimal.ZERO) == 0) |
| | | .filter(order -> order.getReturnCount() == null |
| | | || order.getReturnCount().compareTo(BigDecimal.ZERO) == 0) |
| | | .sorted(Comparator |
| | | .comparing((ErpPurchaseOrderDO order) -> |
| | | hasLinkedRequestItem(orderItemMap.get(order.getId())) ? 1 : 0) |
| | | .thenComparing(ErpPurchaseOrderDO::getId)) |
| | | .toList(); |
| | | if (CollUtil.isEmpty(candidates)) { |
| | | return null; |
| | | } |
| | | if (candidates.size() > 1) { |
| | | log.warn("[findReusablePurchaseOrder] éè´ç³è¯·({}) å¹é
å° {} å¼ ç¸å订åï¼{}ï¼å°å¤ç¨ææ©çä¸å¼ ", |
| | | purchaseRequest.getId(), candidates.size(), |
| | | candidates.stream().map(ErpPurchaseOrderDO::getNo).toList()); |
| | | } |
| | | |
| | | // 2. å è¡éåéæ°å è½½ï¼é¿å
å¹¶å审æ¹éå¤å ç¨ |
| | | ErpPurchaseOrderDO candidate = purchaseOrderMapper.selectByIdForUpdate(candidates.get(0).getId()); |
| | | if (candidate == null || !ErpAuditStatus.APPROVE.getStatus().equals(candidate.getStatus())) { |
| | | return null; |
| | | } |
| | | return candidate; |
| | | } |
| | | |
| | | /** |
| | | * å°éè´ç³è¯·å
³èå°å·²åå¨çéè´è®¢åï¼å¹¶æè®¢åæç»åå¡«ç³è¯·æç»ç¼å·ã |
| | | * 已被å
¶å®ç³è¯·å
³èè¿çæç»ä¸éå¤åå¡«ï¼åªå»ºç«ç³è¯·åä¸è®¢åçå
³èã |
| | | */ |
| | | private void linkPurchaseRequestToOrder(ErpPurchaseRequestDO purchaseRequest, |
| | | List<ErpPurchaseRequestItemDO> requestItems, |
| | | ErpPurchaseOrderDO purchaseOrder) { |
| | | List<ErpPurchaseOrderItemDO> orderItems = purchaseOrderItemMapper.selectListByOrderId(purchaseOrder.getId()); |
| | | List<ErpPurchaseOrderItemDO> remaining = new ArrayList<>(orderItems); |
| | | List<ErpPurchaseOrderItemDO> updates = new ArrayList<>(); |
| | | for (ErpPurchaseRequestItemDO requestItem : requestItems) { |
| | | ErpPurchaseOrderItemDO match = remaining.stream() |
| | | .filter(orderItem -> orderItem.getRequestItemId() == null) |
| | | .filter(orderItem -> Objects.equals(orderItem.getProductId(), requestItem.getProductId()) |
| | | && numberEquals(orderItem.getCount(), requestItem.getCount()) |
| | | && numberEquals(orderItem.getProductPrice(), requestItem.getProductPrice()) |
| | | && numberEquals(orderItem.getTaxPercent(), requestItem.getTaxPercent())) |
| | | .findFirst().orElse(null); |
| | | if (match != null) { |
| | | match.setRequestItemId(requestItem.getId()); |
| | | updates.add(match); |
| | | remaining.remove(match); |
| | | } |
| | | } |
| | | if (CollUtil.isNotEmpty(updates)) { |
| | | purchaseOrderItemMapper.updateBatch(updates); |
| | | } |
| | | |
| | | purchaseRequestMapper.updateById(new ErpPurchaseRequestDO() |
| | | .setId(purchaseRequest.getId()) |
| | | .setOrderId(purchaseOrder.getId()) |
| | | .setOrderNo(purchaseOrder.getNo())); |
| | | log.info("[linkPurchaseRequestToOrder] éè´ç³è¯·({}) å
³èéè´è®¢å({})ï¼åå¡«æç» {} æ¡", |
| | | purchaseRequest.getId(), purchaseOrder.getNo(), updates.size()); |
| | | } |
| | | |
| | | private String buildRequestFingerprint(Long supplierId, List<ErpPurchaseRequestItemDO> items) { |
| | | if (supplierId == null || CollUtil.isEmpty(items)) { |
| | | return null; |
| | | } |
| | | return buildFingerprint(supplierId, items.stream() |
| | | .map(item -> fingerprintPart(item.getProductId(), item.getCount(), |
| | | item.getProductPrice(), item.getTaxPercent())) |
| | | .toList()); |
| | | } |
| | | |
| | | private String buildOrderFingerprint(Long supplierId, List<ErpPurchaseOrderItemDO> items) { |
| | | if (supplierId == null || CollUtil.isEmpty(items)) { |
| | | return null; |
| | | } |
| | | return buildFingerprint(supplierId, items.stream() |
| | | .map(item -> fingerprintPart(item.getProductId(), item.getCount(), |
| | | item.getProductPrice(), item.getTaxPercent())) |
| | | .toList()); |
| | | } |
| | | |
| | | private String buildFingerprint(Long supplierId, List<String> parts) { |
| | | return supplierId + "|" + parts.stream().sorted().collect(java.util.stream.Collectors.joining(",")); |
| | | } |
| | | |
| | | private String fingerprintPart(Long productId, BigDecimal count, BigDecimal productPrice, |
| | | BigDecimal taxPercent) { |
| | | return String.join(":", String.valueOf(productId), normalizeNumber(count), |
| | | normalizeNumber(productPrice), normalizeNumber(taxPercent)); |
| | | } |
| | | |
| | | private String normalizeNumber(BigDecimal value) { |
| | | return value == null ? "" : value.stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | private boolean numberEquals(BigDecimal left, BigDecimal right) { |
| | | if (left == null) { |
| | | return right == null; |
| | | } |
| | | return right != null && left.compareTo(right) == 0; |
| | | } |
| | | |
| | | private boolean hasLinkedRequestItem(List<ErpPurchaseOrderItemDO> orderItems) { |
| | | return CollUtil.isNotEmpty(orderItems) |
| | | && orderItems.stream().anyMatch(item -> item.getRequestItemId() != null); |
| | | } |
| | | |
| | | /** |
| | | * æ ¡éªéè´ç³è¯·å®¡æ¹åç±»åæµç¨å®ä¹æ¯å¦åå¨ |
| | | */ |
| | | private void validatePurchaseRequestApproveCategoryAndProcessDefinition() { |
| | |
| | | ErrorCode ATTENDANCE_EXCEPTION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_004_015, "èå¤å¼å¸¸å®¡æ¹æµç¨ä¸åå¨"); |
| | | ErrorCode ATTENDANCE_CLOCK_ALREADY_CLOCKED = new ErrorCode(1_030_004_016, "å·²æè¿å¡ï¼è¯·å¿é夿å¡"); |
| | | ErrorCode ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID = new ErrorCode(1_030_004_017, "ç»ææ¥æã{}ãä¸è½æ©äºå¼å§æ¥æã{}ãï¼æ æ³ä¿åãè¯·éæ°éæ©æ£ç¡®çæ¥æåºé´"); |
| | | ErrorCode ATTENDANCE_EXCEPTION_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_004_018, "æ°å¢èå¤å¼å¸¸ç³è¯·å¤±è´¥ï¼å½åç»å½è´¦å·ï¼{}ï¼æªå
³èå工档æ¡ï¼æ æ³ç¡®å®ç³è¯·äººåé¨é¨ã请使ç¨å·²å
³èå工档æ¡çè´¦å·ç»å½ï¼æå
å¨ãå工档æ¡ãä¸ç»´æ¤å½åè´¦å·ååæäº¤"); |
| | | |
| | | // ========== èªé
¬ç®¡ç 1-030-005-000 ========== |
| | | ErrorCode SALARY_STRUCTURE_NOT_EXISTS = new ErrorCode(1_030_005_000, "èªé
¬ç»æä¸åå¨"); |
| | |
| | | import lombok.EqualsAndHashCode; |
| | | import lombok.ToString; |
| | | |
| | | import java.time.LocalDate; |
| | | |
| | | @Schema(description = "管çåå° - èå¤å¼å¸¸ç³è¯·å页 Request VO") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | |
| | | @Schema(description = "ç¶æ", example = "0") |
| | | private Integer status; |
| | | |
| | | @Schema(description = "å¼å¸¸æ¥æåºé´ï¼ä¼ [å¼å§æ¥æ, ç»ææ¥æ]", example = "[\"2026-01-01\", \"2026-12-31\"]") |
| | | private LocalDate[] date; |
| | | |
| | | } |
| | |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | import java.time.LocalDate; |
| | | |
| | | /** |
| | | * èå¤å¼å¸¸ç³è¯· Mapper |
| | | * |
| | |
| | | public interface HrmAttendanceExceptionMapper extends BaseMapperX<HrmAttendanceExceptionDO> { |
| | | |
| | | default PageResult<HrmAttendanceExceptionDO> selectPage(HrmAttendanceExceptionPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<HrmAttendanceExceptionDO>() |
| | | LambdaQueryWrapperX<HrmAttendanceExceptionDO> query = new LambdaQueryWrapperX<HrmAttendanceExceptionDO>() |
| | | .likeIfPresent(HrmAttendanceExceptionDO::getNo, reqVO.getNo()) |
| | | .eqIfPresent(HrmAttendanceExceptionDO::getUserId, reqVO.getUserId()) |
| | | .eqIfPresent(HrmAttendanceExceptionDO::getDeptId, reqVO.getDeptId()) |
| | | .eqIfPresent(HrmAttendanceExceptionDO::getType, reqVO.getType()) |
| | | .eqIfPresent(HrmAttendanceExceptionDO::getStatus, reqVO.getStatus()) |
| | | .orderByDesc(HrmAttendanceExceptionDO::getId)); |
| | | .eqIfPresent(HrmAttendanceExceptionDO::getStatus, reqVO.getStatus()); |
| | | // å¼å¸¸æ¥æåºé´çé |
| | | LocalDate[] dateRange = reqVO.getDate(); |
| | | if (dateRange != null && dateRange.length >= 2 && dateRange[0] != null && dateRange[1] != null) { |
| | | query.between(HrmAttendanceExceptionDO::getExceptionDate, dateRange[0], dateRange[1]); |
| | | } |
| | | return selectPage(reqVO, query.orderByDesc(HrmAttendanceExceptionDO::getId)); |
| | | } |
| | | |
| | | default HrmAttendanceExceptionDO selectByNo(String no) { |
| | |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionPageReqVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionSaveReqVO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceExceptionMapper; |
| | | import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; |
| | | import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| | | import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum; |
| | | import jakarta.annotation.Resource; |
| | |
| | | @Resource |
| | | private HrmAttendanceExceptionMapper attendanceExceptionMapper; |
| | | @Resource |
| | | private HrmEmployeeService employeeService; |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | @Resource |
| | | private HrmNoRedisDAO noRedisDAO; |
| | | @Resource |
| | | private BpmProcessInstanceApi bpmProcessInstanceApi; |
| | |
| | | public Long createAttendanceException(HrmAttendanceExceptionSaveReqVO createReqVO) { |
| | | HrmAttendanceExceptionDO attendanceException = BeanUtils.toBean(createReqVO, HrmAttendanceExceptionDO.class); |
| | | attendanceException.setNo(generateNo()); |
| | | // ç³è¯·äººé»è®¤ä¸ºå½åç»å½ç¨æ·å¯¹åºçåå·¥ï¼å端表åä¸éæ©ç³è¯·äºº |
| | | Long loginUserId = cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(); |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | AdminUserRespDTO loginUser = adminUserApi.getUser(loginUserId); |
| | | throw exception(ATTENDANCE_EXCEPTION_EMPLOYEE_NOT_EXISTS, |
| | | loginUser != null ? loginUser.getNickname() : String.valueOf(loginUserId)); |
| | | } |
| | | attendanceException.setUserId(employee.getId()); |
| | | if (attendanceException.getDeptId() == null) { |
| | | attendanceException.setDeptId(employee.getDeptId()); |
| | | } |
| | | attendanceException.setStatus(HrmAuditStatusEnum.DRAFT.getStatus()); |
| | | attendanceExceptionMapper.insert(attendanceException); |
| | | storageAttachmentApi.bindAttachments("file", "hrm_attendance_exception", |