feat(mes): 新增业务码支持并优化追溯查询功能
- 在批次、托盘、袋码表中新增 business_code 字段及唯一约束
- 实现内部扫码兼容业务码与历史追溯码的查询机制
- 添加补生成已有码业务码的功能接口
- 优化托盘绑定时品种信息的自动解析和合并逻辑
- 完善批次入库时有效袋码的进度计算机制
- 更新追溯响应VO增加入库时间等关键字段
- 重构批次预生成逻辑使用统一编码生成器分配码值
- 优化页面列表查询支持业务码模糊匹配搜索
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ä¸è¢åç 宿½è¯´æ |
| | | |
| | | ## è¡ä¸º |
| | | |
| | | åä¸è¢è®°å½æ°å¢ `businessCode`ï¼æ°æ®åº `business_code`ï¼ï¼å `code` ä¿ç为追溯ç ãä¸å¡ç ä¸è¿½æº¯ç åèªçæï¼åéè¿æ¢æ MES / Redis å
¨å±å¯ä¸ç¼ç çæå¨åé
16 使°åï¼ä»¥å符串ä¿ååä¼ è¾ã |
| | | |
| | | çææ°æ®å
ãçæåå§è¢ç ãç ´ææ¿æ¢ååæ¶çæä¸¤ç§ç ãæ´æ¢ç ´æè¢æ¶åæ¾æ°ä¸å¡ç ï¼æ§è¢ç ä»ææ¢ææµç¨ä½åºãä¸å¡äºç»´ç å
容为åå¨çä¸å¡ç å¼ï¼å¯¼åºå¾ç为äºç»´ç å 䏿¹ç å¼ï¼è¿½æº¯äºç»´ç ç»§ç»ä½¿ç¨æ¢æ H5 溯æºç½åè§åã |
| | | |
| | | ä¸è¢ä¸ç 管çå表æ°å¢ä¸å¡ç ååä¸å¡ç çéãExcel 忶坼åºè¿½æº¯ç ãä¸å¡ç ï¼å·¥å
·æ å¯åå«å¯¼åºè¿½æº¯äºç»´ç åä¸å¡äºç»´ç ï¼æçæç» UUID ææ¹æ¬¡å¯¼åºï¼è¡å
å¯é¢è§ä¸å¡äºç»´ç ãæªè¡¥ä¸å¡ç çåå²è®°å½ä¸è½å¯¼åºä¸å¡äºç»´ç ï¼å·²ä½åºè®°å½æé¤äºäºç»´ç æ¹é导åºã |
| | | |
| | | ## å岿°æ® |
| | | |
| | | å¾éå·²æææè¢ç ï¼ç¹å»âè¡¥çæä¸å¡ç âï¼å次æå¤ 200 æ¡ãæ¥å£æ²¿ç¨è¢ç çææé `mes:pro-bag-code:create`ãæå¡ææåºåçå»é ID å è¡éï¼ä»
为æ ä¸å¡ç çè®°å½çæï¼é夿ä½ä¸ä¿®æ¹å·²æç å¼ï¼ä¸æ¹åå追溯ç ãç»å®å
³ç³»ãåºååå°å·ç¶æãåå¨ç¼ºå¤±æä½åºè¢ç æ¶æ´æ¬¡æç»ã |
| | | |
| | | ## æ«ç å¤ç |
| | | |
| | | 管çåå°ä¸ç§»å¨ç«¯æä¸å¡ç æåå²è¿½æº¯ç æ¥æ¾åä¸è¢è®°å½ï¼å
¥åºè¯·æ±ä¿çå®é
æ«æç å¼ä¾¿äºå®¡è®¡ï¼åºåæµæ°´ä»ä»¥å追溯ç ååä¸è¢ ID 为è§èæ è¯ï¼ä¸å°åç ææä¸¤ä»½åºåãéå®åºåºæ£è´§æåä¸è¢ ID å»éï¼å¹¶ä¿çæ¢æè´¨æ£æ ¡éªï¼çç¹æ«ç å®ä½ä»åºæ¹æ¬¡åæ ·è¯å«ä¸å¡ç ã |
| | | |
| | | å
¬ä¼é²ä¼ª / 溯æºä»¥åå
¬ä¼æ«ç äºä»¶ç»§ç»åªä½¿ç¨è¿½æº¯ç ï¼ä¸å¡ç ä¸ä½ä¸ºå
¬ä¼è¿½æº¯å
¥å£ã管çåå°ç¨ä¸å¡ç æ¥å°ä¸è¢ä¿¡æ¯åï¼è¿½æº¯äºç»´ç é¢è§ä¼ä½¿ç¨è¯¥è¢å追溯ç ï¼é¿å
çææ æçå
¬ä¼é¾æ¥ã |
| | | |
| | | æ°å¢æ¥å£ï¼ |
| | | |
| | | - `POST /mes/pro/bag-code/generate-business-code`ï¼è¯·æ± `{ "ids": [1, 2] }`ï¼è¿åæ°çæçæ°éã |
| | | - `GET /mes/pro/bag-code/export-business-qr?uuid=...`ï¼æ `batchId=...`ï¼è¿å ZIPï¼æ²¿ç¨è¢ç å¯¼åºæéã |
| | | |
| | | ## é¨ç½² |
| | | |
| | | 1. å¨ç®æ MySQL åºæ§è¡ `sql/mysql/mes_pro_bag_business_code.sql` å `sql/mysql/mes_pro_pallet_batch_business_code.sql`ï¼ä¸ºè¢ç ãæçåçäº§æ¹æ¬¡æ°å¢å¯ç©ºç VARCHAR(16) åæ®µåå¯ä¸ç´¢å¼ãèæ¬æ£æ¥å段åç´¢å¼æ¯å¦åå¨ï¼å¯é夿§è¡ï¼ä¸èªå¨åå¡«æä¿®æ¹å·²æè¿½æº¯ç ã |
| | | 2. é¨ç½² MES å端åå端æå»ºãæ°å¢ DO æ¥è¯¢ä¼è¯»åè¯¥åæ®µï¼é¡»å
åçº§æ°æ®åºåå¯å¨å端ã |
| | | 3. 卿µè¯ä¸å¡æ°æ®ä¸éªè¯çæã导åºãæ«æãå
¥åºä¸éå®åºåºï¼å·²ææ ç¾éè¿å¾éè¡¥çæä¸å¡ç ï¼å导åºäº¤ä»å°å·ã |
| | | |
| | | 2026-10-10 å·²æç¨æ·è¦æ±å¯¹æ¬æºå端é»è®¤è¿æ¥ç `localhost:3306/mom-thzy` æ§è¡ä¸è¿°ä¸¤ä¸ªèæ¬ï¼ç¡®è®¤è¢ç ãæçãçäº§æ¹æ¬¡ä¸è¡¨åæ `business_code VARCHAR(16)` å¯ç©ºå段å `uk_business_code` å¯ä¸ç´¢å¼ãè¢ç åæå段åç´¢å¼éè¿èæ¬æ£æ¥åä¿çï¼æçåæ¹æ¬¡è¡¥é½ç¼ºå¤±ç»æãæ²¡ææ§è¡çå®åºååå
¥æåå²ä¸å¡ç åå¡«ï¼å
¶ä»é¨ç½²ç¯å¢ä»é¡»å嫿§è¡åçº§èæ¬ãæ°å»ºåºçåå§åè¡¨ç»æå·²åæ¥å
å«ä¸å¡ç åæ®µä¸å¯ä¸ç´¢å¼ã |
| | | |
| | | ## éªè¯ |
| | | |
| | | å端ç产æå»ºéè¿ï¼480Ã800 æ¨¡ææµè§å¨åå½è¦ç纯ä¸å¡ç è¯å«ãæ¾å¼ç¡®è®¤å
¥åºãè¶
æ¶éè¯ååæç§»å¨æµç¨ãå端ç¸å
³ 30 项æµè¯éè¿ï¼è¦çåç çæãæ¿æ¢ãè¡¥çæå¹ç / ä½åºæ ¡éªã纯ç äºç»´ç è¯è¯»ãå
¬ä¼ä¸å
鍿¥è¯¢å离ãè§èåºåç å¼ãæç䏿¹æ¬¡æå®é
è¢æ°å
¥åºãé宿£è´§å»éåè´¨æ£éå¶ï¼ä»¥åç§»å¨æé䏿«ç åå½ã |
| | | |
| | | å
¨é¡¹ç®ç±»åæ£æ¥ä»ææ¢æéè¯¯ï¼æ¬æ¬¡ä¸è¢ä¸ç ãä¸å¡äºç»´ç å¼¹å±ãAPI ä¸ç§»å¨æ¥è¯¢æ¹å¨æªè¢«æ¥åç±»åéè¯¯ãæ¨¡ææ¥å£ååå
æµè¯ä¸æ¿ä»£ç°åºè®¾å¤ä¸çå®åºåèè°ã |
| | | |
| | | ## æçåçäº§æ¹æ¬¡æ©å± |
| | | |
| | | æçåçäº§æ¹æ¬¡åæ ·æ¥æç¬ç«ç `businessCode`ãé¢çææ¶å³åé
å¹¶ä¿åï¼æçç»å®æ¹æ¬¡ãæ¹æ¬¡è¡¥å½çäº§ä¿¡æ¯æ¶ä¿æä¸å¡ç ä¸åãç´æ¥çææçãæ°å»ºçäº§æ¹æ¬¡ãçæè¢ç æ¶èªå¨å»ºç«çæçä¹çæä¸å¡ç ã |
| | | |
| | | é¢çæç»ä¸ä½¿ç¨ç¼ç çæå¨ï¼ä¸åæ§è¡æ§ç `MAX(CAST(SUBSTRING(temp_code, 4) AS UNSIGNED))` åºå·æ¥è¯¢ãè¯¥æ§æ¥è¯¢ä¼å°é¿æ°å临æ¶ç 误读为 Integer åºå·å¹¶æº¢åºï¼ä¿®å¤æ 鿏
çå岿 ç¾ææ¹åå·²æä¸´æ¶ç ï¼æ´æ°å端çæã |
| | | |
| | | æç管çæéä¸è®°å½è¡¥çæä¸å¡ç ã导åºçº¯ç äºç»´ç ï¼çäº§æ¹æ¬¡å¨è¡å
æ´å¤æä½ä¸è¡¥çæãé¢è§åå¯¼åºæ´å¥ä¸å¡äºç»´ç ï¼æ¹æ¬¡èªèº«åå
¶ä¸æææçï¼ãåå²è¡¥çææå¤ 200 æ¡ï¼ä»
补缺失记å½ï¼ä½åºæç¼ºå¤±è®°å½æ´æ¬¡æç»ï¼é夿ä½ä¿æå·²æç å¼ã |
| | | |
| | | å
鍿¥è¯¢æ¯æä¸å¡ç ãæ£å¼ç å临æ¶ç ãæ´æãæ´æ¹å
¥åºä½¿ç¨åæè®°å½çå®é
è¢æ°ååçäº§æ¹æ¬¡å
³èï¼æ«æä¸å¡ç ä¸ä¼å建æ°çåºå身份ãé宿£è´§çæ¢ææ´æè·¯å¾ä¹è¯å«æçä¸å¡ç ï¼æ¬æ¬¡æ²¡ææ°å¢æ´æ¹éå®åºåºæµç¨ãå
¬ä¼æº¯æºä»ä½¿ç¨å追溯å
¥å£ã |
| | | |
| | | æ°å¢æ¥å£ï¼ |
| | | |
| | | - `POST /mes/pro/pallet/generate-business-code`ã`POST /mes/pro/batch/generate-business-code`ï¼è¯·æ± `{ "ids": [1, 2] }`ã |
| | | - `POST /mes/pro/pallet/export-business-qr-by-ids`ï¼è¯·æ± `{ "ids": [1, 2] }`ã |
| | | - `GET /mes/pro/batch/export-business-qr?batchId=...`ï¼å¯¼åºæ¹æ¬¡ä¸æææçä¸å¡ç ZIPã |
| | | |
| | | ## 管ç页颿 ·å¼ |
| | | |
| | | 沿ç¨ç°æç³»ç»çèç½é
è²ãæé®ãè¡¨æ ¼åå¼¹å±ç»ä»¶ãè¢ç ãæçãæ¹æ¬¡å·¥å
·æ ä¿ææ é¢åè¡ï¼å¸¸ç¨æä½ç´æ¥å±ç¤ºï¼å¯¼åºéä¸å°â导åºâèåï¼è¡¥çæçä½é¢æä½æ¾å
¥âæ´å¤æä½âæè¡å
èåï¼é¿å
å·¥å
·æ å æ»¡æé®ãæ¡é¢å彿£æ¥ 1920 å 1280 宽度çä¸é¡µå¸å±ä¸ä¸å¡äºç»´ç é¢è§ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # æçä¸çäº§æ¹æ¬¡åç§ç»å® |
| | | |
| | | æ¹æ¬¡æ°å¢ãè¡¥å½ãç¼è¾ä»¥åæçç´æ¥ç»å®æ¹æ¬¡åæ ¡éªï¼é䏿çåªè½å±äºç¸ååç§ï¼ä¸ä¸æ¹æ¬¡å·²æåç§ãææè¢ç åå·²ç»å®æçä¸è´ãåç§ä¸åæç¼ºå¤±æ¶æç»æ´ä¸ªæä½ï¼åç§ç±æç带å
¥ï¼ä¸è½éè¿ä¿®æ¹è¯·æ±åæ°æ´æ¢å·²ææ¹æ¬¡çåç§ã |
| | | |
| | | æçæ°å¢ `item_id`ï¼è¢ç æ°æ®å
èªå¨çææçæ¶ç«å³ä¿ååç§ç¼å·ï¼é¦æ¬¡è£
è¢æ¶ç¡®å®åç§ï¼åç»åªè½è£
å
¥ååç§è¢ç ãå岿çå¯ä»¥ä»ææè¢ç æ¨æåç§ï¼æ··åç§æç¼ºå¤±åç§ä¸è½ç»å®ãä½åºè¢ç ä¸åä¸åç§æ¨æã |
| | | |
| | | æ¹æ¬¡è¡¨åæ¾ç¤ºåªè¯»åç§åç§°åç¼ç ï¼éæ©é¦ä¸ªæçåèªå¨å¸¦å
¥ï¼å¹¶ç¦ç¨å
¶ä»åç§å缺å°åç§çæçãç¼è¾å·²æåç§æ¹æ¬¡æ¶ï¼é项æè¯¥åç§éå¶ãæçåæ¹æ¬¡å表å Excel åæ¶å±ç¤ºåç§åç§°åç¼ç ï¼æªç»å®æ¹æ¬¡çèªå¨æçä¹è½å±ç¤ºåç§ã |
| | | |
| | | è¢ç å½å
¥æ¹æ¬¡æ¶çæç级èç»å®åæ ·æ§è¡åç§æ ¡éªï¼é¿å
ç»è¿æ¹æ¬¡é¡µé¢éå¶ãç»å®å®æååæ¥æ´æ°æ¹æ¬¡å®æ´æ§æè¦ã |
| | | |
| | | ## æ°æ®åº |
| | | |
| | | åçº§èæ¬ï¼`sql/mysql/mes_pro_pallet_item.sql`ãæ§è¡åæ ¸å¯¹ç®æ åºï¼æ°å¢æçåç§å段ï¼åªåå¡«ç±ææè¢ç å¯ä¸ç¡®å®ãä¸ä¸ä¸å·²ææ¹æ¬¡åç§å²çªçè®°å½ãä¸ä¼æ´æ¹å·²æç 弿åºåã |
| | | |
| | | 2026-10-10 å·²éè¿ MySQL MCP 卿¬å° `mom-thzy` 宿å级ï¼åå¡« 2 个æçãåæ¹æ¬¡ 1 çæè¦è¦çåç§ï¼ç´æ¥ä¿®æ¹å岿è¦è¢«èªå¨å®¡æ¹æç»ï¼å·²æ¤åæ¬æ¬¡è¯¥æ¹æ¬¡çåç§åå¡«ï¼ä¿çå审计记å½ãå岿¹æ¬¡éè¿æ£å¸¸ç¼è¾ä¿åå³å¯ç±ææè¢ç åæçæ¨æåç§å¹¶å·æ°æè¦ï¼è¿ç§»èæ¬ä»
åå¡«å°æ 宿´æ§æè¦çæ¹æ¬¡ï¼é¿å
æ£å¸¸åçº§é æç¯¡æ¹è¯¯æ¥ã |
| | | |
| | | ## éªè¯ |
| | | |
| | | ç¸å
³æµè¯è¦çèªå¨æçä¿ååç§ãååç§å¸¦å
¥ãæ··åç§æç»ãå·²ææ¹æ¬¡åç§éå¶ãææè¢ç 䏿çå²çªã缺失åç§æç»ãä½åºè¢ç 忽ç¥ã请æ±ä¼ªé åç§æç»ï¼ä»¥åä¸å¡ç åå
¥åºåå½ãæµè§å¨æµè¯ä½¿ç¨æ¨¡ææ¥å£ï¼æ£æ¥æ¹æ¬¡è¡¨ååç§å¸¦å
¥åé项ç¦ç¨ï¼ä¸æ§è¡çå®ä¸å¡ç»å®æåºååå
¥ã |
| | | |
| | | 代ç éæ´æ°å端并é¨ç½²å端æå»ºåçæï¼æ°æ®åºå级已æ§è¡ï¼ä½æ¬æ¬¡æªéå¯ç¨æ·çå端è¿ç¨ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç§»å¨ç«¯ä¸æå®æ½è¯´æ |
| | | |
| | | 宿½æ¥æï¼2026-10-10ã便®ãç§»å¨ç«¯è®¾è®¡æ¹æ¡.mdã第 9 è第ä¸é¶æ®µï¼åºäºç°æ `mom-pro2-before` 项ç®å¢å ç§»å¨é¡µé¢ï¼å¤ç¨å½åå端ãè´¦å·åè§è²æéã第äºãä¸é¶æ®µä¸å¨æ¬æ¬¡äº¤ä»èå´ã |
| | | |
| | | ## ä½¿ç¨æ¹å¼ |
| | | |
| | | è¿å
¥åç³»ç»ç»å½å°åï¼éæ©âç§»å¨ç«¯âåç»å½ï¼è¿å
¥ `/mobile/home`ã馿¬¡æ CSS è§å£å®½åº¦åå§åï¼å°äº 768px é»è®¤ç§»å¨ç«¯ï¼å¦åé»è®¤ PC 端ï¼å¯ä»¥æå¨ä¿®æ¹ãéæ©å¨å½åæ ç¾é¡µç sessionStorage ä¿åï¼å·æ°ãæè½¬åçªå£ç¼©æ¾ä¸ä¼è¦çï¼æ¾å¼éåºæ¸
çéæ©åä½ä¸ç¼åï¼ç»å½è¿æéæ°è®¤è¯ä¿çå¾
确认æä½ãç»å½æ¹å¼ä¸ççä¿¡ mobile ä¸çé¢ terminalType åå¼ã |
| | | |
| | | ç§»å¨å¸å±ä»¥ 480Ã800 CSS è§å£ä¸ºåºåï¼æå¤§å®½åº¦ 480pxï¼æ¯æå
容æ»å¨ãå®å
¨åºå软é®çã忢 PC ç«¯éæ°å è½½å¯¹åºæéèåãPC 页继ç»ä½¿ç¨ç°æå¸å±ã |
| | | |
| | | ## å·²å®ç°åè½ |
| | | |
| | | | é¡µé¢ | åè½ | |
| | | |---|---| |
| | | | å·¥ä½å° / ä½ä¸ | å·²ææå
¥å£ã䏿¬¡æäº¤çä½ç½®ãæè¿æ¬äººè®°å½ãæ æéåé¦ | |
| | | | æ«ç å
¥åº | ç产æåä»åºåºåºä½éæ©ï¼è¢ç / æçç / æ¹æ¬¡ç è¯å«ï¼æ ¸å¯¹å确认ï¼ç»æå±ç¤º | |
| | | | è¢ç æ¥è¯¢ | æ«ç åå表æ¥è¯¢ã宿´è¯¦æ
ãç®æ æçä¸å¾
ç»å®è¢ç ééãéå¤ééæç»ã确认ç»å® | |
| | | | æçæ¥è¯¢ | æ«ç åå表æ¥è¯¢ã宿´è¯¦æ
ãç»å®ç®æ æ¹æ¬¡ | |
| | | | æ¹æ¬¡æ¥è¯¢ | æ«ç ãå表åç产信æ¯è¯¦æ
| |
| | | | åºåæ¥è¯¢ | åç§ãæ¹æ¬¡ãå¯æç´¢çä»åº / åºåº / åºä½çéï¼åºåæ°éä¸å使ç»ï¼è¿åä¿çæ¥è¯¢ä¸ä¸æ | |
| | | | ä½ä¸è®°å½ | æå¡ç«¯é宿¬äººï¼ææ¶å°æ¶é´åç»æçéãå页ãé误åæä½å·æç» | |
| | | | æç | 使ç¨è¯´æã忢 PCãéåº | |
| | | |
| | | æåå¤´ä½¿ç¨ getUserMedia + æéå è½½ jsQRï¼ç¨æ·è§¦åææï¼è¯å«åæåï¼ç¦»é¡µæååå°åæ¢ç¸æºãæ æ³ä½¿ç¨ç¸æºæ¶å¯æå¨è¾å
¥æä½¿ç¨é®çæ¨¡ææ«ç 设å¤ã溯æºé¾æ¥åªæå code åæ°ï¼ä¸æå¼é¾æ¥ãè¯å«ä¸ä¼èªå¨å
¥åºæç»å®ã |
| | | |
| | | è¢ç æ°éå
许ç¼è¾ï¼æ´æ / æ´æ¹æ°éææ¢ææå¡ç«¯è§å计ç®ï¼å¹¶è®°å
¥äºä»¶ã第ä¸çä¸ºåæ¬¡ç¡®è®¤ï¼ä¸èªå¨è¿ç»æäº¤ãç»å®æ¥å£æ²¿ç¨å·²æäºå¡æ¥å£ï¼è¿åæ´ä½æåç»æï¼æ²¡æèæéæ¡æå¡ç«¯åæ§ãç»æä¸ç¡®å®æ¶ä¿ååæä½å¿«ç
§ï¼éè¿å¯¹è±¡å
³ç³»æ ¸å®ã |
| | | |
| | | ## æéä¸å端æ¹å¨ |
| | | |
| | | `GET /system/auth/get-permission-info?terminalType=mobile` å
èµ°ç°æç¨æ·ãè§è²ãç§æ·ææé»è¾ï¼åå°å¯è§èååå¯è§ç¶ç®å½ä¸çå·²éé
ç»ä»¶æ å°ä¸ºç§»å¨èåãçç¥åæ°æä¼ pc ä¿æåèåã端类åä¸ä¼å¢å æ¥å£æéã |
| | | |
| | | 馿æ å°ä¿åå¨ `MobileMenuAdapter.java`ï¼æ éæ°å¢èåæä¿®æ¹æ°æ®åºï¼ |
| | | - mes/pro/scan-in/index â /mobile/inbound |
| | | - mes/pro/bagCode/index â /mobile/bags |
| | | - mes/pro/pallet/index â /mobile/pallets |
| | | - mes/pro/batch/index â /mobile/batches |
| | | - wls/materialstock/index â /mobile/stock |
| | | |
| | | å端å
¥å£åæ¶æ£æ¥è¿åèåä¸å¯¹åºæéç ï¼ç´æ¥è¾å
¥ URL ä¹ç»è¿å®å«ãå
¥åºãæ¥è¯¢ãç»å®ç»§ç»ç±åæ¥å£æ£æ¥æéãâè®°å½âéè¦æ«ç è®°å½ query æéåæ«ç èåã |
| | | |
| | | æ°å¢ `GET /mes/pro/scan-in/my-page` å `GET /mes/pro/scan-in/my-event`ãæ¬äººå页è¦ç客æ·ç«¯ä¼ å
¥ç creatorï¼åæ¡æ ¸å®ä»
è¿åæ¬äººäºä»¶ï¼my-event å¯ç±å
¥åºæé访é®ï¼ä¾¿äºæ²¡æè®°å½æ¥è¯¢æéçæä½åæ ¸å®æäº¤ç»æãç ç±»åè§£æå
许è¢ç ãæçãæ¹æ¬¡æ¥è¯¢æéä¸çä»»ä¸é¡¹ï¼å
·ä½è¯¦æ
仿ç ç±»åæéæ£æ¥ã |
| | | |
| | | å
¥åº eventId å²çªä¸åæ¹åæ§äºä»¶ãç¸åæä½äººã对象ãç±»åãä½ç½®ä¸æææ°éçéè¯è¿ååç»æï¼å
容ææä½äººä¸ååæç»ãè¢ç æ°éå䏿¯è¾ï¼æ´æ / æ´æ¹æ°éç±æå¡å¨å³å®ï¼éè¯ä¸ä»¥å®¢æ·ç«¯å 使°éæ¹ååè®°å½ã |
| | | |
| | | ## å¼±ç½ä¸ç¼å |
| | | |
| | | å
¥åºæäº¤åä¿å宿´è¯·æ±åç¨³å® eventIdãè¶
æ¶æ¾ç¤ºâç»æå¾
确认âï¼å·æ°æéæ°ç»å½å坿 ¸å®æ¬äººäºä»¶ï¼éè¯æ²¿ç¨å请æ±ï¼ä¸äº§çæ°æä½å·ãç»å®è¶
æ¶ä¿åç®æ ä¸å¯¹è±¡ IDï¼æ ¸å®å·²ç»å®å
³ç³»ï¼ç»ææ··åæ¶ä¿æå¾
ç¡®è®¤ç¶æå¹¶æç¤ºäººå·¥æ ¸å¯¹ã没æç¦»çº¿èªå¨åå
¥ã |
| | | |
| | | å¾
确认æä½ç¼åæå½åç¨æ·å页é¢åºåï¼ä»
ä¿åå¨å½åæ ç¾é¡µãè¢ç / æç / æ¹æ¬¡æ¥è¯¢ååºå页é¢å¨å½åç§»å¨å¸å±å
ä¿ççéãå·²å è½½å表ã详æ
䏿»å¨ä½ç½®ï¼å·æ°æµè§å¨ä¼éæ°è·åå表ï¼éåºç»å½ä¼éæ¯é¡µé¢ç¼åãå è½½æ´å¤æ²¿ç¨å·²æäº¤çéï¼å¤±è´¥ä¸è·³é¡µï¼ä¿®æ¹æ¡ä»¶åç¹å»æ¥è¯¢æåºç¨æ°æ¡ä»¶ãæ¾å¼éåºä¼æ¸
çï¼éåºåææªæ ¸å®ä¸å¡åºå
å®ææ ¸å®ãåºä½å¨éæ°éæ©æ¶æ£æ¥å»ç»åå¯ç¨ç¶æï¼å
¥åºä½ç½®å¨ç¡®è®¤é¡µå次å±ç¤ºã |
| | | |
| | | ## éªè¯ä¸å¯å¨ |
| | | |
| | | æµè§å¨åå½èæ¬ï¼`node scripts/mobile-ui-smoke.cjs`ãé»è®¤æµè¯å°å http://127.0.0.1:5173ï¼å¯ç¨ MOBILE_TEST_URL ä¿®æ¹ï¼éå·²å®è£
Edge å scripts å
ç Playwrightãèæ¬æ¦æªå
¨é¨ä¸å¡ APIï¼ä½¿ç¨ç¬ç«æ¨¡æè´¦å·ï¼ä¸è¿æ¥çå®åºåãè¦ç 480Ã800 ç»å½é»è®¤ãæå¨éç«¯ä¿æãæ«ç ä»
ééãç¦ç¨åºä½ãç¸æºä¸å¯ç¨åè¾å
¥ãå
¥åºè¶
æ¶å·æ°æ¢å¤ãå请æ±éè¯ãéå¤è¢ç ã确认ç»å®ãæ¬äººè®°å½ãåºåæç»ãæ æéè·¯ç±ãèåå 载失败æ¢å¤ãPC èåéè½½ãéåºæ¸
çãPC é»è®¤ãæç´¢é¢æ¿ç©ºç»æä¸ç¦ç¹å¾ªç¯ãå
³éæ»å¨æ¢å¤ãå表è¿åç¶æä¸æ»å¨ä½ç½®ä¿çãå页ç½ç»å¤±è´¥éè¯åæªæäº¤çéé离ã |
| | | |
| | | å端ç产æå»ºéè¿ãå
¨é vue-tsc 仿项ç®å·²æä¸å¡é¡µé¢ / å
Œ
±ç±»åæ¥éï¼æ°å¢ mobile 页é¢åæ¬æ¬¡è®¤è¯æ¹å¨æ²¡æè¢«æ¥åé误ï¼ä¸è½å°æ¤æ¬¡æå»ºéè¿çåäºå
¨é¡¹ç®ç±»åæ£æ¥éè¿ã |
| | | |
| | | å端éªè¯å½ä»¤ï¼ |
| | | ```powershell |
| | | mvn -o -pl yudao-module-mes -am '-Dtest=MobileMenuAdapterTest,MobileScanAccessTest,MobileScanRetryTest,MobileScanValidityTest' '-Dsurefire.failIfNoSpecifiedTests=false' test |
| | | ``` |
| | | |
| | | 13 项æµè¯å
¨é¨éè¿ï¼è¦çç§»å¨èåéèç¶ç®å½ãPC èåå对象ä¿çã伪é creatorãè·¨ç¨æ·äºä»¶è¯»åãéå¤è¯·æ±ãå
容å²çªãè·¨ç¨æ·éè¯ãè¢ç æ°éå²çªåæ´æå®é
æ°ééè¯ãä½åºè¢ç / æç / æ¹æ¬¡æç»å
¥åºä¸éæ³æ«ç ç±»åæç»ã |
| | | |
| | | å¼åé¢è§ï¼å¨ mom-pro2-before æ§è¡ `pnpm dev --host 127.0.0.1`ï¼ä»¥å®é
æ§å¶å°å°å为åãäº¤ä»æå»ºå¨è¯¥é¡¹ç® dist å dist.zipï¼æ²¿ç¨åé¨ç½²æ¹å¼ï¼åæ¶é¨ç½²å端 system / mes æ¹å¨ãç产 build ä¿çæ¢æå
¬å¸ç¯å¢å API é
ç½®ï¼ä¸çº¿åæç®æ ç¯å¢ç¡®è®¤ã |
| | | |
| | | ## ç°åºèè°é¡¹ç® |
| | | |
| | | å½å宿代ç ãæå»ºãæ¨¡ææµè§å¨ååå
æµè¯éªè¯ï¼å°æªé¨ç½²ç产ï¼ä¹æ²¡ææ§è¡å®é
åºååå
¥ãç°åºéç¨ç®æ ææº / PDA æ ¸æ¥ HTTPS ç¸æºãå®ç©äºç»´ç åæ«ç æªç»æå符ï¼åç¨æµè¯ä¸å¡æ°æ®æ ¸å¯¹åºåãåä½ãæ´æ / æ´æ¹å
¥åºåç»å®äºå¡ã |
| | | |
| | | èåä¾èµç°ææ°æ®åºè§è²é
ç½®åç¶ç®å½å¯è§æ§ãå
·æå
¥åºæéçç¨æ·è¿é对åºç ç±»åæ¥è¯¢æéï¼éèç¶ç®å½ä¸ä¼è¢«ç§»å¨ç«¯èªå¨å¼æ¾ãæ¢æä»åºæ¥å£ä¸æ°æ®æéç»§ç»çæï¼æ¬æ¬¡æ²¡ææ°å¢å²ä½ææ©å¤§ä»åºæéã |
| | | |
| | | å®ç°éç¨é离ç CSSãåç dialog åºé¨æç´¢é¢æ¿ä¸è¡¨åæ§ä»¶ï¼æªå¼å
¥ Vantãåºåºåºä½æ¯æåç§° / ç¼ç æç´¢ãçº§èæ¸
空ãå è½½åé¦ï¼é¢æ¿å
³éãç¦»é¡µæ¶æ¢å¤ç¦ç¹å页颿»å¨ãå¯ç¼è¾ç§»å¨èå端é
ç½®ãå²ä½å¸å±ãåºåºçç¹æ¥å·¥å±äºåç»å¢å¼ºãå
¬ä¼é²ä¼ª / 溯æºé¡µç¬ç«ï¼æ¬æ¬¡ç§»å¨å
¥å£ä¸æ¿ä»£å
¬ä¼æ¥è¯¢ã |
| | | |
| | | ## æ¬è½®è¡¥å
ï¼2026-10-10ï¼ |
| | | |
| | | è¡¥é½å¯æç´¢çåºé¨ä½ç½®éæ©é¢æ¿ãæ¥è¯¢è¿åä¸ä¸æãå页çéé离ãç¸æºéç¼å页é¢ç¦»å¼åæ¢ãæµè§å¨ç¼©æ¾ä¸å®å
¨åºæ¯æãä½åºå¯¹è±¡å¯ä»¥ç»§ç»æ¥è¯¢ï¼ä½åç«¯é»æ¢å
¥åºåç»å®ééï¼å端对è¢ç ãæçåæå±çäº§æ¹æ¬¡ä½åºç¶æå次éªè¯ï¼å¹¶è®°å½å¤±è´¥äºä»¶èä¸å建åºåæµæ°´ãPC å¸å±ç»§ç»æ²¿ç¨åå®ç°ã |
| | | |
| | | ## å·¥ä¸ç»ç«¯å¯å¨ææ¥ï¼2026-10-10ï¼ |
| | | |
| | | ç°åºç
§ç确认 DS-MDT302 è¿è¡ Android 10 / Chrome 77.0.3865.92ï¼å¨ http://192.168.0.10:5666/ åçäºå¯å¨ç»é¢ï¼æ®éææºå¯æ£å¸¸è®¿é®ã已确认å½å Vite å¼å客æ·ç«¯å
å«å¯éé¾ / 空å¼åå¹¶è¯æ³ï¼Chrome 77 æ æ³è§£æï¼æ¤å¤ Tailwind 4 ç宿¹ Chrome åºçº¿ä¸º 111ãéä½ç产 build.target ä¸è½ä½¿å½åå¼åæå¡åå
¨é¨æ ·å¼å
¼å®¹ Chrome 77ã |
| | | |
| | | ä¼å
å°è®¾å¤ Chrome æ´æ°å°ææ°å¯å®è£
ç稳å®çï¼æ ·å¼æä½è¦æ± Chrome 111ï¼ä½æ¤çæ¬å·ä¸ä»£è¡¨å¼åæå¡å
¨é¨ä¾èµçå
¼å®¹ä¿è¯ï¼ãAndroid 10 符å Chrome å½åå®è£
è¦æ±ï¼ä¼ä¸è®¾å¤çå®è£
æéä»éç°åºç¡®è®¤ãå¦æè®¾å¤å¿
é¡»ä¿ç Chrome 77ï¼éè¦å¦è¡å¤çå¼åå
¥å£ãä¾èµè¿è¡æ¶ APIãæ ·å¼é级å宿ºåå½ï¼ä¸è½å®£ç§°ç°æçæ¬å·²å
¼å®¹ã |
| | | |
| | | æ°å¢ç¬ç« `/browser-check.html` æ£æµé¡µï¼ä½¿ç¨ ES5 èæ¬æ¾ç¤º UAãæ¨¡å导å
¥ãå¯éé¾ãProxyãåå¨ãå¼¹å±çåºç¡è½åãæ°å¢åºç¨å¯å¨å¤±è´¥å 30 ç§æªå®æåå§åæç¤ºï¼æ¾ç¤ºéè¯¯ä¸æ£æµå
¥å£ï¼åå§åæç»æå伿¸
é¤æç¤ºã主åºç¨å¯¼å
¥/è¯æ³å¤±è´¥æ¶æç¤ºèæ¬ä»è½ç¬ç«å·¥ä½ï¼è¯æä¿¡æ¯ä»
å±ç¤ºæ¬æºï¼ä¸ä¸ä¼ ãç¼åæ ç¾æåºæ¹ä¸ºå¨ filter è¿åçæ°æ°ç»ä¸ sortï¼ç§»é¤è¯¥å¤ toSorted API ä¾èµã |
| | | |
| | | æå»ºéè¿ï¼å¯å¨æ£æµåå½å½ä»¤ä¸º `node scripts/browser-startup-smoke.cjs`ãæ¤æ£æµä¸çåäº Chrome 77 æ´ä¸ªå¹³å°å®æºå
¼å®¹éªè¯ãæºç ä¿®æ¹éç±å®é
æä¾ 5666 æå¡çå端å®ä¾å è½½ï¼å°æªæä½è®¾å¤ææ¿æ¢ç°åºæå¡ã |
| | | |
| | | 便®ï¼[Vite å¼åæµè§å¨è¦æ±](https://vite.dev/guide/)ã[V8 å¯é龿¯æçæ¬](https://v8.dev/features/optional-chaining)ã[Tailwind æ ·å¼å
¼å®¹è¦æ±](https://tailwindcss.com/docs/compatibility)ã[Chrome Android å®è£
è¦æ±](https://support.google.com/chrome/answer/95346?co=GENIE.Platform%3DAndroid)ã |
| | |
| | | # MOM ç§»å¨ç«¯è®¾è®¡æ¹æ¡ |
| | | |
| | | çæ¬ï¼ä¼å稿 V3ï¼ui-ux-pro-maxï¼ï¼2026-10-09ãç¨éï¼åè½ä¸äº¤äºè¯å®¡ï¼å°æªå®æ½ã |
| | | çæ¬ï¼ä¼å稿 V3ï¼ui-ux-pro-maxï¼ï¼2026-10-09ã2026-10-10 å·²æç¬¬ 9 è宿½ç¬¬ä¸é¶æ®µï¼å¹¶æ©å±ç§åè´¨æ£ãè´¨æ£è®°å½åéå®åºåºï¼äº¤ä»æ
åµè§ãç§»å¨ç«¯ä¸æå®æ½è¯´æ.mdãåãç§»å¨ç«¯è´¨æ£åºåºä¸èªå¨å®æå®æ½è¯´æ.mdããå
¶ä½ç¬¬äºãä¸é¶æ®µè½åä»ä¸ºè§åã |
| | | |
| | | æ¬çä¿çæµè§å¨è®¿é®ã480 à 800 è§å£ãç»å½æ¶éæ©ç§»å¨ç«¯ / PC ç«¯ä»¥åæ°æ®åºæéæ å°ã设计æ¹å为æ¸
æ°ãç´§åçç°åºä½ä¸çé¢ï¼ä¸é¡µå´ç»ä¸ä¸ªä¸»è¦ä»»å¡ï¼å§ç»æ¾ç¤ºä½ä¸å¯¹è±¡ä¸ä½ç½®ï¼æä½ç»æå¯ä»¥åçã第 11ï½12 èä¸ºæ¬æ¬¡è¡¥å
çè§è§ç³»ç»ã页é¢è§æ ¼åè¯å®¡æ åã |
| | | |
| | |
| | | | æ¹æ¬¡æ¥è¯¢ | æ¹æ¬¡ç产管ç / 602612 | `mes/pro/batch/index` | 馿æ¥è¯¢ | |
| | | | åºåæ¥è¯¢ | åºåç°æé / 5786 | `wls/materialstock/index` | 馿æ¥è¯¢ | |
| | | | å
¥åºä»»å¡ | 产åå
¥åº / 5819 | `wls/productreceipt/index` | 第äºé¶æ®µ | |
| | | | åºåºä»»å¡ | éå®åºåº / 5970 | `wls/productsales/index` | 第äºé¶æ®µ | |
| | | | éå®åºåº | éå®åºåº / 5970 | `wls/productsales/index` | å·²éé
æ¥è¯¢ãæ«ç æ£è´§ãè¿åãæ§è¡åºåº | |
| | | | ç§åè´¨æ£ | ç§åè´¨æ£ / 1065217 | `mes/qc/seedQuality/index` | å·²éé
è¢ç ãæçç ãçäº§æ¹æ¬¡ç ååé¡¹ææ æäº¤ | |
| | | | è´¨æ£è®°å½ | è´¨æ£è®°å½ / 1065224 | `mes/qc/seedQuality/record` | å·²éé
è¢ç ãæ¹æ¬¡ãç»æçé | |
| | | | åè´§ä»»å¡ | åè´§éç¥ / 5867 | `wls/salesnotice/index` | 第äºé¶æ®µ | |
| | | | çç¹ä»»å¡ | çç¹ä»»å¡ / 5957 | `wls/stocktaking/task/index` | 第äºé¶æ®µ | |
| | | | åºåæµæ°´ | åºåäºå¡æµæ°´ / 602476 | `wls/transaction/index` | 馿ç®çæ¥è¯¢ | |
| | |
| | | |
| | | ç§»å¨ç«¯åéå
¥å£ â© å½åç¨æ·ææåè½ â© ç§æ·å¼æ¾èå´ï¼ææå®é
èåãæ¯ä¸ªæé®åæ£æ¥å¯¹åºæä½æéï¼æææ¥å£ç»§ç»åæå¡ç«¯æ ¡éªãéèç®å½çç§»å¨å¼æ¾çç¥éè¦æç¡®é
ç½®ï¼ä¸è½å ææºç«¯å
¥å£éç»èç»è¿æéã |
| | | |
| | | ç«¯éæ©è´è´£ç¡®å®âå±ç¤ºåªå¥åè½çé¢âï¼è´¦å·æéè´è´£ç¡®å®âå
许访é®åªäºåè½âã建议å端æéè·åæ¥å£æ¯æç¬ç«å段 `terminalType=pc|mobile`ï¼è¿åæé端éç¨çå·²ææèåï¼è¿æ¯å¾
å¼åçæ¥å£æ¹æ¡ï¼ç°ææ¥å£å°æªæ¯æè¯¥åæ°ã端类åä¸è½ä½ä¸ºæ°å¢æéç便®ã |
| | | ç«¯éæ©è´è´£ç¡®å®âå±ç¤ºåªå¥åè½çé¢âï¼è´¦å·æéè´è´£ç¡®å®âå
许访é®åªäºåè½âãå端æéè·åæ¥å£å·²æ¯æç¬ç«å段 `terminalType=pc|mobile`ï¼è¿åæé端éç¨çå·²ææèåï¼çç¥æ¶ä¿æ PC 端è¡ä¸ºã端类åä¸è½ä½ä¸ºæ°å¢æéç便®ã |
| | | |
| | | PC 端ä¿çå½åèåç»ä»¶æ å°ãç§»å¨ç«¯å»ºç«å¯¹åºçç§»å¨ç»ä»¶æ å°ååè½æ¸
åï¼å·²ç»éé
䏿æéçåè½æè¿å
¥èåãèå端é
ç½®å»ºè®®æ¯æ PCãç§»å¨ãå端ï¼é
ç½®å±çº§åè¿ç§»æ¹å¼å¨å¼åæ¶ç¡®è®¤ãç¶ç®å½ãæé®æéåç§æ·èå´éè¦ä¸èµ·å¤çï¼ä¸è½åªæ ¹æ®èåè·¯å¾åç§°è¿æ»¤ã |
| | | |
| | |
| | | |
| | | ### 访é®åç»å½ |
| | | |
| | | ç¨æ·éè¿ HTTPS å°åè¿å
¥ç»ä¸ç»å½é¡µï¼éæ©ç§»å¨ç«¯æ PC 端ï¼ä½¿ç¨ç°æç³»ç»è´¦å·ç»å½ï¼å¤ç¨ç»å½æå·æ°ãéåºåæéè·åæºå¶ã建议移å¨ä¸å¡å
¥å£è·¯å¾ä¸º `/mobile/`ï¼æ¡é¢ä¸å¡åå
¬ä¼æº¯æºé¡µä¿çåèªå
¥å£ï¼è¿æ¯è§åè·¯å¾ï¼å½åå°æªå建ã |
| | | ç¨æ·éè¿ HTTPS å°åè¿å
¥ç»ä¸ç»å½é¡µï¼éæ©ç§»å¨ç«¯æ PC 端ï¼ä½¿ç¨ç°æç³»ç»è´¦å·ç»å½ï¼å¤ç¨ç»å½æå·æ°ãéåºåæéè·åæºå¶ãç§»å¨ä¸å¡å
¥å£è·¯å¾å·²å®ç°ä¸º `/mobile/`ï¼æ¡é¢ä¸å¡åå
¬ä¼æº¯æºé¡µä¿çåèªå
¥å£ã |
| | | |
| | | ç»å½é¡µæ ¹æ®è§å£å®½åº¦é»è®¤éæ©ç«¯ï¼ç¨æ·æå¨éæ©å
·æä¼å
æãæé端å³å®ä¸å¡å¸å±åèåï¼åç»å®½åº¦åååªåå¸å±éé
ãè¾çªçæ¡é¢æµè§å¨ä¹è½ç¨äºé¢è§ï¼ä½ç°åºéªæ¶å¿
é¡»ç¨ç®æ 设å¤å®æã |
| | | |
| | |
| | | | å
容边è·ä¸åç»é´è· | 96px | é¦å°¾ååºåé´çç½ | |
| | | | å½åä½ä¸ä¸ä¸æ | 64px | å²ä½ãä½ç½®ã忢å
¥å£ | |
| | | | 主æä½åºå | 96px | æ«ç å
¥åº 52pxï¼æ«ç æ¥è¯¢æ¬¡è¦å
¥å£ | |
| | | | 常ç¨å
¥å£ | 160px | 两è¡ä¸¤åï¼å
±å项 | |
| | | | 常ç¨å
¥å£ | ææéèªç¶å¢é¿ | 两åæåå
¨é¨å·²ææå
¥å£ï¼å
å«è´¨æ£ä¸åºåºï¼å
容è¶
åºæ¶æ»å¨ | |
| | | | æè¿è®°å½é¢è§ | 220px | æ é¢å两æ¡è®°å½ï¼æä¾âæ¥çå
¨é¨â | |
| | | | å¼¹æ§ä½é | 44px | é¿ä½ç½®åç§°ãæç¤ºæå
容åå | |
| | | |
| | | æ»è®¡ 680pxãå¾
å任塿¥éå坿¿æ¢è®°å½é¢è§ï¼ä¸å¨é¦å±å å æ°çç»è®¡åºãå²ä½æ²¡æå个å¯ç¨å
¥å£æ¶ï¼æå®é
æ°éæåï¼ä¸ç¨æ æåè½è¡¥é½ã |
| | | åå项å
¥å£æ¶æ»è®¡çº¦ 680pxï¼æ©å±å
¥å£åæå®é
æéèªç¶å¢é¿å¹¶æ»å¨ãå¾
å任塿¥éå坿¿æ¢è®°å½é¢è§ï¼ä¸å å æ°çç»è®¡åºï¼ä¸ç¨æ æåè½è¡¥é½ã |
| | | |
| | | æ«ç 页éç¨ 56px æ é¢ã64px ä¸ä¸æã600px ä¸é´åºåå80px åºé¨æä½ãä¸é´åºå容纳约 320px 忝åºãæä½è¯´æãè¯å«ç¶æåæå¨è¾å
¥å
¥å£ï¼æåæ¾å¨æäº®èæ¯ä¸ï¼é¿å
ç´æ¥è¦çå¤æç¸æºç»é¢ãç¸æºåæ¯ä¸è¯å«æ§è½é¡»å¨çå®è®¾å¤éªè¯ã |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # ç§»å¨ç«¯è´¨æ£ãéå®åºåºä¸ç产å
¥åºèªå¨å®æ |
| | | |
| | | 宿½æ¥æï¼2026-10-10ãåºäºç°æå端ä¸å端æ©å±ï¼æ²¿ç¨ç§»å¨ç«¯èç½æ ·å¼ã480Ã800 å¸å±åè´¦å·æéã |
| | | |
| | | ## åè½ä¸å
¥å£ |
| | | |
| | | | å
¥å£ | å°å | åè½ | |
| | | |---|---|---| |
| | | | ç§åè´¨æ£ | `/mobile/quality` | éæ©è¢ç ãæçç æçäº§æ¹æ¬¡ç ï¼æ ¸å¯¹å¾
æ£è¢æ°ï¼å½å
¥åé¡¹ææ åä¸ä¸éï¼éæ©åæ ¼åºï¼ç¡®è®¤è´¨æ£ | |
| | | | è´¨æ£è®°å½ | `/mobile/quality-records` | æä¸å¡ç æè¿½æº¯ç ãæ¹æ¬¡ãç»ææ¥è¯¢ï¼æ¥çæ£éªæ¶é´ååå ï¼å页å è½½ | |
| | | | éå®åºåº | `/mobile/sales` | æ¥è¯¢ç°æåºåºåï¼æ¥çåç§åå·²æ£æ°éï¼æ ¸å¯¹è¢ç ææ´æï¼å®ææ£è´§ãå¡«åè¿åãæ§è¡åºåº | |
| | | |
| | | èåä»å·²æææä¸å¯è§ç PC èåæ å°ï¼ç»å½åéæ°è·åç§»å¨èåå³å¯æ¾ç¤ºãä¸ä¼èªå¨æäºæéãè´¨æ£ä½¿ç¨ `mes:qc-seed-quality:submit/query`ï¼é宿¥è¯¢ãæ£è´§ãè¿åãåºåºåå«ä½¿ç¨ `mes:wm-product-sales:query/stock/shipping/finish`ã |
| | | |
| | | è´¨æ£æ¯æè¢ç ãæçç åçäº§æ¹æ¬¡ç ä¸ç§å
¥å£ï¼æç»éè¢ä¿åè®°å½ä¸æµè½¬åºåãä¸å¡ç å追溯ç éå®åä¸å¯¹è±¡ï¼ç»æç»ä¸åè¢è¿½æº¯ç ï¼é²æ¢åç éå¤è´¨æ£ãä¸åæ ¼å¿
须填ååå 并确认æ¥åºåæäº¤ï¼åæ ¼æµè½¬è³åæ ¼åºãåé¡¹ææ ä¸ä¸é沿ç¨ç°æå½å
¥æ¹å¼ï¼éè¦æä½åæåç§æ£éªæ åæ ¸å¯¹ã |
| | | |
| | | æçãæ¹æ¬¡è¯å«åæ¾ç¤ºææè¢æ°ãå·²æ£è¢æ°ãæ¬æ¬¡å¾
æ£è¢æ°ãä½åºè¢æé¤ï¼å·²æ£è¢ä¸éå¤å¤çï¼å
¨é¨ææè¢ç å
¥åºåæå
许æäº¤ãç»ä¸ææ åç»è®ºåºç¨äºæ¬æ¬¡å
¨é¨å¾
æ£è¢ï¼æä½è
é¡»ç¡®è®¤æ½æ£ç»æéç¨äºè¯¥èå´ãæäº¤æºå¸¦è¯å«æ¶ç¡®è®¤çè¢ ID æ¸
åï¼æå¡ç«¯éå®å¹¶åæ¬¡æ ¸å¯¹ï¼æ¸
åååééæ°æ«æï¼ä»»ä½ä¸è¢å¤±è´¥ï¼æ´æ¬¡è´¨æ£ååºåæä½åæ»ã |
| | | |
| | | ç§»å¨ç«¯æ°å¢ `POST /mes/qc/seed-quality/submit-group`ï¼ä¸åæäº¤æ¥å£ä½¿ç¨ç¸åæéï¼è¯å«æ¥å£å¢å `scanType=BAG|PALLET|BATCH`ãPC åè¢æäº¤æ¥å£ä¿æå
¼å®¹ãæçº¿åæåç¡®è®¤è¢æ¸
åæ ¸å®æ¯å¦å·²æéè¢è´¨æ£è®°å½ï¼ä¸èªå¨éå¤æäº¤ã |
| | | |
| | | åºåºå
éè¿åªè¯»æ¥å£æ ¸å¯¹ç å¼ãåç§ãæ¹æ¬¡åè¢æ°ï¼ç¹å»ç¡®è®¤æçææ£è´§æç»ãPC æ§è¡æ£è´§åç§»å¨ç«¯åæ¯æè¢ç ãæçç ãçäº§æ¹æ¬¡ç ãåè¢æ 1 è¢ï¼æçãæ¹æ¬¡éè¦æç¡®éæ©å®é
è¢ç ï¼å¯è¾å
¥æ´æ°æ°éå¹¶ææ°ééè¢ï¼åéè¢è°æ´ãæ°éä¸¥æ ¼çäºæéè¢ç æ°ï¼ä¸é»è®¤æ´æææ´æ¹å
¨é¨æ£è´§ã |
| | | |
| | | è¯å«æ¾ç¤ºæ¯è¢è¿½æº¯ç ãä¸å¡ç åä¸å¯éåå ï¼æ¯ææ¥æ¾ãå¾éãæ¸
ç©ºåæ ¸å¯¹å
¨é¨å·²éè¢ç ãä½åºè¢æé¤ï¼æªè´¨æ£åæ ¼ãä¸å¨åæ ¼åºåãå»ç»ãå·²åºåºæå·²å å
¥æªåæ¶åºåºåçè¢ç ä¸å¯éãé¢è§ä¸å建åºåæ¹æ¬¡ãæç»æåºåæµæ°´ãæäº¤åæ¬¡æ ¸å¯¹è¢ç å½å±ãç¶æãåºåä½ç½®å计åä½éï¼ææ¯è¢ 1 è¢çææç»ï¼ä¿çè¢ç IDãæç IDãæ¹æ¬¡åæ¥æºç ï¼æ§è¡åºåºéè¢æ´æ°åºååå»åï¼é¨åæç䏿æ´ä¸ªæçæ 记为æ¬ååºåºã |
| | | |
| | | ååç¸åè¢æ¸
åéè¯è¿åå·²æç»æï¼é¨åéå¤è¦æ±éæ°æ ¸å¯¹ï¼è·¨åéå¤åæ§æ´ææç»éå å黿¢ãè¢ç ãåºåè¢æç»ååºåºåå¨äºå¡å
éå®ï¼ä»»ä½ä¸è¢åå
¥å¤±è´¥æ´ä½åæ»ãå·²å
³èè¢ç çæç»ä¸è½æå·¥æ´æ¹æ°éæåºåï¼å¯å¨å PC æ£è´§æµç¨å é¤åéæ°æ«æéæ©ãåºåæç»ä»ç±åæ§è¡åºåºæµç¨æ£åã |
| | | |
| | | è稿维æ¤ãåºåºåå建/åç§ç¼è¾ãå¾
æ£æµå¤çç»§ç»ä½¿ç¨ç°æ PC æµç¨ï¼æ¬æ¬¡ç§»å¨é¡µé¢è¦ç已确认çç°åºè´¨æ£åéå®åºåºæä½ã |
| | | |
| | | ## ç产å
¥åºèªå¨å®æ |
| | | |
| | | ç¨æ·ç¡®è®¤çè§åæ¯ï¼**çäº§æ¹æ¬¡çææè¢ç å
¨é¨å
¥åºåï¼æ¹æ¬¡èªå¨è¿å
¥å·²å®æ**ã |
| | | |
| | | - æ«è¢ç åå该è¢å
¥åºæ è®°ï¼æå®é
è¢ç æ è®°ç»è®¡æ¹æ¬¡è¿åº¦ã |
| | | - æ«æçç åªå¯¹æ¬æå©ä½æªå
¥åºææè¢ç å
¥åºï¼å¹¶åæ¥è¢ç æ è®°ã |
| | | - æ«æ¹æ¬¡ç åªå
¥åºè¯¥æ¹æ¬¡å©ä½ææè¢ç ï¼æ¯ææ¤åå·²ç»éè¢æéæå
¥åºçæ¹æ¬¡ã |
| | | - æ¹æ¬¡ç´¯è®¡æ°éä¸ä¾èµéå¤å 计ï¼å
¨é¨ææè¢ç å
¥åºåç½®ç¶æ 30ãç©ºæ¹æ¬¡ä¸èªå¨å®æï¼å·²ä½åºæ¹æ¬¡ä¸å
许å
¥åºã |
| | | - åè¢å
¨é¨æ«å®æ¶åæ¥å®ææå±æçå
¥åºæ è®°ï¼æ´æ¹å
¥åºæ¶åæ¥åæçãPC å表åç§»å¨è¯¦æ
æ¾ç¤ºè¢ç ãæççå·²å
¥åº/æªå
¥åºç¶æã |
| | | - ä½åºè¢ãé»è¾å é¤è¢æé¤å¨å
¥åºå宿夿ä¹å¤ãæ°æ®åºåå
¥å¨ååºåäºå¡ä¸æ§è¡ï¼å¤±è´¥ä¸ååå®æç¶æï¼äºä»¶å·å¹ç沿ç¨åé»è¾ã |
| | | |
| | | æ¬å°åº `127.0.0.1:3306/mom-thzy` æ£æ¥åç°æ¹æ¬¡ 2 ç 48 个ææè¢ç åå·²å
¥åºï¼ä½ç¶æä»ä¸ºç产ä¸ï¼å·²å¨å®æ´è¢æ°åç¶ææ¡ä»¶æ ¡éªä¸ä¿®æ£ä¸ºå·²å®æãæç 2 åæ¥è¡¥é½å
¥åºæ è®°ï¼äºä»¶å·åå
³èè¢ç æå¤§å
¥åºäºä»¶ 7ãæ¶é´åæåä¸è¢å®é
å
¥åºæ¶é´ãæ¬æ¬¡æ²¡ææ°å¢åºåäºå¡ï¼æ²¡æåæ´è¢ç ãåç§æå®æ´æ§æè¦ã |
| | | |
| | | ## æçº¿å¤ç |
| | | |
| | | è´¨æ£åéå®åå
¥æäº¤åä¿åå½åè´¦å·çå¾
确认æä½ãå·æ°åå
æ ¸å®è´¨æ£ç»ææåºåºåç¶æåæ£è´§æç»ï¼ä¸èªå¨æäº¤ä¸ä¸è¢æèªå¨éè¯æªç¥æä½ãå½åæªåç°ç»ææ¶éè¦æä½è
确认å请æ±å·²ç»æå¹¶éæ°æ ¸å¯¹ï¼æå¡ç«¯ä»æ§è¡é夿 ¡éªãæ²¡ææ°å¢ç¦»çº¿èªå¨åå
¥ã |
| | | |
| | | æ°å¢åªè¯»è¾
婿¥å£ï¼`GET /mes/qc/seed-quality/resolve`ï¼è´¨æ£æäº¤æéï¼ã`GET /mes/wm/product-sales/scan-preview`ï¼æ£è´§æéï¼ãå®ä»¬ä¸å建质æ£è®°å½ãæ£è´§æç»æåºåæµæ°´ãæ£è´§é¢è§å¢å `scanType=BAG|PALLET|BATCH` å `salesId`ï¼å `POST /mes/wm/product-sales/scan` å¢å `scanType`ã`bagIds`ï¼`quantity` å¿
é¡»çäºæéè¢æ°ï¼æç/æ¹æ¬¡ç¼ºå°æç¡®è¢æ¸
åä¼æç»æäº¤ãæçº¿æ¢å¤ææéå
¨é¨è¢ ID æ ¸å®ï¼é¨åå卿¶è¦æ±è´è´£äººæ ¸å¯¹ã |
| | | |
| | | ## éªè¯ä¸å¯ç¨ |
| | | |
| | | å端ç¸å
³åå½å
± 45 项éè¿ï¼å
å«çå® Mapper SQL å¨ H2 MySQL 模å¼ä¸çå®æç¶æéªè¯ãæ··æ«å©ä½è¢æ°ãä½åºæé¤ãåç è´¨æ£é²éå¤åææèåæ å°ã |
| | | |
| | | ä¸ç§è´¨æ£å
¥å£æ©å±å¦éè¿ 11 项å端æµè¯ï¼è¦çè¯å«èå´ãå·²æ£æé¤ãæªå
¨é¨å
¥åºé»æ¢æäº¤ãå¾
æ£æ¸
åååãä½åºå¯¹è±¡åçå® Spring äºå¡æ´ä½åæ»ã |
| | | |
| | | ä¸ç§å
¥å£æµè§å¨æ¨¡ææµè¯éè¿ï¼è¢ç ãæçç ãçäº§æ¹æ¬¡ç è¯å«æ¶ä¸ååºåï¼æäº¤æºå¸¦ç¡®è®¤çå¾
æ£è¢æ¸
åï¼æçº¿åå·æ°æ ¸å®å·²æè®°å½ä¸ä¸éå¤æäº¤ãæ´æãæ´æ¹é¡µé¢æ²¿ç¨ç°ææ ·å¼ï¼480px æªå¾å·²æ£æ¥ï¼ç产æå»ºéè¿ãå
¨é¡¹ç®ç±»åæ£æ¥çæ¢åé误ä»åå¨ï¼æ¬æ¬¡è´¨æ£é¡µé¢ãæ¥å£åç§»å¨å
¥å£æä»¶æ ç±»åé误ã |
| | | |
| | | æçãçäº§æ¹æ¬¡éæ©è¢ç æ£è´§æ©å±éè¿ 28 项å端åç¸å
³è´¨æ£æµè¯ï¼å
æ¬å®é
Mapper SQLãè·¨åéå¤ãæ§æ´æéå ã确认æ°é/å½å±æ ¡éªãæå®é
åºä½éè¢çææç»ãååºå
³èåæ®µå Spring äºå¡æ´ä½åæ»ãç§»å¨æµè§å¨æ¨¡ææµè¯è¦çææ°ééæ©ãéè¢è°æ´ãä¸å¯éè¢ãé¨åæçæ£è´§åç»§ç»æ¹æ¬¡æ£è´§ãæçº¿æ ¸å®å 480/320px å¸å±ãPC æµè§å¨æ¨¡ææµè¯éè¿æ¹æ¬¡è¯å«ãé¨åéè¢ãæ°éä¸è´å确认æäº¤ï¼é¢è§ä¸åå
¥ãæç»ç产æå»ºéè¿ï¼æ£è´§æ¥å£ãç§»å¨é¡µé¢å PC æ£è´§ç¸å
³æä»¶æ æ°å¢ç±»åéè¯¯ãæªåæ¬å°æ°æ®åºåå
¥æµè¯æ£è´§æåºåºæµæ°´ï¼æ¬æ¬¡æ©å±ä¸éè¦æ°æ®åºç»æåæ´ã |
| | | |
| | | å端ç产æå»ºéè¿ãæ°å¢æµè§å¨æ¨¡ææµè¯è¦çè´¨æ£æçº¿æ¢å¤ãè®°å½æ¥è¯¢ãåªè¯»æ«ç æ ¸å¯¹ãæ£è´§æçº¿æ¢å¤ãè¿åæçº¿æ¢å¤ãæ§è¡åºåºãæéé离å 480/320px 宽度ï¼åç§»å¨ç«¯æ«ç å
¥åº/ç»å®/åºå/ç»å½æµè¯éè¿ãæªå¾å¨ `.export_tmp/mobile-quality-sales/`ãæµè§å¨æµè¯ä½¿ç¨æ¨¡ææ¥å£ï¼æ²¡æå®é
åå
¥è´¨æ£æéå®åºåãå
¨é¡¹ç®ç±»åæ£æ¥ä»ææ¢åéè¯¯ï¼æ¬æ¬¡ä¿®æ¹æ¶åçç§»å¨ç«¯ä¸æ¥å£ãè¢ç /æçå表æä»¶æ ç±»åé误ã |
| | | |
| | | çæéè¦æ´æ°å¹¶éå¯åç«¯ãæ´æ°å端ï¼ç¶åéæ°ç»å½ç§»å¨ç«¯è·åèåãå½åé»è®¤ç产æå»ºæ²¿ç¨é¡¹ç®å·²æ TEST å
¬å¸é
ç½®ï¼å
¶ API æå `http://1.95.78.124:9008/admin-api`ï¼æ¬å° 5666 ç¯å¢é¨ç½²åºä½¿ç¨è¯¥ç¯å¢åææå»º/API é
ç½®ãæªèªå¨æ¿æ¢å¨çº¿é¡µé¢æé坿£å¨è¿è¡çåç«¯ãæ¬æ¬¡ä»£ç ä¸éè¦æ°å¢æ°æ®åºå段ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const { chromium } = require('playwright'); |
| | | const fs = require('node:fs'); |
| | | const path = require('node:path'); |
| | | const assert = require('node:assert/strict'); |
| | | |
| | | (async () => { |
| | | const root = path.resolve('mom-pro2-before/dist'); |
| | | const script = fs.readFileSync(path.join(root, 'startup-check.js'), 'utf8'); |
| | | const diagnostic = fs.readFileSync(path.join(root, 'browser-check.html'), 'utf8'); |
| | | assert.ok(fs.readFileSync(path.join(root, 'index.html'), 'utf8').includes('data-startup-check')); |
| | | const browser = await chromium.launch({ headless: true, channel: 'msedge' }); |
| | | try { |
| | | const page = await browser.newPage({ viewport: { width: 480, height: 800 } }); |
| | | page.on('pageerror', error => console.error('Browser error:', error.message)); |
| | | await page.route('http://startup.test/**', route => { |
| | | const pathname = new URL(route.request().url()).pathname; |
| | | if (pathname.endsWith('startup-check.js')) return route.fulfill({ contentType: 'application/javascript', body: script }); |
| | | if (pathname.endsWith('browser-check.html')) return route.fulfill({ contentType: 'text/html', body: diagnostic }); |
| | | return route.fulfill({ contentType: 'text/html', body: '<div id="__app-loading__">å¯å¨ä¸</div><script data-startup-check src="/nested/startup-check.js"></script>' }); |
| | | }); |
| | | await page.clock.install(); |
| | | await page.goto('http://startup.test/nested/'); |
| | | await page.evaluate(() => window.dispatchEvent(new ErrorEvent('error', { message: "Unexpected token '?'" }))); |
| | | await page.clock.fastForward(31000); |
| | | assert.match(await page.locator('#mom-startup-error').innerText(), /Unexpected token/); |
| | | assert.equal(await page.locator('#mom-startup-error a').getAttribute('href'), 'http://startup.test/nested/browser-check.html'); |
| | | await page.evaluate(() => window.__MOM_STARTUP__.finish()); |
| | | assert.equal(await page.locator('#mom-startup-error').count(), 0); |
| | | await page.goto('http://startup.test/nested/'); |
| | | await page.evaluate(() => window.__MOM_STARTUP__.finish()); |
| | | await page.clock.fastForward(31000); |
| | | assert.equal(await page.locator('#mom-startup-error').count(), 0); |
| | | await page.goto('http://startup.test/nested/'); |
| | | await page.evaluate(() => window.__MOM_STARTUP__.fail(new Error('<script>bad</script>'))); |
| | | assert.match(await page.locator('#mom-startup-error').innerText(), /<script>bad<\/script>/); |
| | | assert.equal(await page.locator('#mom-startup-error script').count(), 0); |
| | | await page.locator('#mom-startup-error a').click(); |
| | | assert.equal(await page.locator('#checks tr').count(), 10); |
| | | assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)); |
| | | console.log('PASS: diagnostic page, startup failure/timeout, success cleanup, safe text, nested base URL'); |
| | | } finally { await browser.close(); } |
| | | })().catch(error => { console.error(error); process.exitCode = 1; }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const { chromium } = require('playwright'); |
| | | const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); |
| | | const base = process.env.MOBILE_TEST_URL || 'http://127.0.0.1:5173'; |
| | | const out = path.resolve('.export_tmp/code-toolbar'); fs.mkdirSync(out, { recursive: true }); |
| | | (async () => { |
| | | const browser = await chromium.launch({ headless: true, channel: 'msedge' }); |
| | | try { |
| | | const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); |
| | | const page = await context.newPage(), errors = [], calls = []; |
| | | page.on('pageerror', error => errors.push(error.message)); |
| | | const permissions = ['bag-code','pallet','batch'].flatMap(kind => ['query','create','update','replace','delete','export'].map(action => `mes:pro-${kind}:${action}`)); |
| | | await context.route('**/*', async route => { |
| | | const url = new URL(route.request().url()), p = url.pathname; |
| | | if (url.origin !== base) return route.abort(); |
| | | if (!p.startsWith('/admin-api/')) return route.continue(); |
| | | calls.push(p); |
| | | let data = null; |
| | | if (p.endsWith('/auth/login')) data = { accessToken:'fixture-token', refreshToken:'fixture-refresh', userId:7, expiresTime:Date.now()+3600000 }; |
| | | else if (p.endsWith('/get-permission-info')) data = { user:{ id:7, nickname:'æµè¯å', username:'fixture' }, roles:['worker'], permissions, |
| | | menus:[{ id:1, parentId:0, name:'ä¸è¢ä¸ç 管ç', path:'/bagcode', component:'Layout', visible:true, children: |
| | | [['bag-code','bagCode','èµç 管ç'],['pallet','pallet','æçç 管ç'],['batch','batch','æ¹æ¬¡ç产管ç']].map(([key,folder,name],i)=>({id:i+2,parentId:1,path:key,name,component:`mes/pro/${folder}/index`,visible:true})) }] }; |
| | | else if (p.endsWith('/bag-code/page')) data = { list:[{id:11,code:'6028472024079516',businessCode:'8888888888888888',itemName:'çç±³ç§å',status:10}], total:1 }; |
| | | else if (p.endsWith('/pallet/page')) data = { list:[{id:12,code:'P-TEST',businessCode:'7777777777777777',bagCount:1,status:10}], total:1 }; |
| | | else if (p.endsWith('/batch/page')) data = { list:[{id:20,code:'B-TEST',businessCode:'6666666666666666',status:20}], total:1 }; |
| | | else if (p.endsWith('/pallet/unbound-list')) data = [ |
| | | {id:1,tempCode:'P-ONE',itemId:1,itemName:'塿²³4å·',itemCode:'ITEM-1',bagCount:25}, |
| | | {id:2,tempCode:'P-TWO',itemId:1,itemName:'塿²³4å·',itemCode:'ITEM-1',bagCount:25}, |
| | | {id:3,tempCode:'P-OTHER',itemId:2,itemName:'å
¶ä»åç§',itemCode:'ITEM-2',bagCount:25}, |
| | | {id:4,tempCode:'P-EMPTY',bagCount:0}, |
| | | ]; |
| | | else if (p.includes('export')) return route.fulfill({contentType:'application/zip',body:'fixture',headers:{'content-disposition':'attachment;filename=fixture.zip'}}); |
| | | else if (p.endsWith('/generate-business-code')) data = 1; |
| | | else if (p.endsWith('/page')) data = {list:[],total:0}; |
| | | else if (p.endsWith('/simple-list') || p.endsWith('/list-all-simple') || p.endsWith('/get-unread-count')) data = []; |
| | | return route.fulfill({json:{code:0,msg:'',data}}); |
| | | }); |
| | | await page.goto(base+'/auth/login'); |
| | | await page.locator('input[autocomplete=username]').fill('fixture'); |
| | | await page.locator('input[autocomplete=current-password]').fill('fixture-password'); |
| | | await page.getByRole('button',{name:'ç»å½ PC 端',exact:true}).click(); |
| | | await page.waitForURL(url => !url.pathname.startsWith('/auth/')); |
| | | for (const [key,title,code] of [['bag-code','è¢ç å表ï¼ä¸è¢ä¸ç ï¼','8888888888888888'],['pallet','æçç å表','7777777777777777'],['batch','çäº§æ¹æ¬¡å表','6666666666666666']]) { |
| | | await page.goto(base+'/bagcode/'+key); |
| | | await page.getByText(title,{exact:true}).waitFor(); |
| | | await page.getByText(code,{exact:true}).first().waitFor(); |
| | | const toolbar = page.locator('.mes-code-toolbar'); |
| | | for (const width of [1920,1280]) { |
| | | await page.setViewportSize({width,height:1080}); |
| | | const buttons = await toolbar.locator('button').evaluateAll(nodes => nodes.map(node => ({text:node.textContent.trim(),y:node.getBoundingClientRect().top,x:node.getBoundingClientRect().right}))); |
| | | assert(buttons.length <= 6, 'Too many visible toolbar operations'); |
| | | assert(Math.max(...buttons.map(b=>b.y))-Math.min(...buttons.map(b=>b.y)) < 4, JSON.stringify(buttons)); |
| | | assert(buttons.every(b=>b.x <= width), 'Toolbar exceeds screen'); |
| | | const heading = await page.getByText(title,{exact:true}).boundingBox(); |
| | | assert(heading.height < 30, 'Title wraps'); |
| | | await page.screenshot({path:path.join(out,`${key}-${width}.png`),fullPage:true}); |
| | | } |
| | | await toolbar.getByRole('button',{name:'导åº',exact:true}).click(); |
| | | await page.getByRole('menuitem',{name:key==='bag-code'?'ä¸å¡äºç»´ç ':key==='pallet'?'ä¸å¡äºç»´ç ï¼éä¸è®°å½ï¼':'Excel æ°æ®',exact:true}).waitFor(); |
| | | await page.getByText(title,{exact:true}).click(); |
| | | const qr = page.getByRole('button',{name:'ä¸å¡äºç»´ç ',exact:true}); |
| | | if (key !== 'batch') { |
| | | await qr.click(); |
| | | await page.getByRole('dialog',{name:'ä¸å¡ç äºç»´ç '}).waitFor(); |
| | | assert(await page.getByRole('dialog').getByText(code,{exact:true}).isVisible()); |
| | | await page.screenshot({path:path.join(out,`${key}-business-qr.png`)}); |
| | | await page.keyboard.press('Escape'); |
| | | } |
| | | } |
| | | await page.getByRole('button',{name:'æ°å¢æ¹æ¬¡',exact:true}).click(); |
| | | const dialog=page.getByRole('dialog');await dialog.waitFor(); |
| | | const select=dialog.locator('.ant-select').last(); |
| | | await select.click(); |
| | | const first=page.locator('.ant-select-item-option').filter({hasText:'P-ONE'}); |
| | | await first.click(); |
| | | const itemFields=dialog.getByPlaceholder('éæ©æçåèªå¨å¸¦å
¥'); |
| | | await page.waitForFunction(()=>[...document.querySelectorAll('input')].some(input=>input.value==='塿²³4å·')); |
| | | assert.equal(await itemFields.nth(0).inputValue(),'塿²³4å·'); |
| | | assert.equal(await itemFields.nth(1).inputValue(),'ITEM-1'); |
| | | const other=page.locator('.ant-select-item-option').filter({hasText:'P-OTHER'}); |
| | | assert((await other.getAttribute('class')).includes('ant-select-item-option-disabled')); |
| | | const empty=page.locator('.ant-select-item-option').filter({hasText:'P-EMPTY'}); |
| | | assert((await empty.getAttribute('class')).includes('ant-select-item-option-disabled')); |
| | | await page.locator('.ant-select-item-option').filter({hasText:'P-TWO'}).click(); |
| | | await dialog.click({position:{x:10,y:10}}); |
| | | await page.screenshot({path:path.join(out,'batch-variety-binding.png'),fullPage:true}); |
| | | await page.keyboard.press('Escape'); |
| | | if(await dialog.isVisible()) await dialog.getByRole('button',{name:/å\s*æ¶/}).click(); |
| | | assert.deepEqual(errors,[]); |
| | | console.log('PASS: three code pages, grouped toolbar at 1920/1280, export dropdown, business QR preview'); |
| | | } finally { await browser.close(); } |
| | | })().catch(error=>{console.error(error);process.exitCode=1;}); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const { chromium } = require('playwright'); |
| | | const fs = require('fs'), path = require('path'), assert = require('assert/strict'); |
| | | const base = process.env.MOBILE_TEST_URL || 'http://127.0.0.1:5174'; |
| | | const out = path.resolve('.export_tmp/mobile-quality-sales'); fs.mkdirSync(out, { recursive: true }); |
| | | (async () => { |
| | | const browser = await chromium.launch({ headless: true, channel: 'msedge' }); |
| | | const context = await browser.newContext({ viewport: { width: 480, height: 800 } }); |
| | | const page = await context.newPage(), errors = [], writes = [], picked = []; |
| | | let quality = null, salesStatus = 2, firstScan = true, readOnly = false; |
| | | const permissions = ['mes:qc-seed-quality:submit', 'mes:qc-seed-quality:query', 'mes:wm-product-sales:query', 'mes:wm-product-sales:stock', 'mes:wm-product-sales:shipping', 'mes:wm-product-sales:finish']; |
| | | page.on('pageerror', e => errors.push(e.message)); |
| | | page.on('dialog', d => d.accept()); |
| | | await context.route('**/*', async route => { |
| | | const req = route.request(), u = new URL(req.url()), p = u.pathname; |
| | | if (u.origin !== base) return route.abort(); |
| | | if (!p.startsWith('/admin-api/')) return route.continue(); |
| | | let data = null; |
| | | if (p.endsWith('/auth/login')) data = { accessToken: 'test-token', refreshToken: 'test-refresh', userId: 7, expiresTime: Date.now() + 3600000 }; |
| | | else if (p.endsWith('/get-permission-info')) data = { user: { id: 7, nickname: 'è´¨æ£ä¸ä»åºæµè¯å', username: 'fixture' }, roles: ['worker'], permissions: readOnly ? ['mes:wm-product-sales:query'] : permissions, menus: ['quality', 'quality-records', 'sales'].map((key, i) => ({ id: i + 1, path: '/mobile/' + key, name: key, visible: true, component: 'mobile/' + key })) }; |
| | | else if (p.endsWith('/warehouse/simple-list')) data = [{ id: 8, name: 'ç§ååæ ¼åº', type: 20 }, { id: 9, name: 'å»ç»åæ ¼åº', type: 20, frozen: true }, { id: 10, name: 'æååº', type: 10 }]; |
| | | else if (p.endsWith('/seed-quality/resolve')) { |
| | | const type = u.searchParams.get('scanType') || 'BAG'; |
| | | data = { code: type === 'BAG' ? 'TRACE-1' : type + '-TEST', businessCode: '8888888888888888', batchCode: 'BATCH-1', scanType: type, inbound: true, canceled: false, qualityId: quality?.id, result: quality?.result, totalCount: type === 'BAG' ? 1 : 3, pendingBagIds: quality ? [] : type === 'BAG' ? [1] : [2,3], completedBagIds: quality ? type === 'BAG' ? [1] : [1,2,3] : type === 'BAG' ? [] : [1] }; |
| | | } |
| | | else if (p.endsWith('/seed-quality/submit-group')) { |
| | | const body = req.postDataJSON(); writes.push({ quality: body }); |
| | | quality = { id: 101, code: body.code, batchCode: 'BATCH-1', result: 1, inspectedAt: '2026-10-10 12:00:00' }; |
| | | return route.abort('timedout'); |
| | | } else if (p.endsWith('/seed-quality/page')) data = { list: quality ? [quality] : [], total: quality ? 1 : 0 }; |
| | | else if (p.endsWith('/product-sales/page')) data = { list: [{ id: 11, code: 'SALE-1', clientName: 'æµè¯ç»éå', status: salesStatus }], total: 1 }; |
| | | else if (p.endsWith('/product-sales/get')) data = { id: 11, code: 'SALE-1', clientName: 'æµè¯ç»éå', contactName: 'ç°åºæ¶è´§å', contactAddress: 'åé叿µè¯ä»åº', status: salesStatus, carrier: salesStatus === 3 || salesStatus === 4 ? '顺丰' : '', shippingNumber: salesStatus === 3 || salesStatus === 4 ? 'SF12345' : '' }; |
| | | else if (p.endsWith('/product-sales-line/page')) data = { list: [{ id: 21, itemId: 1, itemName: '塿²³4å·', quantity: 2, unitMeasureName: 'è¢' }], total: 1 }; |
| | | else if (p.endsWith('/product-sales-detail/list-by-line')) data = picked; |
| | | else if (p.endsWith('/product-sales/scan-preview')) { |
| | | const type = u.searchParams.get('scanType') || 'BAG', bagId = u.searchParams.get('code') === 'BAG-2' ? 32 : 31; |
| | | const options = (type === 'BAG' ? [bagId] : [31,32,33]).map(id => ({ id, code: 'TRACE-' + id, businessCode: 'BUS-' + id, selectable: id !== 33 && !picked.some(d => d.bagCodeId === id), unavailableReason: id === 33 ? 'å°æªè´¨æ£åæ ¼' : picked.some(d => d.bagCodeId === id) ? 'å·²å å
¥åºåºåæ£è´§' : undefined })); |
| | | data = { scanType: type, codeId: type === 'BAG' ? bagId : type === 'PALLET' ? 200 : 100, scanContent: u.searchParams.get('code'), itemId: 1, batchCode: 'BATCH-1', scanQuantity: options.filter(b => b.selectable).length, bags: options }; |
| | | } |
| | | else if (p.endsWith('/product-sales/scan')) { |
| | | const body = req.postDataJSON(); writes.push({ scan: body }); |
| | | assert.equal(body.quantity, body.bagIds.length); |
| | | for (const bagId of body.bagIds) picked.push({ id: 50 + picked.length, lineId: 21, bagCodeId: bagId, quantity: 1, batchCode: 'BATCH-1', warehouseName: 'ç§ååæ ¼åº', locationName: 'ä¸åº', areaName: 'A01', remark: 'è¢ç TRACE-' + bagId }); |
| | | if (firstScan) { firstScan = false; return route.abort('timedout'); } |
| | | data = { outboundQuantity: 1, alreadyScanned: false }; |
| | | } else if (p.endsWith('/product-sales/check-quantity')) data = picked.length === 2; |
| | | else if (p.endsWith('/product-sales/stock')) { writes.push({ stock: true }); salesStatus = 10; data = true; } |
| | | else if (p.endsWith('/product-sales/shipping')) { writes.push({ shipping: req.postDataJSON() }); salesStatus = 3; return route.abort('timedout'); } |
| | | else if (p.endsWith('/product-sales/finish')) { writes.push({ finish: true }); salesStatus = 4; data = true; } |
| | | else if (p.endsWith('/simple-list') || p.endsWith('/get-unread-count')) data = []; |
| | | return route.fulfill({ json: { code: 0, data, msg: '' } }); |
| | | }); |
| | | async function scan(value) { await page.getByRole('textbox', { name: 'ç å¼ / 溯æºé¾æ¥' }).fill(value); await page.getByRole('button', { name: 'è¯å«ç å¼', exact: true }).click(); } |
| | | async function noOverflow() { assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Page must fit mobile width'); } |
| | | async function capture(name) { await page.locator('#__app-loading__').waitFor({ state: 'detached' }); await page.evaluate(() => document.activeElement?.blur()); await page.waitForFunction(() => { window.scrollTo({ top: 0, behavior: 'instant' }); return window.scrollY <= 1; }); await page.screenshot({ path: path.join(out, name), fullPage: true }); } |
| | | try { |
| | | await page.goto(base + '/auth/login'); |
| | | await page.getByRole('radio', { name: 'ç§»å¨ç«¯', exact: true }).check(); |
| | | await page.locator('input[autocomplete=username]').fill('fixture'); await page.locator('input[autocomplete=current-password]').fill('fixture'); |
| | | await page.getByRole('button', { name: 'ç»å½ç§»å¨ç«¯', exact: true }).click(); await page.waitForURL('**/mobile/home'); |
| | | await page.locator('#__app-loading__').waitFor({ state: 'detached' }); |
| | | await page.getByRole('link', { name: 'ç§åè´¨æ£ è¢ç ãæç䏿¹æ¬¡' }).waitFor(); |
| | | await capture('home-480.png'); |
| | | await page.goto(base + '/mobile/quality'); await scan('8888888888888888'); |
| | | await page.getByRole('heading', { name: 'æ¬æ¬¡æ£éªå¯¹è±¡' }).waitFor(); assert.equal(writes.length, 0); |
| | | for (const field of await page.getByRole('spinbutton', { name: '宿µå¼', exact: true }).all()) await field.fill('90'); |
| | | await page.getByRole('spinbutton', { name: 'åæ ¼ä¸é', exact: true }).first().fill('95'); |
| | | assert(await page.getByRole('textbox', { name: 'ä¸åæ ¼åå ï¼å¿
å¡«ï¼' }).evaluate(el => el.required)); |
| | | await page.getByRole('spinbutton', { name: 'åæ ¼ä¸é', exact: true }).first().fill('0'); |
| | | await page.getByRole('button', { name: 'ç®æ åæ ¼åº è¯·éæ©', exact: true }).click(); |
| | | assert.equal(await page.getByRole('button', { name: 'å»ç»åæ ¼åº', exact: true }).count(), 0); |
| | | await page.getByRole('button', { name: 'ç§ååæ ¼åº', exact: true }).click(); |
| | | await noOverflow(); await capture('quality-480.png'); |
| | | await page.getByRole('button', { name: '确认æäº¤è´¨æ£', exact: true }).click(); |
| | | await page.getByRole('heading', { name: 'è´¨æ£ç»æå¾
确认' }).waitFor(); assert.equal(writes.length, 1); assert.equal(writes[0].quality.code, 'TRACE-1'); |
| | | await page.reload(); await page.getByRole('button', { name: 'æ ¸å®è´¨æ£ç»æ' }).click(); |
| | | await page.getByText(/å·²æ ¸å®ï¼æ¬æ¬¡è¢ç åå·²æè´¨æ£è®°å½/).waitFor(); assert.equal(writes.length, 1, 'Verification must not resubmit quality'); |
| | | await page.goto(base + '/mobile/quality-records'); await page.getByText('åæ ¼', { exact: true }).last().waitFor(); |
| | | await capture('quality-records-480.png'); |
| | | for (const type of ['PALLET', 'BATCH']) { |
| | | quality = null; |
| | | await page.goto(base + '/mobile/quality'); |
| | | await page.getByRole('combobox', { name: 'è´¨æ£æ«ç ç±»å' }).selectOption(type); |
| | | await scan(type + '-TEST'); await page.getByRole('heading', { name: 'æ¬æ¬¡æ£éªå¯¹è±¡' }).waitFor(); |
| | | await page.getByText('2 è¢', { exact: true }).waitFor(); |
| | | const previousWrites = writes.length; |
| | | for (const field of await page.getByRole('spinbutton', { name: '宿µå¼', exact: true }).all()) await field.fill('90'); |
| | | await page.getByRole('button', { name: 'ç®æ åæ ¼åº è¯·éæ©', exact: true }).click(); |
| | | await page.getByRole('button', { name: 'ç§ååæ ¼åº', exact: true }).click(); |
| | | await noOverflow(); await capture('quality-' + type.toLowerCase() + '-480.png'); |
| | | assert.equal(writes.length, previousWrites, 'Group recognition must not write inventory'); |
| | | await page.getByRole('button', { name: '确认æäº¤è´¨æ£' }).click(); |
| | | await page.getByRole('heading', { name: 'è´¨æ£ç»æå¾
确认' }).waitFor(); |
| | | assert.equal(writes.at(-1).quality.scanType, type); assert.deepEqual(writes.at(-1).quality.bagIds, [2,3]); |
| | | await page.reload(); await page.getByRole('button', { name: 'æ ¸å®è´¨æ£ç»æ' }).click(); |
| | | await page.getByText(/å·²æ ¸å®ï¼æ¬æ¬¡è¢ç åå·²æè´¨æ£è®°å½/).waitFor(); |
| | | assert.equal(writes.length, previousWrites + 1, 'Group recovery must not resubmit'); |
| | | } |
| | | await page.goto(base + '/mobile/sales'); await page.getByRole('button', { name: 'SALE-1 å¾
æ£è´§ æµè¯ç»éå' }).click(); |
| | | await scan('BAG-1'); await page.getByRole('heading', { name: 'æ ¸å¯¹æ£è´§å¯¹è±¡' }).waitFor(); assert.equal(writes.length, 3); |
| | | await page.getByRole('button', { name: 'åæ¶æ¬æ¬¡æ«ç ' }).click(); |
| | | await page.getByRole('combobox', { name: 'æ£è´§æ«ç ç±»å' }).selectOption('PALLET');await scan('PALLET-200'); |
| | | assert(await page.getByRole('button', { name: '确认å å
¥æ£è´§' }).isDisabled()); |
| | | assert(await page.getByRole('checkbox', { name: 'éæ©è¢ç TRACE-33', exact: true }).isDisabled()); |
| | | await page.getByRole('spinbutton', { name: 'æ¬æ¬¡æ£è´§æ°é' }).fill('1');await page.getByRole('button', { name: 'ææ°ééæ©è¢ç ' }).click(); |
| | | await page.getByRole('checkbox', { name: 'éæ©è¢ç TRACE-31', exact: true }).uncheck();await page.getByRole('checkbox', { name: 'éæ©è¢ç TRACE-32', exact: true }).check(); |
| | | await noOverflow(); await capture('sales-picking-480.png'); |
| | | await page.getByRole('button', { name: '确认å å
¥æ£è´§' }).click(); await page.getByRole('heading', { name: 'åºåºæä½ç»æå¾
确认' }).waitFor(); |
| | | await page.reload(); await page.getByRole('button', { name: 'æ ¸å®æä½ç»æ' }).click(); await page.getByText('å·²æ ¸å®ï¼è¯¥ç 已卿£è´§æ¸
åä¸ã').waitFor(); |
| | | assert.equal(writes.length, 4);assert.deepEqual(writes.at(-1).scan.bagIds, [32]);assert.equal(writes.at(-1).scan.scanType, 'PALLET'); |
| | | await page.getByRole('combobox', { name: 'æ£è´§æ«ç ç±»å' }).selectOption('BATCH');await scan('BATCH-100'); |
| | | assert(await page.getByRole('checkbox', { name: 'éæ©è¢ç TRACE-32', exact: true }).isDisabled()); |
| | | await page.getByRole('spinbutton', { name: 'æ¬æ¬¡æ£è´§æ°é' }).fill('1');await page.getByRole('button', { name: 'ææ°ééæ©è¢ç ' }).click(); |
| | | await capture('sales-batch-selection-480.png');await page.setViewportSize({width:320,height:640});await noOverflow();await capture('sales-batch-selection-320.png');await page.setViewportSize({width:480,height:800}); |
| | | await page.getByRole('button', { name: '确认å å
¥æ£è´§' }).click(); |
| | | await page.getByText('å·²å å
¥æ£è´§ï¼1 è¢ã').waitFor();assert.deepEqual(writes.at(-1).scan.bagIds,[31]);assert.equal(writes.at(-1).scan.scanType,'BATCH'); await page.getByRole('button', { name: '宿æ£è´§å¹¶æ ¸å¯¹æ°é' }).click(); |
| | | await page.getByRole('heading', { name: 'å¡«åè¿å', exact: true }).waitFor(); |
| | | await page.getByRole('textbox', { name: 'æ¿è¿å', exact: true }).fill('顺丰'); await page.getByRole('textbox', { name: 'è¿è¾åå·', exact: true }).fill('SF12345'); |
| | | await capture('sales-shipping-480.png'); |
| | | await page.getByRole('button', { name: 'ä¿åè¿å' }).click(); await page.getByRole('heading', { name: 'åºåºæä½ç»æå¾
确认' }).waitFor(); |
| | | await page.getByRole('button', { name: 'æ ¸å®æä½ç»æ' }).click(); await page.getByRole('button', { name: '确认æ§è¡åºåº' }).click(); |
| | | await page.getByText('该å已宿åºåºã').waitFor(); assert.equal(writes.length, 8); await noOverflow(); |
| | | await capture('sales-finished-480.png'); |
| | | await page.setViewportSize({ width: 320, height: 640 }); await noOverflow(); |
| | | await capture('sales-finished-320.png'); |
| | | readOnly = true; await page.reload(); await page.goto(base + '/mobile/quality'); await page.waitForURL('**/mobile/home**'); |
| | | assert.equal(await page.getByRole('link', { name: /ç§åè´¨æ£/ }).count(), 0); |
| | | assert.deepEqual(errors, []); console.log('PASS: bag/pallet/batch quality submit/timeout recovery, records, sales preview/picking/shipping/outbound, 480/320 widths and permissions'); |
| | | } finally { await browser.close(); } |
| | | })().catch(e => { console.error(e); process.exitCode = 1; }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const { chromium } = require('playwright'); |
| | | const fs=require('fs'), path=require('path'), assert=require('assert/strict'); |
| | | const base=process.env.MOBILE_TEST_URL||'http://127.0.0.1:5173'; |
| | | const out=path.resolve('.export_tmp/mobile-ui');fs.mkdirSync(out,{recursive:true}); |
| | | (async()=>{ |
| | | const browser=await chromium.launch({headless:true,channel:'msedge'}); |
| | | const context=await browser.newContext({viewport:{width:480,height:800}}); |
| | | await context.addInitScript(() => { |
| | | if (navigator.mediaDevices) Object.defineProperty(navigator.mediaDevices, 'getUserMedia', { |
| | | value: () => Promise.reject(new DOMException('æµè¯è®¾å¤æªé
ç½®ç¸æº', 'NotFoundError')), |
| | | }); |
| | | }); |
| | | const page=await context.newPage(),errors=[],calls=[],submissions=[]; |
| | | page.on('pageerror',e=>errors.push(e.message)); |
| | | let restricted=false, firstInbound=true,permissionFails=false,failStockMore=true; |
| | | const permissions=['mes:pro-scan-in:inbound','mes:pro-scan-in:query','mes:pro-bag-code:query','mes:pro-bag-code:update','mes:pro-pallet:query','mes:pro-pallet:update','mes:pro-batch:query','mes:wm-material-stock:query']; |
| | | const event={id:31,eventId:'fixture',bagCode:'6028472024079516',status:'SUCCESS',scanType:'BAG',quantity:1,batchCode:'PB-TEST',receivedTime:'2026-10-10 08:30:00'}; |
| | | await context.route('**/*',async r=>{ |
| | | const u=new URL(r.request().url()),p=u.pathname; |
| | | if(u.origin!==base){return r.abort();} |
| | | if(!p.startsWith('/admin-api/'))return r.continue(); |
| | | calls.push(p+u.search); |
| | | let data=null; |
| | | if(p.endsWith('/auth/login'))data={accessToken:'fixture-token',refreshToken:'fixture-refresh',userId:7,expiresTime:Date.now()+3600000}; |
| | | else if(p.endsWith('/get-permission-info')){ |
| | | if(permissionFails)return r.fulfill({json:{code:500,msg:'æµè¯ï¼èåææ¶ä¸å¯ç¨'}}); |
| | | const mobile=u.searchParams.get('terminalType')==='mobile'; |
| | | data={user:{id:7,nickname:'ç°åºæµè¯å',username:'fixture'},roles:['worker'],permissions:restricted?[]:permissions,menus:mobile?['inbound','bags','pallets','batches','stock'].map((key,i)=>({id:i+1,path:'/mobile/'+key,name:key,visible:true,component:'mobile/'+key})):[]}; |
| | | }else if(p.endsWith('/warehouse/simple-list'))data=[{id:1,code:'WIP_VIRTUAL_WAREHOUSE',name:'ç产æåä»'}]; |
| | | else if(p.endsWith('/warehouse-location/simple-list'))data=[{id:2,name:'ç产ä¸åº',frozen:false}]; |
| | | else if(p.endsWith('/warehouse-area/simple-list'))data=[{id:3,name:'A01',status:0,frozen:false},{id:4,name:'åç¨åºä½',status:1,frozen:false}]; |
| | | else if(p.endsWith('/resolve-type'))data=u.searchParams.get('code').startsWith('P')?'pallet':'bag'; |
| | | else if(p.endsWith('/bag-code/bind-pallet')){submissions.push({binding:r.request().postDataJSON()});data=true;} |
| | | else if(p.endsWith('/bag-code/get-by-code'))data={id:11,code:u.searchParams.get('code')==='8888888888888888'?'6028472024079516':u.searchParams.get('code'),businessCode:'8888888888888888',itemName:'çç±³ç§å',batchId:20,batchCode:'PB-TEST',produceDate:'2026-10-09',lineName:'ä¸å·äº§çº¿'}; |
| | | else if(p.endsWith('/pallet/get-by-code'))data={id:12,code:u.searchParams.get('code'),batchId:20,batchCode:'PB-TEST',itemName:'çç±³ç§å'}; |
| | | else if(p.endsWith('/scan-in/my-event'))data=null; |
| | | else if(p.endsWith('/scan-in/inbound')){ |
| | | const body=r.request().postDataJSON();submissions.push(body); |
| | | if(firstInbound){firstInbound=false;return r.abort('timedout');} |
| | | data={...event,eventId:body.eventId}; |
| | | } |
| | | else if(p.endsWith('/scan-in/my-page'))data={list:[event],total:1}; |
| | | else if(p.endsWith('/material-stock/page')){ |
| | | const more=u.searchParams.get('pageNo')==='2'; |
| | | if(more&&failStockMore){failStockMore=false;return r.abort('timedout');} |
| | | data={list:[{id:more?2:1,itemName:more?'ç¬¬äºæ¡åºå':'çç±³ç§å',batchCode:'PB-TEST',quantity:25,availableQuantity:22,unitMeasureName:'è¢',warehouseName:'ç产æåä»',locationName:'ç产ä¸åº',areaName:'A01'}],total:2}; |
| | | } |
| | | else if(p.endsWith('/page'))data={list:[],total:0}; |
| | | else if(p.endsWith('/simple-list')||p.endsWith('/list-all-simple')||p.endsWith('/get-unread-count'))data=[]; |
| | | return r.fulfill({json:{code:0,msg:'',data}}); |
| | | }); |
| | | await page.goto(base+'/auth/login');await page.getByRole('radio',{name:'ç§»å¨ç«¯',exact:true}).waitFor(); |
| | | assert(await page.getByRole('radio',{name:'ç§»å¨ç«¯',exact:true}).isChecked()); |
| | | await page.getByRole('radio',{name:'PC 端',exact:true}).check(); |
| | | await page.setViewportSize({width:390,height:800}); |
| | | assert(await page.getByRole('radio',{name:'PC 端',exact:true}).isChecked()); |
| | | await page.getByRole('radio',{name:'ç§»å¨ç«¯',exact:true}).check();await page.setViewportSize({width:480,height:800}); |
| | | await page.locator('input[autocomplete=username]').fill('fixture'); |
| | | await page.locator('input[autocomplete=current-password]').fill('fixture-password'); |
| | | await page.screenshot({path:path.join(out,'login-480.png'),fullPage:true}); |
| | | await page.getByRole('button',{name:'ç»å½ç§»å¨ç«¯',exact:true}).click(); |
| | | await page.waitForURL('**/mobile/home'); |
| | | await page.getByRole('heading',{name:'ä»ä¸æ¬¡æ«ç å¼å§'}).waitFor(); |
| | | await page.screenshot({path:path.join(out,'home-480.png'),fullPage:true}); |
| | | assert(calls.some(c=>c.includes('terminalType=mobile'))); |
| | | await page.goto(base+'/mobile/inbound'); |
| | | await page.getByRole('button',{name:'åºåº è¯·éæ©åºåº',exact:true}).click(); |
| | | await page.getByRole('textbox',{name:'æç´¢åºåº'}).fill('ç产'); |
| | | for(let i=0;i<4;i++){await page.keyboard.press('Tab');assert(await page.getByRole('dialog',{name:'éæ©åºåº'}).evaluate(el=>el.contains(document.activeElement)));} |
| | | await page.keyboard.press('Escape'); |
| | | await page.getByRole('dialog',{name:'éæ©åºåº'}).waitFor({state:'hidden'}); |
| | | assert.equal(await page.evaluate(()=>document.body.style.overflow),''); |
| | | await page.getByRole('button',{name:'åºåº è¯·éæ©åºåº',exact:true}).click(); |
| | | await page.getByRole('textbox',{name:'æç´¢åºåº'}).fill('ç产'); |
| | | await page.getByRole('button',{name:'ç产ä¸åº',exact:true}).click(); |
| | | await page.getByRole('button',{name:'åºä½ è¯·éæ©åºä½',exact:true}).click(); |
| | | assert.equal(await page.getByRole('button',{name:'åç¨åºä½',exact:true}).count(),0); |
| | | await page.getByRole('textbox',{name:'æç´¢åºä½'}).fill('ä¸åå¨'); |
| | | await page.getByText('没æå¹é
çåºä½').waitFor(); |
| | | await page.getByRole('textbox',{name:'æç´¢åºä½'}).fill('A01'); |
| | | await page.locator('#__app-loading__').waitFor({state:'detached'}); |
| | | await page.screenshot({path:path.join(out,'location-picker-480.png'),fullPage:true}); |
| | | await page.getByRole('button',{name:'A01',exact:true}).click(); |
| | | |
| | | await page.getByRole('button',{name:'æå¼æå头æ«ç '}).click(); |
| | | await page.getByRole('alert').waitFor(); |
| | | await page.getByRole('textbox',{name:'ç å¼ / 溯æºé¾æ¥'}).fill('https://example.test/trace?code=6028472024079516'); |
| | | await page.getByRole('button',{name:'è¯å«ç å¼',exact:true}).click(); |
| | | await page.getByRole('heading',{name:'æ ¸å¯¹å
¥åºå¯¹è±¡'}).waitFor(); |
| | | await page.screenshot({path:path.join(out,'inbound-confirm-480.png'),fullPage:true}); |
| | | assert.equal(submissions.length,0,'Recognition must not submit inventory'); |
| | | await page.getByRole('button',{name:'确认å
¥åº',exact:true}).click(); |
| | | await page.getByRole('heading',{name:'æ ¸å®ä¸æ¬¡å
¥åºç»æ'}).waitFor(); |
| | | assert.equal(submissions.length,1); |
| | | await page.reload(); |
| | | await page.getByRole('heading',{name:'æ ¸å®ä¸æ¬¡å
¥åºç»æ'}).waitFor(); |
| | | await page.getByRole('button',{name:'æ ¸å®ç»æ',exact:true}).click(); |
| | | await page.getByText('å°æªæ¥å°è®°å½ã请ç¨åæ ¸å®ï¼æä½¿ç¨åæä½å·éè¯ã').waitFor(); |
| | | await page.getByRole('button',{name:'使ç¨åæä½å·éè¯'}).click(); |
| | | await page.getByRole('heading',{name:'å
¥åºæå',exact:true}).waitFor(); |
| | | assert.equal(submissions.length,2);assert.deepEqual(submissions[0],submissions[1],'Timeout retry must preserve the entire payload'); |
| | | await page.getByRole('link',{name:'æ¥çä½ä¸è®°å½'}).click(); |
| | | await page.getByText('æ¥è¯¢æ¬äººè®°å½').waitFor(); |
| | | await page.screenshot({path:path.join(out,'records-480.png'),fullPage:true}); |
| | | assert(calls.some(c=>c.includes('/my-page')&&c.includes('receivedTime'))); |
| | | await page.goto(base+'/mobile/inbound'); |
| | | await page.getByRole('textbox',{name:'ç å¼ / 溯æºé¾æ¥'}).fill('8888888888888888'); |
| | | await page.getByRole('button',{name:'è¯å«ç å¼',exact:true}).click(); |
| | | await page.getByRole('heading',{name:'æ ¸å¯¹å
¥åºå¯¹è±¡'}).waitFor(); |
| | | assert.equal(submissions.length,2,'Business-code recognition must not submit inventory'); |
| | | await page.getByRole('button',{name:'确认å
¥åº',exact:true}).click(); |
| | | await page.getByRole('heading',{name:'å
¥åºæå',exact:true}).waitFor(); |
| | | assert.equal(submissions[2].bagCode,'8888888888888888'); |
| | | await page.goto(base+'/mobile/bags'); |
| | | await page.getByRole('button',{name:'å¼å§è¢ç ç»å®æç'}).click(); |
| | | await page.getByRole('textbox',{name:'ç å¼ / 溯æºé¾æ¥'}).fill('PALLET-TEST'); |
| | | await page.getByRole('button',{name:'è¯å«ç å¼',exact:true}).click(); |
| | | await page.getByText('éè¢éé',{exact:true}).waitFor(); |
| | | await page.getByRole('textbox',{name:'ç å¼ / 溯æºé¾æ¥'}).fill('6028472024079516'); |
| | | await page.getByRole('button',{name:'è¯å«ç å¼',exact:true}).click(); |
| | | await page.getByText('å·²éé 1 è¢ï¼å°æªæäº¤ç»å®').waitFor(); |
| | | await page.getByRole('button',{name:'è¯å«ç å¼',exact:true}).click(); |
| | | await page.getByText('æ¬æ¬¡å·²ç»éé该è¢ç ').waitFor(); |
| | | assert.equal(submissions.length,3); |
| | | await page.getByRole('button',{name:'确认ç»å®',exact:true}).click(); |
| | | await page.getByText('ç»å®æåï¼å·²æ´æ°å¯¹è±¡çç»å®å
³ç³»ã').waitFor(); |
| | | assert.deepEqual(submissions[3],{binding:{ids:[11],palletId:12}}); |
| | | await page.goto(base+'/mobile/stock');await page.getByText('æ¥è¯¢åºå',{exact:true}).waitFor(); |
| | | await page.getByRole('button',{name:'æ¥çæ°éæç»'}).click(); |
| | | await page.getByText('å¨åºæ°é',{exact:true}).waitFor(); |
| | | await page.screenshot({path:path.join(out,'stock-480.png'),fullPage:true}); |
| | | await page.getByRole('textbox',{name:'åç§åç§°',exact:true}).fill('çç±³'); |
| | | await page.getByRole('button',{name:'æ¥è¯¢åºå',exact:true}).click(); |
| | | await page.getByRole('button',{name:'æ¥è¯¢åºå',exact:true}).waitFor(); |
| | | await page.getByRole('textbox',{name:'åç§åç§°',exact:true}).fill('å°æªæ¥è¯¢'); |
| | | await page.getByRole('button',{name:'å è½½æ´å¤',exact:true}).click(); |
| | | await page.getByRole('alert').waitFor(); |
| | | await page.getByRole('button',{name:'å è½½æ´å¤',exact:true}).click(); |
| | | await page.getByText('ç¬¬äºæ¡åºå',{exact:true}).waitFor(); |
| | | const stockMore=calls.filter(c=>c.includes('/material-stock/page')&&c.includes('pageNo=2')); |
| | | assert.equal(stockMore.length,2);assert(stockMore.every(c=>c.includes('itemName=%E7%8E%89%E7%B1%B3'))); |
| | | const savedScroll=await page.evaluate(()=>{window.scrollTo(0,200);return window.scrollY;}); |
| | | await page.getByRole('button',{name:'è¿åä¸ä¸é¡µ'}).click(); |
| | | await page.getByRole('link',{name:'ä½ä¸',exact:true}).click(); |
| | | await page.getByRole('link',{name:'åºåæ¥è¯¢ åç§ãæ¹æ¬¡ä¸åºä½',exact:true}).click(); |
| | | assert.equal(await page.getByRole('textbox',{name:'åç§åç§°',exact:true}).inputValue(),'å°æªæ¥è¯¢'); |
| | | await page.waitForFunction(y=>Math.abs(window.scrollY-y)<2,savedScroll); |
| | | await page.getByText('ç¬¬äºæ¡åºå',{exact:true}).waitFor(); |
| | | assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>window.innerWidth),false); |
| | | restricted=true;await page.goto(base+'/mobile/stock'); |
| | | await page.waitForURL('**/mobile/home?forbidden=1');await page.getByText('ææ ç§»å¨ç«¯å¯ç¨åè½').waitFor(); |
| | | restricted=false;permissionFails=true;await page.goto(base+'/mobile/home'); |
| | | await page.waitForURL('**/auth/login?loadError=1'); |
| | | await page.getByText('èåå 载失败ï¼è¯·éæ°ç»å½åéè¯ã').waitFor(); |
| | | permissionFails=false; |
| | | await page.getByRole('button',{name:'ç»å½ç§»å¨ç«¯',exact:true}).click();await page.waitForURL('**/mobile/home'); |
| | | await page.goto(base+'/mobile/mine');await Promise.all([page.waitForResponse(r=>r.url().includes('terminalType=pc')),page.getByRole('button',{name:'åæ¢è³ PC 端'}).click()]); |
| | | await page.waitForFunction(()=>sessionStorage.getItem('mom-terminal')==='pc'); |
| | | |
| | | assert(calls.some(c=>c.includes('terminalType=pc')),'Switch must reload PC menus'); |
| | | await page.evaluate(()=>sessionStorage.setItem('mom-terminal','mobile'));await page.goto(base+'/mobile/mine'); |
| | | await page.getByRole('button',{name:'éåºç»å½',exact:true}).click();await page.waitForURL('**/auth/login'); |
| | | assert.equal(await page.evaluate(()=>Object.keys(sessionStorage).some(k=>k.startsWith('mom-mobile-'))),false); |
| | | await page.goto(base+'/mobile/inbound');await page.waitForURL('**/auth/login?redirect=*'); |
| | | const pc=await browser.newContext({viewport:{width:1280,height:800}});await pc.route('**/admin-api/**',r=>r.fulfill({json:{code:0,data:[]}})); |
| | | const desktop=await pc.newPage();await desktop.goto(base+'/auth/login'); |
| | | assert(await desktop.getByRole('radio',{name:'PC 端',exact:true}).isChecked()); |
| | | await desktop.screenshot({path:path.join(out,'login-pc.png'),fullPage:true}); |
| | | assert.deepEqual(errors,[]); |
| | | await browser.close();console.log(JSON.stringify({passed:true,calls,submissions},null,2)); |
| | | |
| | | })().catch(e=>{console.error(e);process.exit(1);}); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const { chromium } = require('playwright'); |
| | | const fs = require('fs'), path = require('path'), assert = require('assert/strict'); |
| | | const base = process.env.MOBILE_TEST_URL || 'http://127.0.0.1:5174'; |
| | | const out = path.resolve('.export_tmp/mobile-quality-sales');fs.mkdirSync(out,{recursive:true}); |
| | | (async()=>{ |
| | | const browser=await chromium.launch({headless:true,channel:'msedge'}); |
| | | const context=await browser.newContext({viewport:{width:1440,height:1000}}),page=await context.newPage(); |
| | | const writes=[], errors=[];page.on('pageerror',e=>errors.push(e.message)); |
| | | const order={id:11,code:'SALE-1',name:'æµè¯éå®åºåº',clientId:1,clientName:'æµè¯ç»éå',status:2,salesDate:'2026-10-10'}; |
| | | await context.route('**/*',async route=>{ |
| | | const req=route.request(),u=new URL(req.url()),p=u.pathname; |
| | | if(u.origin!==base)return route.abort();if(!p.startsWith('/admin-api/'))return route.continue(); |
| | | let data=[]; |
| | | if(p.endsWith('/auth/login'))data={accessToken:'test-token',refreshToken:'test-refresh',userId:7,expiresTime:Date.now()+3600000}; |
| | | else if(p.endsWith('/get-permission-info'))data={user:{id:7,nickname:'ä»åºæµè¯å',username:'fixture',homePath:'/wls/productsales'},roles:['worker'],permissions:['mes:wm-product-sales:query','mes:wm-product-sales:stock'],menus:[{id:1,name:'éå®åºåº',path:'/wls/productsales',component:'wls/productsales/index',visible:true}]}; |
| | | else if(p.endsWith('/product-sales/page'))data={list:[order],total:1}; |
| | | else if(p.endsWith('/product-sales/get'))data=order; |
| | | else if(p.endsWith('/product-sales-line/page'))data={list:[{id:21,itemId:1,itemName:'塿²³4å·',quantity:5,unitMeasureName:'è¢'}],total:1}; |
| | | else if(p.endsWith('/product-sales/scan-preview'))data={scanType:u.searchParams.get('scanType'),codeId:100,scanContent:u.searchParams.get('code'),batchCode:'BATCH-1',itemId:1,scanQuantity:3,bags:[31,32,33].map(id=>({id,code:'TRACE-'+id,businessCode:'BUS-'+id,selectable:true}))}; |
| | | else if(p.endsWith('/product-sales/scan')){const body=req.postDataJSON();writes.push(body);data={outboundQuantity:body.bagIds.length,alreadyScanned:false};} |
| | | else if(p.endsWith('/get-unread-count'))data=0; |
| | | else if(p.endsWith('/page'))data={list:[],total:0}; |
| | | return route.fulfill({json:{code:0,data,msg:''}}); |
| | | }); |
| | | try{ |
| | | await page.goto(base+'/auth/login');await page.getByRole('radio',{name:'PC 端',exact:true}).check(); |
| | | await page.locator('input[autocomplete=username]').fill('fixture');await page.locator('input[autocomplete=current-password]').fill('fixture');await page.getByRole('button',{name:'ç»å½ PC 端',exact:true}).click(); |
| | | await page.waitForURL('**/wls/productsales');await page.getByRole('button',{name:'æ£è´§',exact:true}).first().click(); |
| | | await page.getByRole('heading',{name:'æ§è¡æ£è´§',exact:true}).waitFor(); |
| | | await page.locator('.ant-select').filter({has:page.getByRole('combobox',{name:'æ£è´§æ«ç ç±»å'})}).click();await page.getByText('çäº§æ¹æ¬¡ç ',{exact:true}).click(); |
| | | await page.getByPlaceholder('æ«ææéç±»åç ï¼å车è¯å«').fill('BATCH-100');await page.getByRole('button',{name:'è¯å«ç å¼',exact:true}).click(); |
| | | await page.getByRole('heading',{name:'æ ¸å¯¹æ£è´§å¯¹è±¡'}).waitFor();assert.equal(writes.length,0); |
| | | await page.getByRole('spinbutton',{name:'æ¬æ¬¡æ£è´§æ°é'}).fill('2');await page.getByRole('button',{name:'ææ°ééæ©è¢ç '}).click(); |
| | | await page.getByRole('checkbox',{name:'éæ©è¢ç TRACE-31',exact:true}).uncheck();assert.equal(await page.getByRole('spinbutton',{name:'æ¬æ¬¡æ£è´§æ°é'}).inputValue(),'1');await page.getByRole('checkbox',{name:'éæ©è¢ç TRACE-33',exact:true}).check(); |
| | | await page.screenshot({path:path.join(out,'pc-sales-batch-selection.png'),fullPage:true}); |
| | | await page.getByRole('button',{name:'确认å å
¥æ£è´§'}).click();await page.getByRole('button',{name:/^ç¡®\s*认$/}).click(); |
| | | await page.getByText('å·²å å
¥æ£è´§ 2 è¢ï¼éè¢å
³è追溯ç ',{exact:true}).waitFor(); |
| | | assert.equal(writes.length,1);assert.equal(writes[0].scanType,'BATCH');assert.equal(writes[0].quantity,2);assert.deepEqual(writes[0].bagIds,[32,33]); |
| | | assert.deepEqual(errors,[]);console.log('PASS: PC batch partial picking, selected bag IDs and exact quantity; preview performs no writes'); |
| | | }catch(e){await page.screenshot({path:path.join(out,'pc-sales-selection-failure.png')});console.error(e);process.exitCode=1;} |
| | | finally{await browser.close();} |
| | | })(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ä¸è¢åç ï¼code ä¿æå追溯ç ï¼business_code 为å
鍿«ç åºå
¥åºçç¬ç«ç å¼ã |
| | | -- å
æ§è¡æ¬èæ¬ï¼åé¨ç½²å端ï¼å岿°æ®éè¿åå°å¾éè¡¥çæï¼ç¦æ¢ç´æ¥å¤å¶è¿½æº¯ç ã |
| | | SET @bag_business_column_exists = ( |
| | | SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_bag_code' AND column_name = 'business_code' |
| | | ); |
| | | SET @bag_business_ddl = IF(@bag_business_column_exists = 0, |
| | | 'ALTER TABLE mes_pro_bag_code ADD COLUMN business_code VARCHAR(16) DEFAULT NULL COMMENT ''å
é¨åºå
¥åºä¸å¡ç ï¼16使°åï¼'' AFTER code', 'SELECT 1'); |
| | | PREPARE bag_business_stmt FROM @bag_business_ddl; |
| | | EXECUTE bag_business_stmt; |
| | | DEALLOCATE PREPARE bag_business_stmt; |
| | | |
| | | SET @bag_business_index_exists = ( |
| | | SELECT COUNT(*) FROM information_schema.statistics |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_bag_code' AND index_name = 'uk_business_code' |
| | | ); |
| | | SET @bag_business_ddl = IF(@bag_business_index_exists = 0, |
| | | 'ALTER TABLE mes_pro_bag_code ADD UNIQUE KEY uk_business_code (business_code)', 'SELECT 1'); |
| | | PREPARE bag_business_stmt FROM @bag_business_ddl; |
| | | EXECUTE bag_business_stmt; |
| | | DEALLOCATE PREPARE bag_business_stmt; |
| | |
| | | CREATE TABLE IF NOT EXISTS `mes_pro_batch` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', |
| | | `code` VARCHAR(64) DEFAULT NULL COMMENT 'æ¹æ¬¡å·ï¼è¡¥å½ä¸å¡ä¿¡æ¯åèªå¨çæï¼å次å½å
¥æè¡¥å½æµç¨å¿
å¡«ï¼', |
| | | `business_code` VARCHAR(16) DEFAULT NULL COMMENT 'å
é¨åºå
¥åºä¸å¡ç ï¼16使°åï¼', |
| | | `temp_code` VARCHAR(64) DEFAULT NULL COMMENT 'ä¸´æ¶æ¹æ¬¡ç ï¼PB-8ä½å
¨å±åºå·ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼', |
| | | `item_id` BIGINT NOT NULL COMMENT 'åç§ï¼äº§åç©æï¼ç¼å·ï¼å
³è mdm_item.id', |
| | | `line_id` BIGINT DEFAULT NULL COMMENT '产线ç¼å·ï¼å
³è mes_pro_line.id', |
| | |
| | | `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_code` (`code`), |
| | | UNIQUE KEY `uk_business_code` (`business_code`), |
| | | UNIQUE KEY `uk_temp_code` (`temp_code`), |
| | | KEY `idx_item_id` (`item_id`), |
| | | KEY `idx_work_order_id` (`work_order_id`), |
| | |
| | | CREATE TABLE IF NOT EXISTS `mes_pro_pallet` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', |
| | | `code` VARCHAR(64) DEFAULT NULL COMMENT 'æçç ï¼æ¹æ¬¡å·-P+æçåºå·ï¼ç»å®æ¹æ¬¡åçæï¼', |
| | | `business_code` VARCHAR(16) DEFAULT NULL COMMENT 'å
é¨åºå
¥åºä¸å¡ç ï¼16使°åï¼', |
| | | `temp_code` VARCHAR(64) DEFAULT NULL COMMENT 'ä¸´æ¶æçç ï¼PL-8ä½å
¨å±åºå·ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼', |
| | | `item_id` BIGINT DEFAULT NULL COMMENT 'æçåç§ç¼å·', |
| | | `batch_id` BIGINT DEFAULT NULL COMMENT 'çäº§æ¹æ¬¡ç¼å·ï¼ç»å®æ¹æ¬¡åå
³è mes_pro_batch.id', |
| | | `batch_code` VARCHAR(64) DEFAULT NULL COMMENT 'æ¹æ¬¡å·ï¼åä½ï¼ä¾¿äºå±ç¤ºï¼', |
| | | `pallet_no` INT DEFAULT NULL COMMENT 'æ¹æ¬¡å
æçåºå·ï¼ç»å®æ¹æ¬¡åçæï¼', |
| | |
| | | `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_code` (`code`), |
| | | UNIQUE KEY `uk_business_code` (`business_code`), |
| | | UNIQUE KEY `uk_temp_code` (`temp_code`), |
| | | KEY `idx_batch_id` (`batch_id`) |
| | | ) ENGINE = InnoDB COMMENT = 'MES æçç '; |
| | |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', |
| | | `uuid` VARCHAR(32) DEFAULT NULL COMMENT 'çæç» UUIDï¼æ è®°å䏿¬¡çæçæ°æ®å
ï¼', |
| | | `code` VARCHAR(64) NOT NULL COMMENT 'è¢ç ï¼äºç»´ç å
容ï¼å
¨å±å¯ä¸ï¼', |
| | | `business_code` VARCHAR(16) DEFAULT NULL COMMENT 'å
é¨åºå
¥åºä¸å¡ç ï¼16使°åï¼', |
| | | `bag_no` INT NOT NULL COMMENT 'å
¨å±è¢åºå·', |
| | | `batch_id` BIGINT DEFAULT NULL COMMENT 'çäº§æ¹æ¬¡ç¼å·ï¼å½å
¥ä¿¡æ¯åå
³è mes_pro_batch.id', |
| | | `batch_code` VARCHAR(64) DEFAULT NULL COMMENT 'æ¹æ¬¡å·ï¼åä½ï¼ä¾¿äºå±ç¤ºï¼', |
| | |
| | | `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_code` (`code`), |
| | | UNIQUE KEY `uk_business_code` (`business_code`), |
| | | KEY `idx_batch_id` (`batch_id`), |
| | | KEY `idx_uuid` (`uuid`), |
| | | KEY `idx_pallet_id` (`pallet_id`) |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- æçåçäº§æ¹æ¬¡ç¬ç«ä¸å¡ç ï¼å
åçº§æ°æ®åºåé¨ç½²å端ãåå²è®°å½å¨é¡µé¢è¡¥çæã |
| | | SET @business_exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_pallet' AND column_name = 'business_code'); |
| | | SET @business_ddl = IF(@business_exists = 0, |
| | | 'ALTER TABLE mes_pro_pallet ADD COLUMN business_code VARCHAR(16) DEFAULT NULL COMMENT ''å
é¨åºå
¥åºä¸å¡ç ï¼16使°åï¼'' AFTER code', 'SELECT 1'); |
| | | PREPARE business_stmt FROM @business_ddl; |
| | | EXECUTE business_stmt; |
| | | DEALLOCATE PREPARE business_stmt; |
| | | SET @business_exists = (SELECT COUNT(*) FROM information_schema.statistics |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_pallet' AND index_name = 'uk_business_code'); |
| | | SET @business_ddl = IF(@business_exists = 0, |
| | | 'ALTER TABLE mes_pro_pallet ADD UNIQUE KEY uk_business_code (business_code)', 'SELECT 1'); |
| | | PREPARE business_stmt FROM @business_ddl; |
| | | EXECUTE business_stmt; |
| | | DEALLOCATE PREPARE business_stmt; |
| | | |
| | | SET @business_exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_batch' AND column_name = 'business_code'); |
| | | SET @business_ddl = IF(@business_exists = 0, |
| | | 'ALTER TABLE mes_pro_batch ADD COLUMN business_code VARCHAR(16) DEFAULT NULL COMMENT ''å
é¨åºå
¥åºä¸å¡ç ï¼16使°åï¼'' AFTER code', 'SELECT 1'); |
| | | PREPARE business_stmt FROM @business_ddl; |
| | | EXECUTE business_stmt; |
| | | DEALLOCATE PREPARE business_stmt; |
| | | SET @business_exists = (SELECT COUNT(*) FROM information_schema.statistics |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_batch' AND index_name = 'uk_business_code'); |
| | | SET @business_ddl = IF(@business_exists = 0, |
| | | 'ALTER TABLE mes_pro_batch ADD UNIQUE KEY uk_business_code (business_code)', 'SELECT 1'); |
| | | PREPARE business_stmt FROM @business_ddl; |
| | | EXECUTE business_stmt; |
| | | DEALLOCATE PREPARE business_stmt; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- æçä¿ååç§ï¼ä»
仿æè¢ç ä¸å¯ä¸ä¸ä¸å·²ææ¹æ¬¡å
¼å®¹çåç§åå¡«ã |
| | | SET @pallet_item_exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema=DATABASE() AND table_name='mes_pro_pallet' AND column_name='item_id'); |
| | | SET @pallet_item_ddl = IF(@pallet_item_exists=0, |
| | | 'ALTER TABLE mes_pro_pallet ADD COLUMN item_id BIGINT DEFAULT NULL COMMENT ''æçåç§ç¼å·'' AFTER temp_code', 'SELECT 1'); |
| | | PREPARE pallet_item_stmt FROM @pallet_item_ddl; |
| | | EXECUTE pallet_item_stmt; |
| | | DEALLOCATE PREPARE pallet_item_stmt; |
| | | |
| | | UPDATE mes_pro_pallet p |
| | | JOIN (SELECT pallet_id, MIN(item_id) item_id FROM mes_pro_bag_code |
| | | WHERE deleted=0 AND status<>30 AND pallet_id IS NOT NULL GROUP BY pallet_id |
| | | HAVING COUNT(DISTINCT item_id)=1 AND COUNT(*)=COUNT(item_id)) b ON b.pallet_id=p.id |
| | | LEFT JOIN mes_pro_batch ba ON ba.id=p.batch_id AND ba.deleted=0 |
| | | SET p.item_id=b.item_id |
| | | WHERE p.deleted=0 AND p.item_id IS NULL AND (ba.item_id IS NULL OR ba.item_id=b.item_id); |
| | | |
| | | UPDATE mes_pro_batch ba |
| | | JOIN (SELECT batch_id,MIN(item_id) item_id FROM mes_pro_bag_code |
| | | WHERE deleted=0 AND status<>30 AND batch_id IS NOT NULL GROUP BY batch_id |
| | | HAVING COUNT(DISTINCT item_id)=1 AND COUNT(*)=COUNT(item_id)) b ON b.batch_id=ba.id |
| | | SET ba.item_id=b.item_id |
| | | WHERE ba.deleted=0 AND ba.item_id IS NULL |
| | | AND NOT EXISTS (SELECT 1 FROM mes_trace_integrity_record r WHERE r.biz_type='BATCH' AND r.biz_id=ba.id AND r.deleted=0) |
| | | AND NOT EXISTS (SELECT 1 FROM mes_pro_pallet p WHERE p.batch_id=ba.id AND p.deleted=0 AND p.item_id IS NOT NULL AND p.item_id<>b.item_id); |
| | |
| | | } |
| | | |
| | | @GetMapping("/get-by-code") |
| | | @Operation(summary = "æ«ç è¿½æº¯ï¼æ ¹æ®è¢ç ï¼äºç»´ç å
容ï¼åæ¥æ¹æ¬¡ã产线ã责任人çä¿¡æ¯") |
| | | @Operation(summary = "å
鍿«ç æ¥è¯¢ï¼æ ¹æ®è¿½æº¯ç æä¸å¡ç 忥è¢ãæ¹æ¬¡ã产线信æ¯") |
| | | @Parameter(name = "code", description = "åå
è¯å«ä»£ç ï¼å½æ ï¼ä¸è¶
è¿ 20 个åç¬¦ï¼æ°åæåæ¯æ°åç»åï¼", |
| | | required = true, example = "0286267849370923") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:query')") |
| | | public CommonResult<MesProBagCodeTraceRespVO> getBagCodeByCode(@RequestParam("code") String code) { |
| | | MesProBagCodeDO bagCode = bagCodeService.getBagCodeByCode(code); |
| | | MesProBagCodeDO bagCode = bagCodeService.getBagCodeByScanCode(code); |
| | | if (bagCode == null) { |
| | | throw exception(PRO_BAG_CODE_NOT_EXISTS); |
| | | } |
| | |
| | | |
| | | // ==================== æ¼æ¥ VO ==================== |
| | | |
| | | @PostMapping("/generate-business-code") |
| | | @Operation(summary = "è¡¥çæå·²æè¢ç çåºå
¥åºä¸å¡ç ï¼å·²æç å¼ä¸åï¼") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:create')") |
| | | public CommonResult<Integer> generateBusinessCodes(@Valid @RequestBody |
| | | cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagBusinessCodeGenerateReqVO reqVO) { |
| | | return success(bagCodeService.generateBusinessCodes(reqVO.getIds())); |
| | | } |
| | | |
| | | @GetMapping("/export-business-qr") |
| | | @Operation(summary = "导åºåºå
¥åºä¸å¡ç äºç»´ç ZIPï¼äºç»´ç ä»
ç¼ç ä¸å¡ç å¼ï¼") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:export')") |
| | | @ApiAccessLog(operateType = EXPORT) |
| | | public void exportBusinessQr(@RequestParam(value = "uuid", required = false) String uuid, |
| | | @RequestParam(value = "batchId", required = false) Long batchId, |
| | | HttpServletResponse response) throws IOException { |
| | | List<MesProBagCodeDO> list; |
| | | if (StrUtil.isNotBlank(uuid)) list = bagCodeService.getPrintableBagCodeListByUuid(uuid); |
| | | else if (batchId != null) list = bagCodeService.getPrintableBagCodeListByBatchId(batchId); |
| | | else throw exception(PRO_QR_EXPORT_PARAMS_INVALID); |
| | | if (CollUtil.isEmpty(list)) throw exception(PRO_BAG_CODE_NOT_EXISTS); |
| | | if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) throw exception(PRO_QR_EXPORT_TOO_MANY); |
| | | List<QrLabelInfo> labels = list.stream().map(MesProQrLabelBuilder::buildBagBusinessCodeLabel).toList(); |
| | | QrCodeImageUtils.writeZip(response, "è¢ç ä¸å¡äºç»´ç -" + (StrUtil.isNotBlank(uuid) ? uuid : batchId), labels); |
| | | } |
| | | |
| | | private List<MesProBagCodeRespVO> buildBagCodeRespVOList(List<MesProBagCodeDO> list) { |
| | | if (CollUtil.isEmpty(list)) { |
| | | return Collections.emptyList(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.constraints.NotEmpty; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | import lombok.Data; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | @Schema(description = "管çåå° - å·²æè¢ç è¡¥çæä¸å¡ç ") |
| | | public class MesProBagBusinessCodeGenerateReqVO { |
| | | @NotEmpty |
| | | @Size(max = 200) |
| | | private List<@NotNull Long> ids; |
| | | } |
| | |
| | | @Schema(description = "è¢ç ", example = "PC20260818L01A001-0001") |
| | | private String code; |
| | | |
| | | @Schema(description = "åºå
¥åºä¸å¡ç ") |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "çäº§æ¹æ¬¡ç¼å·", example = "1024") |
| | | private Long batchId; |
| | | |
| | |
| | | @Schema(description = "管çåå° - MES è¢ç èµç Response VO") |
| | | @Data |
| | | public class MesProBagCodeRespVO { |
| | | @ExcelProperty("å
¥åºæ¶é´") |
| | | private LocalDateTime inboundTime; |
| | | |
| | | @Schema(description = "ç¼å·", example = "1024") |
| | | @ExcelIgnore |
| | |
| | | private String uuid; |
| | | |
| | | @Schema(description = "è¢ç ï¼äºç»´ç å
容ï¼", example = "PC20260818L01001-0001") |
| | | @ExcelProperty("äºç»´ç å
容") |
| | | @ExcelProperty("追溯ç ") |
| | | @ColumnWidth(30) |
| | | private String code; |
| | | |
| | | @Schema(description = "åºå
¥åºä¸å¡ç ï¼äºç»´ç ç¼ç 纯ç å¼ï¼") |
| | | @ExcelProperty("ä¸å¡ç ") |
| | | @ColumnWidth(24) |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "æ¹æ¬¡å
è¢åºå·", example = "1") |
| | | @ExcelProperty("è¢åºå·") |
| | | private Integer bagNo; |
| | |
| | | @Schema(description = "管çåå° - MES è¢ç æ«ç 追溯 Response VO") |
| | | @Data |
| | | public class MesProBagCodeTraceRespVO { |
| | | private LocalDateTime inboundTime; |
| | | |
| | | // ========== è¢ç ä¿¡æ¯ ========== |
| | | |
| | |
| | | @Schema(description = "è¢ç ï¼äºç»´ç å
容ï¼", example = "PC20260818L01001-0001") |
| | | private String code; |
| | | |
| | | @Schema(description = "å
é¨åºå
¥åºä¸å¡ç ") |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "æ¹æ¬¡å
è¢åºå·", example = "1") |
| | | private Integer bagNo; |
| | | |
| | |
| | | @Parameter(name = "code", description = "æ¹æ¬¡å·ï¼äºç»´ç å
容ï¼", required = true, example = "PC20260818L01001") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-batch:query')") |
| | | public CommonResult<MesProBatchTraceRespVO> getBatchByCode(@RequestParam("code") String code) { |
| | | MesProBatchDO batch = batchService.getBatchByCode(code); |
| | | MesProBatchDO batch = batchService.getBatchByScanCode(code); |
| | | if (batch == null) { |
| | | throw exception(PRO_BATCH_NOT_EXISTS); |
| | | } |
| | |
| | | |
| | | // ==================== æ¼æ¥ VO ==================== |
| | | |
| | | @PostMapping("/generate-business-code") |
| | | @Operation(summary = "è¡¥çæå·²æçäº§æ¹æ¬¡ä¸å¡ç ï¼å·²æç å¼ä¸å") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-batch:create')") |
| | | public CommonResult<Integer> generateBusinessCodes(@Valid @RequestBody |
| | | cn.iocoder.yudao.module.mes.controller.admin.pro.vo.MesProBusinessCodeGenerateReqVO reqVO) { |
| | | return success(batchService.generateBusinessCodes(reqVO.getIds())); |
| | | } |
| | | |
| | | @GetMapping("/export-business-qr") |
| | | @Operation(summary = "å¯¼åºæ¹æ¬¡åæå±æçä¸å¡äºç»´ç ï¼å
容为纯ä¸å¡ç å¼") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-batch:export')") |
| | | @ApiAccessLog(operateType = EXPORT) |
| | | public void exportBusinessQr(@RequestParam("batchId") Long batchId, |
| | | HttpServletResponse response) throws IOException { |
| | | MesProBatchDO batch = batchService.getBatch(batchId); |
| | | if (batch == null) throw exception(PRO_BATCH_NOT_EXISTS); |
| | | if (cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum.CANCELED.getStatus().equals(batch.getStatus())) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "å·²ä½åºæ¹æ¬¡ä¸å¯å¯¼åºä¸å¡äºç»´ç "); |
| | | List<MesProPalletDO> pallets = palletService.getPalletListByBatchId(batchId).stream() |
| | | .filter(record -> !cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus())).toList(); |
| | | if (pallets.size() + 1 > QrCodeImageUtils.MAX_QR_EXPORT) throw exception(PRO_QR_EXPORT_TOO_MANY); |
| | | List<QrLabelInfo> labels = new java.util.ArrayList<>(); |
| | | labels.add(MesProQrLabelBuilder.buildBatchBusinessCodeLabel(batch)); |
| | | pallets.forEach(pallet -> labels.add(MesProQrLabelBuilder.buildPalletBusinessCodeLabel(pallet))); |
| | | QrCodeImageUtils.writeZip(response, "æ¹æ¬¡ä¸å¡äºç»´ç -" + batch.getBusinessCode(), labels); |
| | | } |
| | | |
| | | private List<MesProBatchRespVO> buildBatchRespVOList(List<MesProBatchDO> list) { |
| | | if (CollUtil.isEmpty(list)) { |
| | | return Collections.emptyList(); |
| | |
| | | // 1. è·å¾å
³èæ°æ®ï¼è´£ä»»äººï¼ï¼åç§å·²ä¸æ²å°è¢ç æ°æ®å
ï¼æ¹æ¬¡å表ä¸åå±ç¤º |
| | | Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap( |
| | | convertSet(list, MesProBatchDO::getOwnerUserId)); |
| | | // 2. æå»ºç»æ |
| | | return BeanUtils.toBean(list, MesProBatchRespVO.class, vo -> |
| | | MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> |
| | | vo.setOwnerUserName(user.getNickname()))); |
| | | Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(convertSet(list, MesProBatchDO::getItemId)); |
| | | return BeanUtils.toBean(list, MesProBatchRespVO.class, vo -> { |
| | | MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname())); |
| | | MapUtils.findAndThen(itemMap, vo.getItemId(), item -> { vo.setItemCode(item.getCode()); vo.setItemName(item.getName()); }); |
| | | }); |
| | | } |
| | | |
| | | private List<MesProBatchWorkerRespVO> buildWorkerRespVOList(List<MesProBatchWorkerDO> workers) { |
| | |
| | | respVO.setPallets(BeanUtils.toBean(palletService.getPalletListByBatchId(batch.getId()), MesProPalletRespVO.class)); |
| | | // è¢ç å表ï¼å°é¡¶ 2000ï¼bagTotal è®°å½æ»æ°ï¼ |
| | | List<MesProBagCodeDO> bags = bagCodeService.getBagCodeListByBatchId(batch.getId()); |
| | | respVO.setPendingBagQuantity((int) bags.stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() == null).count()); |
| | | respVO.setBagTotal(bags.size()); |
| | | if (bags.size() > QrCodeImageUtils.MAX_QR_EXPORT) { |
| | | bags = bags.subList(0, QrCodeImageUtils.MAX_QR_EXPORT); |
| | |
| | | |
| | | @Schema(description = "æ¹æ¬¡å·", example = "PC20260818L01NY001") |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "ä¸´æ¶æ¹æ¬¡ç ", example = "PB-00000001") |
| | | private String tempCode; |
| | |
| | | |
| | | @Schema(description = "ä¸´æ¶æ¹æ¬¡ç ï¼äºç»´ç å
容ï¼", example = "PB-00000001") |
| | | private String tempCode; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | |
| | | } |
| | | } |
| | |
| | | @ExcelProperty("æ¹æ¬¡å·") |
| | | @ColumnWidth(25) |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | @ExcelProperty("ä¸å¡ç ") |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "ä¸´æ¶æ¹æ¬¡ç ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼è¡¥å½æ£å¼ä¿¡æ¯å为空æä¿çï¼", example = "PB-00000001") |
| | | @ExcelIgnore |
| | | private String tempCode; |
| | | @ExcelIgnore |
| | | private Long itemId; |
| | | @ExcelProperty("åç§ç¼ç ") |
| | | private String itemCode; |
| | | @ExcelProperty("åç§åç§°") |
| | | private String itemName; |
| | | |
| | | @Schema(description = "产线ç¼å·", example = "1") |
| | | @ExcelIgnore |
| | |
| | | |
| | | @Schema(description = "ç¼å·", example = "1024") |
| | | private Long id; |
| | | @Schema(description = "åç§ç¼å·ï¼ç±ç»å®æç带å
¥å¹¶ç»åç«¯æ ¡éª") |
| | | private Long itemId; |
| | | |
| | | @Schema(description = "产线ç¼å·ï¼é¡µé¢å±ç¤ºä¸ºãå å·¥åä½ãï¼", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| | | @NotNull(message = "å å·¥åä½ä¸è½ä¸ºç©º") |
| | |
| | | @Schema(description = "管çåå° - MES çäº§æ¹æ¬¡æ«ç 追溯 Response VO") |
| | | @Data |
| | | public class MesProBatchTraceRespVO { |
| | | private Integer inboundBagQuantity; |
| | | private Integer pendingBagQuantity; |
| | | |
| | | // ========== æ¹æ¬¡ä¿¡æ¯ ========== |
| | | |
| | |
| | | |
| | | @Schema(description = "æ¹æ¬¡å·ï¼è¡¥å½åçæ£å¼æ¹æ¬¡å·ï¼", example = "PC20260818L01001") |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "ä¸´æ¶æ¹æ¬¡ç ï¼äºç»´ç å
å®¹ï¼æªè¡¥å½ä¿¡æ¯æ¶å±ç¤ºï¼", example = "PB-00000001") |
| | | private String tempCode; |
| | |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')") |
| | | public CommonResult<MesProPalletRespVO> getPallet(@RequestParam("id") Long id) { |
| | | MesProPalletDO pallet = palletService.getPallet(id); |
| | | return success(BeanUtils.toBean(pallet, MesProPalletRespVO.class)); |
| | | return success(pallet == null ? null : buildPalletRespVOList(List.of(pallet)).get(0)); |
| | | } |
| | | |
| | | @GetMapping("/get-by-code") |
| | |
| | | @Parameter(name = "code", description = "æçç ï¼äºç»´ç å
容ï¼", required = true, example = "PC20260818L01001-P01") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')") |
| | | public CommonResult<MesProPalletTraceRespVO> getPalletByCode(@RequestParam("code") String code) { |
| | | MesProPalletDO pallet = palletService.getPalletByCode(code); |
| | | MesProPalletDO pallet = palletService.getPalletByScanCode(code); |
| | | if (pallet == null) { |
| | | throw exception(PRO_PALLET_NOT_EXISTS); |
| | | } |
| | |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')") |
| | | public CommonResult<PageResult<MesProPalletRespVO>> getPalletPage(@Valid MesProPalletPageReqVO pageReqVO) { |
| | | PageResult<MesProPalletDO> pageResult = palletService.getPalletPage(pageReqVO); |
| | | return success(BeanUtils.toBean(pageResult, MesProPalletRespVO.class)); |
| | | return success(new PageResult<>(buildPalletRespVOList(pageResult.getList()), pageResult.getTotal())); |
| | | } |
| | | |
| | | @GetMapping("/simple-list") |
| | |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')") |
| | | public CommonResult<List<MesProPalletRespVO>> getPalletSimpleList(@RequestParam("batchId") Long batchId) { |
| | | List<MesProPalletDO> list = palletService.getPalletListByBatchId(batchId); |
| | | return success(BeanUtils.toBean(list, MesProPalletRespVO.class)); |
| | | return success(buildPalletRespVOList(list)); |
| | | } |
| | | |
| | | @GetMapping("/unbound-list") |
| | |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')") |
| | | public CommonResult<List<MesProPalletRespVO>> getUnboundPalletList() { |
| | | List<MesProPalletDO> list = palletService.getUnboundPalletList(); |
| | | return success(BeanUtils.toBean(list, MesProPalletRespVO.class)); |
| | | return success(buildPalletRespVOList(list)); |
| | | } |
| | | |
| | | @GetMapping("/export-excel") |
| | |
| | | HttpServletResponse response) throws IOException { |
| | | pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); |
| | | List<MesProPalletDO> list = palletService.getPalletPage(pageReqVO).getList(); |
| | | List<MesProPalletRespVO> voList = BeanUtils.toBean(list, MesProPalletRespVO.class); |
| | | List<MesProPalletRespVO> voList = buildPalletRespVOList(list); |
| | | ExcelUtils.write(response, "æçç .xls", "æ°æ®", MesProPalletRespVO.class, voList); |
| | | } |
| | | |
| | |
| | | |
| | | // ==================== æ¼æ¥ VO ==================== |
| | | |
| | | @PostMapping("/generate-business-code") |
| | | @Operation(summary = "è¡¥çæå·²ææçä¸å¡ç ï¼å·²æç å¼ä¸å") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:create')") |
| | | public CommonResult<Integer> generateBusinessCodes(@Valid @RequestBody |
| | | cn.iocoder.yudao.module.mes.controller.admin.pro.vo.MesProBusinessCodeGenerateReqVO reqVO) { |
| | | return success(palletService.generateBusinessCodes(reqVO.getIds())); |
| | | } |
| | | |
| | | @PostMapping("/export-business-qr-by-ids") |
| | | @Operation(summary = "æç¼å·å¯¼åºæçä¸å¡ç äºç»´ç ï¼å
容为纯ä¸å¡ç å¼") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-pallet:export')") |
| | | @ApiAccessLog(operateType = EXPORT) |
| | | public void exportBusinessQrByIds(@Valid @RequestBody MesProPalletExportQrReqVO reqVO, |
| | | HttpServletResponse response) throws IOException { |
| | | List<MesProPalletDO> list = palletService.getPalletListByIds(reqVO.getIds()).stream() |
| | | .filter(record -> !cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus())).toList(); |
| | | if (CollUtil.isEmpty(list)) throw exception(PRO_PALLET_NOT_EXISTS); |
| | | if (list.size() > QrCodeImageUtils.MAX_QR_EXPORT) throw exception(PRO_QR_EXPORT_TOO_MANY); |
| | | QrCodeImageUtils.writeZip(response, "æçä¸å¡äºç»´ç ", list.stream() |
| | | .map(MesProQrLabelBuilder::buildPalletBusinessCodeLabel).toList()); |
| | | } |
| | | |
| | | private List<MesProPalletRespVO> buildPalletRespVOList(List<MesProPalletDO> list) { |
| | | Map<Long, MesMdmItemDO> items = mdmItemService.getItemMap(convertSet(list, MesProPalletDO::getItemId)); |
| | | return BeanUtils.toBean(list, MesProPalletRespVO.class, vo -> |
| | | MapUtils.findAndThen(items, vo.getItemId(), item -> { vo.setItemCode(item.getCode()); vo.setItemName(item.getName()); })); |
| | | } |
| | | |
| | | private MesProPalletTraceRespVO buildPalletTraceRespVO(MesProPalletDO pallet) { |
| | | MesProPalletTraceRespVO respVO = BeanUtils.toBean(pallet, MesProPalletTraceRespVO.class); |
| | | respVO.setPendingBagQuantity((int) bagCodeService.getBagCodeListByPalletId(pallet.getId()).stream() |
| | | .filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() == null).count()); |
| | | if (pallet.getItemId() != null) { |
| | | MesMdmItemDO item = mdmItemService.getItem(pallet.getItemId()); |
| | | if (item != null) respVO.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification()); |
| | | } |
| | | // æ¹æ¬¡ä¿¡æ¯ï¼ä¸´æ¶æçæªç»å®æ¹æ¬¡æ¶ä¸ºç©ºæ¹æ¬¡ï¼ |
| | | MesProBatchDO batch = pallet.getBatchId() == null ? null : batchService.getBatch(pallet.getBatchId()); |
| | | if (batch != null) { |
| | |
| | | |
| | | @Schema(description = "æçç ", example = "PC20260818L01A001-P01") |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "ä¸´æ¶æçç ", example = "PL-00000001") |
| | | private String tempCode; |
| | |
| | | |
| | | @Schema(description = "ä¸´æ¶æçç ï¼äºç»´ç å
容ï¼", example = "PL-00000001") |
| | | private String tempCode; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | |
| | | } |
| | | } |
| | |
| | | @Schema(description = "管çåå° - MES æçç Response VO") |
| | | @Data |
| | | public class MesProPalletRespVO { |
| | | @ExcelProperty("å
¥åºæ¶é´") |
| | | private LocalDateTime inboundTime; |
| | | |
| | | @Schema(description = "ç¼å·", example = "1024") |
| | | @ExcelIgnore |
| | |
| | | @ExcelProperty("æçç ") |
| | | @ColumnWidth(30) |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | @ExcelProperty("ä¸å¡ç ") |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "ä¸´æ¶æçç ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼ç»å®æ¹æ¬¡å为空æä¿çï¼", example = "PL-00000001") |
| | | @ExcelIgnore |
| | | private String tempCode; |
| | | @ExcelIgnore |
| | | private Long itemId; |
| | | @ExcelProperty("åç§ç¼ç ") |
| | | private String itemCode; |
| | | @ExcelProperty("åç§åç§°") |
| | | private String itemName; |
| | | |
| | | @Schema(description = "çäº§æ¹æ¬¡ç¼å·", example = "1024") |
| | | @ExcelIgnore |
| | |
| | | @Schema(description = "管çåå° - MES æçç æ«ç 追溯 Response VO") |
| | | @Data |
| | | public class MesProPalletTraceRespVO { |
| | | private LocalDateTime inboundTime; |
| | | private Integer pendingBagQuantity; |
| | | |
| | | // ========== æçä¿¡æ¯ ========== |
| | | |
| | |
| | | |
| | | @Schema(description = "æçç ï¼ç»å®æ¹æ¬¡åçæ£å¼æçç ï¼", example = "PC20260818L01001-P01") |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | |
| | | @Schema(description = "ä¸´æ¶æçç ï¼äºç»´ç å
å®¹ï¼æªç»å®æ¹æ¬¡æ¶å±ç¤ºï¼", example = "PL-00000001") |
| | | private String tempCode; |
| | |
| | | PageResult<MesProScanEventDO> page = service.getPage(req); |
| | | return success(new PageResult<>(BeanUtils.toBean(page.getList(), MesProScanEventRespVO.class), page.getTotal())); |
| | | } |
| | | @GetMapping("/my-page") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:query')") |
| | | @Operation(summary = "æ¬äººæ«ç ä½ä¸è®°å½") |
| | | public CommonResult<PageResult<MesProScanEventRespVO>> myPage(@Valid MesProScanEventPageReqVO req) { |
| | | req.setCreator(String.valueOf(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId())); |
| | | return page(req); |
| | | } |
| | | @GetMapping("/my-event") @PreAuthorize("@ss.hasAnyPermissions('mes:pro-scan-in:query','mes:pro-scan-in:inbound')") |
| | | @Operation(summary = "æ ¸å®æ¬äººæ«ç æä½ç»æ") |
| | | public CommonResult<MesProScanEventRespVO> myEvent(@RequestParam String eventId) { |
| | | MesProScanEventDO event = service.getByEventId(eventId); |
| | | String userId = String.valueOf(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId()); |
| | | return success(event != null && userId.equals(event.getCreator()) |
| | | ? BeanUtils.toBean(event, MesProScanEventRespVO.class) : null); |
| | | } |
| | | @GetMapping("/latest") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:query')") @Operation(summary = "ææ°æ«ç äºä»¶") |
| | | public CommonResult<MesProScanEventRespVO> latest(@RequestParam(required = false) String deviceCode) { |
| | | return success(BeanUtils.toBean(service.getLatest(deviceCode), MesProScanEventRespVO.class)); |
| | |
| | | private String deviceCode; |
| | | private String status; |
| | | private String errorCode; |
| | | // The mobile endpoint always overrides this with the authenticated user. |
| | | private String creator; |
| | | @org.springframework.format.annotation.DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | private java.time.LocalDateTime[] receivedTime; |
| | | } |
| | |
| | | @GetMapping("/resolve-type") |
| | | @Operation(summary = "æ ¹æ®ç¼ç è§£ææº¯æºç±»åï¼bag / pallet / batchï¼åä¸å¹é
è¿å空") |
| | | @Parameter(name = "code", description = "ç¼ç ï¼äºç»´ç å
容ï¼", required = true) |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:query')") |
| | | @PreAuthorize("@ss.hasAnyPermissions('mes:pro-bag-code:query', 'mes:pro-pallet:query', 'mes:pro-batch:query')") |
| | | public CommonResult<String> resolveType(@RequestParam("code") String code) { |
| | | if (bagCodeService.getBagCodeByCode(code) != null) { |
| | | if (bagCodeService.getBagCodeByScanCode(code) != null) { |
| | | return success("bag"); |
| | | } |
| | | if (palletService.getPalletByCode(code) != null) { |
| | | if (palletService.getPalletByScanCode(code) != null) { |
| | | return success("pallet"); |
| | | } |
| | | if (batchService.getBatchByCode(code) != null) { |
| | | if (batchService.getBatchByScanCode(code) != null) { |
| | | return success("batch"); |
| | | } |
| | | return success(null); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.pro.vo; |
| | | |
| | | import jakarta.validation.constraints.NotEmpty; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | import lombok.Data; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | public class MesProBusinessCodeGenerateReqVO { |
| | | @NotEmpty @Size(max = 200) |
| | | private List<@NotNull Long> ids; |
| | | } |
| | |
| | | @RestController @RequestMapping("/mes/qc/seed-quality") @Validated |
| | | public class MesQcSeedQualityController { |
| | | @Resource private MesQcSeedQualityService service; |
| | | @GetMapping("/resolve") |
| | | @Operation(summary = "è§£æå¾
è´¨æ£è¢ç å确认该è¢è´¨æ£ç»æ") |
| | | @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')") |
| | | public CommonResult<java.util.Map<String, Object>> resolve(@RequestParam String code, @RequestParam(defaultValue = "BAG") String scanType) { |
| | | return success(service.resolveScan(code, scanType)); |
| | | } |
| | | @GetMapping("/page") |
| | | @Operation(summary = "è·å¾ç§åè´¨æ£è®°å½å页") |
| | | @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')") |
| | |
| | | @PostMapping("/submit") @Operation(summary = "æäº¤ç§åè´¨æ£") |
| | | @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')") |
| | | public CommonResult<Long> submit(@Valid @RequestBody MesQcSeedQualitySubmitReqVO reqVO) { return success(service.submit(reqVO)); } |
| | | |
| | | @PostMapping("/submit-group") @Operation(summary = "æè¢ç ãæçæçäº§æ¹æ¬¡ç»ä¸æäº¤ç§åè´¨æ£") |
| | | @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')") |
| | | public CommonResult<java.util.Map<String, Object>> submitGroup(@Valid @RequestBody cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO) { |
| | | return success(service.submitGroup(reqVO)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo; |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotEmpty; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Pattern; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public class MesQcSeedQualityGroupSubmitReqVO extends MesQcSeedQualitySubmitReqVO { |
| | | @NotBlank @Pattern(regexp = "BAG|PALLET|BATCH") |
| | | private String scanType; |
| | | /** æä½åæ ¸å¯¹è¿çå¾
æ£è¢æ¸
åï¼æäº¤æ¶éå®å¹¶éæ°æ ¸å®ï¼ç¦æ¢æææ©å¤§æ£éªèå´ã */ |
| | | @NotEmpty |
| | | private List<@NotNull Long> bagIds; |
| | | } |
| | |
| | | return success(productSalesService.scanProductSales(reqVO)); |
| | | } |
| | | |
| | | @GetMapping("/scan-preview") |
| | | @Operation(summary = "æ ¸å¯¹æ«ç æ£è´§å¯¹è±¡ï¼ä¸åå
¥æç»ï¼") |
| | | @PreAuthorize("@ss.hasPermission('mes:wm-product-sales:stock')") |
| | | public CommonResult<MesWmProductSalesScanRespVO> previewScan(@RequestParam String code, |
| | | @RequestParam(required = false) String scanType, @RequestParam(required = false) Long salesId) { |
| | | return success(productSalesService.previewScan(code, scanType, salesId)); |
| | | } |
| | | |
| | | @PutMapping("/stock") |
| | | @Operation(summary = "æ§è¡æ£è´§") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | |
| | | }); |
| | | } |
| | | |
| | | } |
| | | } |
| | |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | @Schema(description = "管çåå° - MES éå®åºåºæ«ç æ£è´§ Request VO") |
| | | @Data |
| | |
| | | @NotNull(message = "éå®åºåºåIDä¸è½ä¸ºç©º") |
| | | private Long salesId; |
| | | |
| | | @Schema(description = "æ«æå
容ï¼è¢ç ææçç ï¼", requiredMode = Schema.RequiredMode.REQUIRED, example = "PC2026081920-0001") |
| | | @Schema(description = "æ«æå
容ï¼è¢ç ãæçç æçäº§æ¹æ¬¡ç ï¼", requiredMode = Schema.RequiredMode.REQUIRED, example = "PC2026081920-0001") |
| | | @NotEmpty(message = "æ«æå
容ä¸è½ä¸ºç©º") |
| | | private String scanContent; |
| | | |
| | | @Schema(description = "æ¬æ¬¡åºåºæ°éï¼ä¸ºç©ºåå该è¢ç /æ´æå
¨é¨æ°éï¼", example = "50") |
| | | @Schema(description = "æ¬æ¬¡æ£è´§è¢æ°ï¼å¿
须䏿éè¢ç æ°éç¸å", example = "50") |
| | | private BigDecimal quantity; |
| | | |
| | | } |
| | | @jakarta.validation.constraints.Pattern(regexp = "BAG|PALLET|BATCH", message = "æ«ç ç±»åæ æ") |
| | | private String scanType; |
| | | |
| | | @Schema(description = "æ¬æ¬¡æç¡®éä¸çè¢ç IDï¼æçåæ¹æ¬¡å¿
须鿩") |
| | | private List<@NotNull Long> bagIds; |
| | | |
| | | } |
| | |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | @Schema(description = "管çåå° - MES éå®åºåºæ«ç æ£è´§ Response VO") |
| | | @Data |
| | | public class MesWmProductSalesScanRespVO { |
| | | |
| | | @Schema(description = "è¯å«ç±»åï¼PALLET-æçç ï¼BAG-è¢ç ", example = "BAG") |
| | | @Schema(description = "è¯å«ç±»åï¼PALLET-æçç ï¼BAG-è¢ç ï¼BATCH-çäº§æ¹æ¬¡ç ", example = "BAG") |
| | | private String scanType; |
| | | |
| | | @Schema(description = "æ«æçç ç¼å·", example = "12") |
| | |
| | | @Schema(description = "æä½ä¿¡æ¯æç¤º", example = "æ«ç æå") |
| | | private String message; |
| | | |
| | | } |
| | | private List<BagOption> bags; |
| | | private List<Long> selectedBagIds; |
| | | |
| | | @Data |
| | | public static class BagOption { |
| | | private Long id; |
| | | private String code; |
| | | private String businessCode; |
| | | private Long palletId; |
| | | private Boolean selectable; |
| | | private String unavailableReason; |
| | | } |
| | | |
| | | } |
| | |
| | | @Schema(description = "æ£è´§æ°é", example = "50") |
| | | private BigDecimal quantity; |
| | | |
| | | @Schema(description = "追溯è¢ç IDï¼æ¯è¢æç»ä¿çå®é
è¢ç å
³è") |
| | | private Long bagCodeId; |
| | | |
| | | @Schema(description = "æå±æçç IDï¼é¨åæ£è´§æ¶ä»
表示è¢ç æ¥æº") |
| | | private Long palletCodeId; |
| | | |
| | | @Schema(description = "åºåè®°å½ID", example = "1") |
| | | private Long materialStockId; |
| | | |
| | |
| | | private Long id; |
| | | private String uuid; |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ç¬ç«äºå¯¹å¤è¿½æº¯ç ï¼äºç»´ç ç¼ç 纯ç å¼ã */ |
| | | private String businessCode; |
| | | private Integer bagNo; |
| | | private Long batchId; |
| | | private String batchCode; |
| | |
| | | * æ¹æ¬¡å·ï¼èªå¨çæï¼åç¼ + æ¥æ + 产线ç¼ç + æµæ°´å·ï¼è¡¥å½ä¸å¡ä¿¡æ¯åçæï¼ |
| | | */ |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | /** |
| | | * ä¸´æ¶æ¹æ¬¡ç ï¼ç å¼ï¼PB-8ä½å
¨å±åºå·ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼è¡¥å½æ£å¼ä¿¡æ¯åä»å¯æ«ç è¿½æº¯ï¼ |
| | | */ |
| | |
| | | * æçç ï¼æ¹æ¬¡å·-P+æçåºå·ï¼ç»å®æ¹æ¬¡åçæï¼ |
| | | */ |
| | | private String code; |
| | | /** å
é¨åºå
¥åºä¸å¡ç ï¼ä¿æç¨³å®ï¼ä¸éæ£å¼ç è¡¥å½æç»å®ååã */ |
| | | private String businessCode; |
| | | /** |
| | | * ä¸´æ¶æçç ï¼ç å¼ï¼PL-8ä½å
¨å±åºå·ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼ç»å®æ¹æ¬¡åä»å¯æ«ç è¿½æº¯ï¼ |
| | | */ |
| | | private String tempCode; |
| | | /** æçåç§ç¼å·ï¼çææé¦æ¬¡è£
è¢æ¶å¸¦å
¥ã */ |
| | | private Long itemId; |
| | | /** |
| | | * çäº§æ¹æ¬¡ç¼å· |
| | | * <p> |
| | |
| | | return selectOne(MesProBagCodeDO::getCode, code); |
| | | } |
| | | |
| | | /** å
鍿«ç å
¼å®¹ä¸å¡ç ä¸åå²è¿½æº¯ç ï¼å
¬ä¼è¿½æº¯ç»§ç»åªæ code æ¥è¯¢ã */ |
| | | default MesProBagCodeDO selectByScanCode(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProBagCodeDO>() |
| | | .and(q -> q.eq(MesProBagCodeDO::getBusinessCode, code).or().eq(MesProBagCodeDO::getCode, code))); |
| | | } |
| | | |
| | | default MesProBagCodeDO selectByScanCodeForUpdate(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProBagCodeDO>() |
| | | .and(q -> q.eq(MesProBagCodeDO::getBusinessCode, code).or().eq(MesProBagCodeDO::getCode, code)) |
| | | .last("FOR UPDATE")); |
| | | } |
| | | |
| | | default MesProBagCodeDO selectByCodeForUpdate(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProBagCodeDO>() |
| | | .eq(MesProBagCodeDO::getCode, code).last("FOR UPDATE")); |
| | |
| | | |
| | | /** æ¹æ¬¡æ´æ¹å
¥åºæ¶æ¹éæ è®°å
¶ä¸æªå
¥åºè¢ç ï¼å¹çï¼ï¼ä¿æè¢ç ä¸åºåç¶æä¸è´ */ |
| | | @Update("UPDATE mes_pro_bag_code SET inbound_time = NOW(), inbound_event_id = #{eventId}, inbound_line_code = #{lineCode}, update_time = NOW() " |
| | | + "WHERE deleted = 0 AND inbound_event_id IS NULL AND batch_id = #{batchId}") |
| | | + "WHERE deleted = 0 AND status <> 30 AND inbound_event_id IS NULL AND batch_id = #{batchId}") |
| | | int markInboundByBatchId(@Param("batchId") Long batchId, @Param("eventId") Long eventId, |
| | | @Param("lineCode") String lineCode); |
| | | |
| | | /** æ´æå
¥åºæ¶æ¹éæ è®°æç䏿ªå
¥åºè¢ç ï¼å¹çï¼ */ |
| | | @Update("UPDATE mes_pro_bag_code SET inbound_time = NOW(), inbound_event_id = #{eventId}, inbound_line_code = #{lineCode}, update_time = NOW() " |
| | | + "WHERE deleted = 0 AND inbound_event_id IS NULL AND pallet_id = #{palletId}") |
| | | + "WHERE deleted = 0 AND status <> 30 AND inbound_event_id IS NULL AND pallet_id = #{palletId}") |
| | | int markInboundByPalletId(@Param("palletId") Long palletId, @Param("eventId") Long eventId, |
| | | @Param("lineCode") String lineCode); |
| | | |
| | |
| | | default List<MesProBagCodeDO> selectListByPalletId(Long palletId) { |
| | | return selectList(MesProBagCodeDO::getPalletId, palletId); |
| | | } |
| | | default List<MesProBagCodeDO> selectListByPalletIdForUpdate(Long palletId) { |
| | | return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>().eq(MesProBagCodeDO::getPalletId, palletId).orderByAsc(MesProBagCodeDO::getId).last("FOR UPDATE")); |
| | | } |
| | | default List<MesProBagCodeDO> selectListByBatchIdForUpdate(Long batchId) { |
| | | return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>().eq(MesProBagCodeDO::getBatchId, batchId).orderByAsc(MesProBagCodeDO::getId).last("FOR UPDATE")); |
| | | } |
| | | |
| | | default PageResult<MesProBagCodeDO> selectPage(MesProBagCodePageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<MesProBagCodeDO>() |
| | | .likeIfPresent(MesProBagCodeDO::getCode, reqVO.getCode()) |
| | | .likeIfPresent(MesProBagCodeDO::getBusinessCode, reqVO.getBusinessCode()) |
| | | .eqIfPresent(MesProBagCodeDO::getBatchId, reqVO.getBatchId()) |
| | | .likeIfPresent(MesProBagCodeDO::getBatchCode, reqVO.getBatchCode()) |
| | | .eqIfPresent(MesProBagCodeDO::getUuid, reqVO.getUuid()) |
| | |
| | | .eqIfPresent(MesProBagCodeDO::getStatus, reqVO.getStatus()) |
| | | .betweenIfPresent(MesProBagCodeDO::getCreateTime, reqVO.getCreateTime()) |
| | | .orderByDesc(MesProBagCodeDO::getCreateTime) |
| | | .orderByDesc(MesProBagCodeDO::getId)); |
| | | .orderByDesc(MesProBagCodeDO::getId) |
| | | .and(cn.hutool.core.util.StrUtil.isNotBlank(reqVO.getCode()), q -> |
| | | q.like(MesProBagCodeDO::getCode, reqVO.getCode()).or() |
| | | .like(MesProBagCodeDO::getBusinessCode, reqVO.getCode()))); |
| | | } |
| | | |
| | | /** |
| | |
| | | "AND deleted = 0 AND status <> 30") |
| | | int selectEffectiveCountByBatchId(@Param("batchId") Long batchId); |
| | | |
| | | @Select("SELECT COUNT(*) FROM mes_pro_bag_code WHERE deleted = 0 AND status <> 30 " |
| | | + "AND inbound_event_id IS NULL AND batch_id = #{batchId}") |
| | | int selectPendingInboundCountByBatchId(@Param("batchId") Long batchId); |
| | | |
| | | @Select("SELECT COUNT(*) FROM mes_pro_bag_code WHERE deleted = 0 AND status <> 30 " |
| | | + "AND inbound_event_id IS NULL AND pallet_id = #{palletId}") |
| | | int selectPendingInboundCountByPalletId(@Param("palletId") Long palletId); |
| | | |
| | | /** |
| | | * æ¹éæ 记为已导åºå°å·ï¼ä»
æ´æ°å¤äºç®æ åç¶æçè®°å½ï¼ |
| | | * |
| | |
| | | return selectOne(MesProBatchDO::getCode, code); |
| | | } |
| | | |
| | | default MesProBatchDO selectByScanCode(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProBatchDO>().and(q -> |
| | | q.eq(MesProBatchDO::getBusinessCode, code).or().eq(MesProBatchDO::getCode, code) |
| | | .or().eq(MesProBatchDO::getTempCode, code))); |
| | | } |
| | | |
| | | default MesProBatchDO selectByScanCodeForUpdate(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProBatchDO>().and(q -> |
| | | q.eq(MesProBatchDO::getBusinessCode, code).or().eq(MesProBatchDO::getCode, code) |
| | | .or().eq(MesProBatchDO::getTempCode, code)).last("FOR UPDATE")); |
| | | } |
| | | |
| | | default MesProBatchDO selectByTempCode(String tempCode) { |
| | | return selectOne(MesProBatchDO::getTempCode, tempCode); |
| | | } |
| | | |
| | | /** çæä¸´æ¶æ¹æ¬¡ç ç¨çå
¨å±åºå·ï¼è§£æ temp_code çæ°ååç¼ï¼ä¸åå¨åè¿å 0ã |
| | | * ä¸è½è¿æ»¤ deletedï¼å¯ä¸ç´¢å¼ uk_temp_code 对é»è¾å é¤è¡ä»çæï¼å¿½ç¥è½¯å ä¼å¯¼è´åºå·åéãç å¼éå¤ */ |
| | | @Select("SELECT IFNULL(MAX(CAST(SUBSTRING(temp_code, 4) AS UNSIGNED)), 0) FROM mes_pro_batch") |
| | | Integer selectMaxTempCodeSeq(); |
| | | |
| | | default Long selectCountByLineId(Long lineId) { |
| | | return selectCount(MesProBatchDO::getLineId, lineId); |
| | |
| | | int markBatchInbound(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("eventId") Long eventId, |
| | | @org.apache.ibatis.annotations.Param("lineCode") String lineCode, @org.apache.ibatis.annotations.Param("bagQuantity") Integer bagQuantity); |
| | | |
| | | /** 以ææè¢ç çå
¥åºæ 记为åï¼æ··æ«ãéè¯åä½åºè¢ä¸å½±å宿夿ãè°ç¨æ¹å·²é宿¹æ¬¡ã */ |
| | | @org.apache.ibatis.annotations.Update("UPDATE mes_pro_batch SET inbound_time = NOW(), inbound_event_id = #{eventId}, " |
| | | + "inbound_device_code = #{deviceCode}, inbound_line_code = #{lineCode}, " |
| | | + "inbound_bag_quantity = (SELECT COUNT(*) FROM mes_pro_bag_code b WHERE b.batch_id = #{id} " |
| | | + "AND b.deleted = 0 AND b.status <> 30 AND b.inbound_event_id IS NOT NULL), " |
| | | + "status = CASE WHEN EXISTS (SELECT 1 FROM mes_pro_bag_code b WHERE b.batch_id = #{id} AND b.deleted = 0 AND b.status <> 30) " |
| | | + "AND NOT EXISTS (SELECT 1 FROM mes_pro_bag_code b WHERE b.batch_id = #{id} AND b.deleted = 0 AND b.status <> 30 AND b.inbound_event_id IS NULL) " |
| | | + "THEN 30 ELSE status END, update_time = NOW() WHERE id = #{id} AND deleted = 0 AND status <> 40") |
| | | int refreshInboundProgress(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("eventId") Long eventId, |
| | | @org.apache.ibatis.annotations.Param("deviceCode") String deviceCode, @org.apache.ibatis.annotations.Param("lineCode") String lineCode); |
| | | |
| | | default PageResult<MesProBatchDO> selectPage(MesProBatchPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<MesProBatchDO>() |
| | | .likeIfPresent(MesProBatchDO::getCode, reqVO.getCode()) |
| | | .likeIfPresent(MesProBatchDO::getBusinessCode, reqVO.getBusinessCode()) |
| | | .likeIfPresent(MesProBatchDO::getTempCode, reqVO.getTempCode()) |
| | | .eqIfPresent(MesProBatchDO::getLineId, reqVO.getLineId()) |
| | | .likeIfPresent(MesProBatchDO::getLineCode, reqVO.getLineCode()) |
| | |
| | | .eqIfPresent(MesProBatchDO::getStatus, reqVO.getStatus()) |
| | | .betweenIfPresent(MesProBatchDO::getProduceDate, reqVO.getProduceDate()) |
| | | .betweenIfPresent(MesProBatchDO::getCreateTime, reqVO.getCreateTime()) |
| | | .orderByDesc(MesProBatchDO::getId)); |
| | | .orderByDesc(MesProBatchDO::getId) |
| | | .and(cn.hutool.core.util.StrUtil.isNotBlank(reqVO.getCode()), q -> |
| | | q.like(MesProBatchDO::getCode, reqVO.getCode()).or() |
| | | .like(MesProBatchDO::getBusinessCode, reqVO.getCode()).or() |
| | | .like(MesProBatchDO::getTempCode, reqVO.getCode()))); |
| | | } |
| | | |
| | | } |
| | |
| | | return selectOne(MesProPalletDO::getCode, code); |
| | | } |
| | | |
| | | default MesProPalletDO selectByScanCode(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProPalletDO>().and(q -> |
| | | q.eq(MesProPalletDO::getBusinessCode, code).or().eq(MesProPalletDO::getCode, code) |
| | | .or().eq(MesProPalletDO::getTempCode, code))); |
| | | } |
| | | |
| | | default MesProPalletDO selectByScanCodeForUpdate(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesProPalletDO>().and(q -> |
| | | q.eq(MesProPalletDO::getBusinessCode, code).or().eq(MesProPalletDO::getCode, code) |
| | | .or().eq(MesProPalletDO::getTempCode, code)).last("FOR UPDATE")); |
| | | } |
| | | |
| | | default MesProPalletDO selectByTempCode(String tempCode) { |
| | | return selectOne(MesProPalletDO::getTempCode, tempCode); |
| | | } |
| | | |
| | | /** çæä¸´æ¶æçç ç¨çå
¨å±åºå·ï¼è§£æ temp_code çæ°ååç¼ï¼ä¸åå¨åè¿å 0ã |
| | | * ä¸è½è¿æ»¤ deletedï¼å¯ä¸ç´¢å¼ uk_temp_code 对é»è¾å é¤è¡ä»çæï¼å¿½ç¥è½¯å ä¼å¯¼è´åºå·åéãç å¼éå¤ */ |
| | | @Select("SELECT IFNULL(MAX(CAST(SUBSTRING(temp_code, 4) AS UNSIGNED)), 0) FROM mes_pro_pallet") |
| | | Integer selectMaxTempCodeSeq(); |
| | | |
| | | @Select("SELECT * FROM mes_pro_pallet WHERE id = #{id} AND deleted = 0 FOR UPDATE") |
| | | MesProPalletDO selectByIdForUpdate(@Param("id") Long id); |
| | |
| | | |
| | | default PageResult<MesProPalletDO> selectPage(MesProPalletPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<MesProPalletDO>() |
| | | .likeIfPresent(MesProPalletDO::getCode, reqVO.getCode()) |
| | | .likeIfPresent(MesProPalletDO::getBusinessCode, reqVO.getBusinessCode()) |
| | | .likeIfPresent(MesProPalletDO::getTempCode, reqVO.getTempCode()) |
| | | .eqIfPresent(MesProPalletDO::getBatchId, reqVO.getBatchId()) |
| | | .likeIfPresent(MesProPalletDO::getBatchCode, reqVO.getBatchCode()) |
| | | .eqIfPresent(MesProPalletDO::getStatus, reqVO.getStatus()) |
| | | .betweenIfPresent(MesProPalletDO::getCreateTime, reqVO.getCreateTime()) |
| | | .orderByDesc(MesProPalletDO::getId)); |
| | | .orderByDesc(MesProPalletDO::getId) |
| | | .and(cn.hutool.core.util.StrUtil.isNotBlank(reqVO.getCode()), q -> |
| | | q.like(MesProPalletDO::getCode, reqVO.getCode()).or() |
| | | .like(MesProPalletDO::getBusinessCode, reqVO.getCode()).or() |
| | | .like(MesProPalletDO::getTempCode, reqVO.getCode()))); |
| | | } |
| | | |
| | | /** |
| | |
| | | + "WHERE id = #{id} AND deleted = 0 AND inbound_event_id IS NULL") |
| | | int markInbound(@Param("id") Long id, @Param("eventId") Long eventId); |
| | | |
| | | /** åè¢ãæ´æãæ´æ¹å
¥åºåï¼åªæææè¢ç å
¨é¨å
¥åºçæçæåæ¥æ è®°å·²å
¥åºã */ |
| | | @org.apache.ibatis.annotations.Update("UPDATE mes_pro_pallet p SET inbound_event_id = #{eventId}, inbound_time = NOW(), update_time = NOW() " |
| | | + "WHERE p.batch_id = #{batchId} AND p.deleted = 0 AND p.status <> 20 AND p.inbound_event_id IS NULL " |
| | | + "AND EXISTS (SELECT 1 FROM mes_pro_bag_code b WHERE b.pallet_id = p.id AND b.deleted = 0 AND b.status <> 30) " |
| | | + "AND NOT EXISTS (SELECT 1 FROM mes_pro_bag_code b WHERE b.pallet_id = p.id AND b.deleted = 0 AND b.status <> 30 AND b.inbound_event_id IS NULL)") |
| | | int markCompletedInboundByBatchId(@Param("batchId") Long batchId, @Param("eventId") Long eventId); |
| | | |
| | | @org.apache.ibatis.annotations.Update("UPDATE mes_pro_pallet SET sales_id = NULL, outbound_time = NULL, update_time = NOW() " |
| | | + "WHERE id = #{id} AND deleted = 0") |
| | | void clearOutbound(@Param("id") Long id); |
| | |
| | | default PageResult<MesProScanEventDO> selectPage(MesProScanEventPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<MesProScanEventDO>() |
| | | .eqIfPresent(MesProScanEventDO::getEventId, reqVO.getEventId()) |
| | | .eqIfPresent(MesProScanEventDO::getCreator, reqVO.getCreator()) |
| | | .betweenIfPresent(MesProScanEventDO::getReceivedTime, reqVO.getReceivedTime()) |
| | | .likeIfPresent(MesProScanEventDO::getBagCode, reqVO.getBagCode()) |
| | | .likeIfPresent(MesProScanEventDO::getBatchCode, reqVO.getBatchCode()) |
| | | .eqIfPresent(MesProScanEventDO::getDeviceCode, reqVO.getDeviceCode()) |
| | | .eqIfPresent(MesProScanEventDO::getStatus, reqVO.getStatus()) |
| | | .eqIfPresent(MesProScanEventDO::getErrorCode, reqVO.getErrorCode()) |
| | | .orderByDesc(MesProScanEventDO::getEventTime)); |
| | | .orderByDesc(MesProScanEventDO::getReceivedTime, MesProScanEventDO::getId)); |
| | | } |
| | | |
| | | default MesProScanEventDO selectLatest(String deviceCode) { |
| | |
| | | default MesQcSeedQualityDO selectByCode(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code)); |
| | | } |
| | | default List<MesQcSeedQualityDO> selectListByCodes(java.util.Collection<String> codes) { |
| | | if (codes.isEmpty()) return List.of(); |
| | | return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>().in(MesQcSeedQualityDO::getCode, codes)); |
| | | } |
| | | default List<MesQcSeedQualityDO> selectListByCodesForUpdate(java.util.Collection<String> codes) { |
| | | if (codes.isEmpty()) return List.of(); |
| | | return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>().in(MesQcSeedQualityDO::getCode, codes).last("FOR UPDATE")); |
| | | } |
| | | default MesQcSeedQualityDO selectByCodeForUpdate(String code) { |
| | | return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code).last("FOR UPDATE")); |
| | | } |
| | | |
| | | default List<MesQcSeedQualityDO> selectListByBatchCode(String batchCode) { |
| | | return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>() |
| | |
| | | @Mapper |
| | | public interface MesWmMaterialStockBagMapper extends BaseMapperX<MesWmMaterialStockBagDO> { |
| | | |
| | | default List<MesWmMaterialStockBagDO> selectByBagIds(Collection<Long> ids, boolean lock) { |
| | | if (ids.isEmpty()) return List.of(); |
| | | var query = new LambdaQueryWrapperX<MesWmMaterialStockBagDO>() |
| | | .in(MesWmMaterialStockBagDO::getBagCodeId, ids).orderByAsc(MesWmMaterialStockBagDO::getBagCodeId); |
| | | if (lock) query.last("FOR UPDATE"); |
| | | return selectList(query); |
| | | } |
| | | |
| | | default PageResult<MesWmMaterialStockBagDO> selectPage(MesWmMaterialStockBagPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<MesWmMaterialStockBagDO>() |
| | | .eq(MesWmMaterialStockBagDO::getMaterialStockId, reqVO.getMaterialStockId()) |
| | |
| | | @Mapper |
| | | public interface MesWmProductSalesDetailMapper extends BaseMapperX<MesWmProductSalesDetailDO> { |
| | | |
| | | // ä»
æªåæ¶ä¸å°æªå®æçåºåºåå ç¨è¢ç ãæ§æ´ææç»ä¹åä¸éå 夿ã |
| | | @org.apache.ibatis.annotations.Select(""" |
| | | SELECT d.* FROM mes_wm_product_sales_detail d |
| | | JOIN mes_wm_product_sales s ON s.id = d.sales_id |
| | | WHERE d.deleted = 0 AND s.deleted = 0 AND s.status IN (2, 10, 3) |
| | | AND d.batch_code = #{batchCode} |
| | | """) |
| | | List<MesWmProductSalesDetailDO> selectPickingByBatchCode(String batchCode); |
| | | |
| | | @org.apache.ibatis.annotations.Select(""" |
| | | SELECT d.* FROM mes_wm_product_sales_detail d |
| | | JOIN mes_wm_product_sales s ON s.id = d.sales_id |
| | | WHERE d.deleted = 0 AND s.deleted = 0 AND s.status IN (2, 10, 3) |
| | | AND d.batch_code = #{batchCode} FOR UPDATE |
| | | """) |
| | | List<MesWmProductSalesDetailDO> selectPickingByBatchCodeForUpdate(String batchCode); |
| | | |
| | | default List<MesWmProductSalesDetailDO> selectListByLineId(Long lineId) { |
| | | return selectList(MesWmProductSalesDetailDO::getLineId, lineId); |
| | | } |
| | |
| | | default MesWmProductSalesDetailDO selectBySalesIdAndPalletCodeId(Long salesId, Long palletCodeId) { |
| | | return selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesWmProductSalesDetailDO>() |
| | | .eq(MesWmProductSalesDetailDO::getSalesId, salesId) |
| | | .eq(MesWmProductSalesDetailDO::getPalletCodeId, palletCodeId)); |
| | | .eq(MesWmProductSalesDetailDO::getPalletCodeId, palletCodeId).orderByAsc(MesWmProductSalesDetailDO::getId).last("LIMIT 1")); |
| | | } |
| | | |
| | | default void deleteByLineId(Long lineId) { |
| | |
| | | @Mapper |
| | | public interface MesWmProductSalesMapper extends BaseMapperX<MesWmProductSalesDO> { |
| | | |
| | | default MesWmProductSalesDO selectByIdForUpdate(Long id) { |
| | | return selectOne(new LambdaQueryWrapperX<MesWmProductSalesDO>().eq(MesWmProductSalesDO::getId, id).last("FOR UPDATE")); |
| | | } |
| | | |
| | | default PageResult<MesWmProductSalesDO> selectPage(MesWmProductSalesPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<MesWmProductSalesDO>() |
| | | .likeIfPresent(MesWmProductSalesDO::getCode, reqVO.getCode()) |
| | |
| | | private MesProQrLabelBuilder() { |
| | | } |
| | | |
| | | /** å
é¨ä¸å¡æ ç¾ç¬ç«äºå
¬ä¼è¿½æº¯æ ç¾ï¼ä¸æ¼æ¥ H5 ç½åã */ |
| | | public static QrLabelInfo buildBagBusinessCodeLabel(MesProBagCodeDO bag) { |
| | | return buildBusinessCodeLabel(bag.getBusinessCode()); |
| | | } |
| | | |
| | | public static QrLabelInfo buildPalletBusinessCodeLabel(MesProPalletDO pallet) { |
| | | return buildBusinessCodeLabel(pallet.getBusinessCode()); |
| | | } |
| | | |
| | | public static QrLabelInfo buildBatchBusinessCodeLabel(MesProBatchDO batch) { |
| | | return buildBusinessCodeLabel(batch.getBusinessCode()); |
| | | } |
| | | |
| | | private static QrLabelInfo buildBusinessCodeLabel(String code) { |
| | | if (code == null || !code.matches("^[0-9]{16}$")) { |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "ç¼ºå°ææä¸å¡ç ï¼è¯·å
è¡¥çæä¸å¡ç "); |
| | | } |
| | | return QrLabelInfo.builder().fileName(code).code(code).keyValues(new LinkedHashMap<>()).build(); |
| | | } |
| | | |
| | | /** |
| | | * åå
è¯å«ä»£ç æ ¼å¼ï¼1~20 使°åæåæ¯æ°åç»åï¼å½æ è¦æ±ï¼ä¸å«è¿å符ç符å·ï¼ |
| | | */ |
| | |
| | | */ |
| | | MesProBagCodeDO getBagCodeByCode(String code); |
| | | |
| | | /** å
鍿¥è¯¢è¯å«ä¸å¡ç ååå²è¿½æº¯ç ï¼ä¸ç¨äºå
¬ä¼è¿½æº¯å
¥å£ã */ |
| | | MesProBagCodeDO getBagCodeByScanCode(String code); |
| | | |
| | | /** è¡¥çæåå²è¢ç ä¸å¡ç ï¼å·²æç ä¿æä¸åï¼è¿åæ°çææ°éã */ |
| | | int generateBusinessCodes(List<Long> ids); |
| | | |
| | | /** |
| | | * æ¥è¯¢è¢ç æ¿æ¢è®°å½ |
| | | */ |
| | |
| | | |
| | | @Resource |
| | | private MesCodeRedisDAO codeRedisDao; |
| | | @Resource |
| | | private cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService integrityService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | for (int i = 0; i < palletCount; i++) { |
| | | pallets.add(new MesProPalletDO() |
| | | .setTempCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setBagCount(perPallet) |
| | | .setAutoGenerated(true) |
| | | .setItemId(item.getId()) |
| | | .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())); |
| | | } |
| | | palletMapper.insertBatch(pallets); |
| | |
| | | bagCodes.add(new MesProBagCodeDO() |
| | | .setUuid(uuid) |
| | | .setCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setBagNo(bagNo) |
| | | .setItemId(item.getId()) |
| | | .setItemCode(item.getCode()) |
| | |
| | | bagCodes.add(new MesProBagCodeDO() |
| | | .setUuid(uuid) |
| | | .setCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setBagNo(bagNo) |
| | | .setItemId(item.getId()) |
| | | .setItemCode(item.getCode()) |
| | |
| | | } |
| | | // å½å
¥çè¢ç å¿
é¡»å±äºåä¸åç§ï¼ä¸ä¸æ¹æ¬¡å·²æè¢ç åç§ä¸è´ï¼åå²ç©ºç½è¢ç item 为空ä¸å䏿 ¡éªï¼ |
| | | Set<Long> itemIds = new LinkedHashSet<>(); |
| | | if (batch.getItemId() != null) itemIds.add(batch.getItemId()); |
| | | bagCodes.stream().map(MesProBagCodeDO::getItemId).filter(Objects::nonNull).forEach(itemIds::add); |
| | | itemIds.addAll(bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId())); |
| | | for (Long palletId : bagCodes.stream().map(MesProBagCodeDO::getPalletId).filter(Objects::nonNull).distinct().sorted().toList()) { |
| | | MesProPalletDO pallet = palletMapper.selectByIdForUpdate(palletId); |
| | | if (pallet == null) throw exception(PRO_PALLET_NOT_EXISTS); |
| | | if (pallet.getBatchId() != null && !pallet.getBatchId().equals(batch.getId())) |
| | | throw exception(PRO_BAG_CODE_BIND_PALLET_MISMATCH); |
| | | Long palletItem = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.resolve(pallet, bagCodeMapper.selectListByPalletId(palletId)); |
| | | if (palletItem != null) itemIds.add(palletItem); |
| | | } |
| | | if (itemIds.size() > 1) { |
| | | throw exception(PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH); |
| | | } |
| | | if (!itemIds.isEmpty()) batch.setItemId(itemIds.iterator().next()); |
| | | if (bagCodeMapper.bindBatchByIds(ids, batch.getId(), batch.getCode(), |
| | | List.of(MesProBagCodeStatusEnum.GENERATED.getStatus(), |
| | | MesProBagCodeStatusEnum.PRINTED.getStatus())) != ids.size()) { |
| | | throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED); |
| | | } |
| | | batchMapper.updateById(new MesProBatchDO().setId(batch.getId()) |
| | | batchMapper.updateById(new MesProBatchDO().setId(batch.getId()).setItemId(batch.getItemId()) |
| | | .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId()))); |
| | | // 级èï¼æ¬ç»è¢ç ç»å®çæªå
¥æ¹æ¬¡æçï¼åé
æ£å¼æçç å¹¶ç»å®æ¹æ¬¡ |
| | | bindPalletsToBatch(bagCodes, batch); |
| | | integrityService.recordBatchIntegrity(batch.getId()); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | String palletCode = codeRedisDao.generate(); |
| | | palletMapper.updateById(new MesProPalletDO().setId(palletId) |
| | | .setItemId(batch.getItemId()) |
| | | .setCode(palletCode) |
| | | .setBatchId(batch.getId()) |
| | | .setBatchCode(batch.getCode()) |
| | |
| | | MesProBagCodeDO newBagCode = new MesProBagCodeDO() |
| | | .setUuid(replaceUuid) |
| | | .setCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setBagNo(newBagNo) |
| | | .setBatchId(batch.getId()) |
| | | .setBatchCode(batch.getCode()) |
| | |
| | | } |
| | | // 2. éå®å¹¶æ ¡éªè¢ç |
| | | List<MesProBagCodeDO> bagCodes = validateBagCodesForBind(bindReqVO.getIds(), pallet, true); |
| | | Long itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.resolve(pallet, |
| | | bagCodeMapper.selectListByPalletId(pallet.getId())); |
| | | for (MesProBagCodeDO bag : bagCodes) { |
| | | if (bag.getItemId() == null) throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "è¢ç 缺å°åç§"); |
| | | itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, bag.getItemId()); |
| | | } |
| | | palletMapper.updateById(new MesProPalletDO().setId(pallet.getId()).setItemId(itemId)); |
| | | // 3. ç»å®è¢ç å¹¶ç´¯å æçå·²è£
è¢æ°ï¼æçæ 容éä¸éï¼ |
| | | int currentCount = pallet.getBagCount() == null ? 0 : pallet.getBagCount(); |
| | | bagCodeMapper.bindPalletByIds(bindReqVO.getIds(), pallet.getId(), pallet.getCode()); |
| | | bagCodeMapper.bindPalletByIds(bindReqVO.getIds(), pallet.getId(), StrUtil.isNotBlank(pallet.getCode()) ? pallet.getCode() : pallet.getTempCode()); |
| | | palletService.updatePalletBagCount(pallet.getId(), currentCount + bagCodes.size()); |
| | | } |
| | | |
| | |
| | | @Override |
| | | public MesProBagCodeDO getBagCodeByCode(String code) { |
| | | return bagCodeMapper.selectByCode(code); |
| | | } |
| | | |
| | | @Override |
| | | public MesProBagCodeDO getBagCodeByScanCode(String code) { |
| | | return bagCodeMapper.selectByScanCode(code); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int generateBusinessCodes(List<Long> ids) { |
| | | if (CollUtil.isEmpty(ids) || ids.size() > 200 || ids.stream().anyMatch(Objects::isNull)) { |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "è¯·éæ© 1 è³ 200 æ¡è¢ç "); |
| | | } |
| | | List<Long> uniqueIds = ids.stream().distinct().sorted().toList(); |
| | | List<MesProBagCodeDO> bags = bagCodeMapper.selectListByIdsForUpdate(uniqueIds); |
| | | if (bags.size() != uniqueIds.size()) throw exception(PRO_BAG_CODE_NOT_EXISTS); |
| | | if (bags.stream().anyMatch(bag -> MesProBagCodeStatusEnum.CANCELED.getStatus().equals(bag.getStatus()))) { |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "å·²ä½åºè¢ç ä¸å¯è¡¥çæä¸å¡ç "); |
| | | } |
| | | int count = 0; |
| | | for (MesProBagCodeDO bag : bags) { |
| | | if (StrUtil.isNotBlank(bag.getBusinessCode())) continue; |
| | | bagCodeMapper.updateById(new MesProBagCodeDO().setId(bag.getId()).setBusinessCode(codeRedisDao.generate())); |
| | | count++; |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | @Override |
| | |
| | | throw exception(PRO_BAG_CODE_NOT_EXISTS); |
| | | } |
| | | for (MesProBagCodeDO bagCode : bagCodes) { |
| | | if (!pallet.getBatchId().equals(bagCode.getBatchId())) { |
| | | if (!Objects.equals(pallet.getBatchId(), bagCode.getBatchId())) { |
| | | throw exception(PRO_BAG_CODE_BIND_PALLET_MISMATCH); |
| | | } |
| | | if (bagCode.getPalletId() != null) { |
| | |
| | | */ |
| | | MesProBatchDO getBatchByCode(String code); |
| | | |
| | | /** å
鍿«ç è¯å«ä¸å¡ç åå岿£å¼/临æ¶ç ã */ |
| | | MesProBatchDO getBatchByScanCode(String code); |
| | | |
| | | int generateBusinessCodes(java.util.List<Long> ids); |
| | | |
| | | /** |
| | | * è·å¾çäº§æ¹æ¬¡å页 |
| | | * |
| | |
| | | // 1. æ ¡éªå
³èæ°æ®ï¼äº§çº¿ãç产人åï¼ |
| | | MesProLineDO line = lineService.validateLineExistsAndEnable(createReqVO.getLineId()); |
| | | validateRelations(createReqVO); |
| | | createReqVO.setItemId(resolveBindingItem(null, createReqVO)); |
| | | |
| | | // 2. çææ¹æ¬¡å·ï¼ç¼ç è§åï¼åç¼ + æ¥æ + 产线ç¼ç + æµæ°´å·ï¼ |
| | | String code = generateBatchCode(line.getCode()); |
| | |
| | | boolean bindPallets = CollUtil.isNotEmpty(createReqVO.getPalletIds()); |
| | | MesProBatchDO batch = BeanUtils.toBean(createReqVO, MesProBatchDO.class) |
| | | .setCode(code) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setLineCode(line.getCode()) |
| | | .setLineName(line.getName()) |
| | | .setBagQuantity(0) |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public List<MesProBatchDO> preGenerateBatch(MesProBatchPreGenerateReqVO reqVO) { |
| | | // 1. çæä¸´æ¶æ¹æ¬¡ç ï¼å
¨å± 8 ä½åºå·ï¼æ ¼å¼ PB-00000001ï¼ |
| | | int startSeq = batchMapper.selectMaxTempCodeSeq() + 1; |
| | | // 1. éè¿ç»ä¸ç¼ç çæå¨åé
临æ¶ç åä¸å¡ç ï¼ä¸åè§£æåå²ä¸´æ¶ç åºå·ã |
| | | List<MesProBatchDO> list = new ArrayList<>(reqVO.getCount()); |
| | | for (int i = 0; i < reqVO.getCount(); i++) { |
| | | list.add(new MesProBatchDO() |
| | | .setTempCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setBagQuantity(0) |
| | | .setPalletQuantity(0) |
| | | .setStatus(MesProBatchStatusEnum.DRAFT.getStatus()) |
| | |
| | | // 2. æ ¡éªå
³èæ°æ®å¹¶çææ£å¼æ¹æ¬¡å· |
| | | MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId()); |
| | | validateRelations(fillReqVO); |
| | | fillReqVO.setItemId(resolveBindingItem(batch, fillReqVO)); |
| | | String code = generateBatchCode(line.getCode()); |
| | | // 3. è½åºä¸å¡ä¿¡æ¯ï¼ä¿çä¸´æ¶æ¹æ¬¡ç ç¨äºå岿«ç 追溯ï¼äº§çº¿ç¼ç /åç§°åä½åå¡«ï¼å¾éç»å®æçæ¶æ¹æ¬¡ç´æ¥è¿å
¥ç产ä¸ï¼ |
| | | boolean bindPallets = CollUtil.isNotEmpty(fillReqVO.getPalletIds()); |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateBatch(MesProBatchSaveReqVO updateReqVO) { |
| | | // 1. æ ¡éªåå¨ + ç¶æï¼è稿/çäº§ä¸æå
许修æ¹ï¼ |
| | | MesProBatchDO oldBatch = validateBatchExists(updateReqVO.getId()); |
| | | MesProBatchDO oldBatch = batchMapper.selectByIdForUpdate(updateReqVO.getId()); |
| | | if (oldBatch == null) throw exception(PRO_BATCH_NOT_EXISTS); |
| | | validateBatchEditable(oldBatch); |
| | | // 2. æ ¡éªå
³èæ°æ®ï¼ç产人åï¼ |
| | | validateRelations(updateReqVO); |
| | | updateReqVO.setItemId(resolveBindingItem(oldBatch, updateReqVO)); |
| | | |
| | | // 3. æªçææ£å¼æ¹æ¬¡å·æäº§çº¿ååæ¶éæ°çææ¹æ¬¡å·ï¼æ¹æ¬¡å·å
å«äº§çº¿ç¼ç è¦ç´ ï¼ |
| | | boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode()); |
| | |
| | | } |
| | | |
| | | @Override |
| | | public MesProBatchDO getBatchByScanCode(String code) { |
| | | return batchMapper.selectByScanCode(code); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int generateBusinessCodes(java.util.List<Long> ids) { |
| | | if (ids == null || ids.isEmpty() || ids.size() > 200 || ids.stream().anyMatch(java.util.Objects::isNull)) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "è¯·éæ© 1 è³ 200 æ¡è®°å½"); |
| | | java.util.List<MesProBatchDO> records = new java.util.ArrayList<>(); |
| | | for (Long id : ids.stream().distinct().sorted().toList()) { |
| | | MesProBatchDO record = batchMapper.selectByIdForUpdate(id); |
| | | if (record == null) throw exception(PRO_BATCH_NOT_EXISTS); |
| | | if (MesProBatchStatusEnum.CANCELED.getStatus().equals(record.getStatus())) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "å·²ä½åºè®°å½ä¸å¯è¡¥çæä¸å¡ç "); |
| | | records.add(record); |
| | | } |
| | | int count = 0; |
| | | for (MesProBatchDO record : records) { |
| | | if (StrUtil.isNotBlank(record.getBusinessCode())) continue; |
| | | batchMapper.updateById(new MesProBatchDO().setId(record.getId()).setBusinessCode(codeRedisDao.generate())); |
| | | count++; |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | @Override |
| | | public MesProBatchDO getBatchByCode(String code) { |
| | | MesProBatchDO batch = batchMapper.selectByCode(code); |
| | | if (batch == null) { |
| | |
| | | * æ¹æ¬¡ç»å®æçï¼å¯éï¼ï¼åäºå¡å
å¤ç¨æçç»å®æ¹æ¬¡é»è¾ï¼ |
| | | * çææ£å¼æçç 并级èå½å
¥æç䏿ªç»æ¹æ¬¡çè¢ç ï¼å¤±è´¥æ´ä½åæ» |
| | | */ |
| | | private Long resolveBindingItem(MesProBatchDO batch, MesProBatchSaveReqVO req) { |
| | | Long itemId = batch == null ? null : batch.getItemId(); |
| | | if (batch != null) { |
| | | for (Long existing : bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId())) |
| | | itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, existing); |
| | | for (var pallet : palletMapper.selectListByBatchId(batch.getId())) |
| | | itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, |
| | | cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.resolve(pallet, bagCodeMapper.selectListByPalletId(pallet.getId()))); |
| | | } |
| | | if (CollUtil.isNotEmpty(req.getPalletIds())) for (Long id : req.getPalletIds().stream().distinct().sorted().toList()) { |
| | | var pallet = palletMapper.selectByIdForUpdate(id); |
| | | if (pallet == null) throw exception(PRO_PALLET_NOT_EXISTS); |
| | | Long candidate = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.resolve(pallet, bagCodeMapper.selectListByPalletId(id)); |
| | | if (candidate == null) throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "æç缺å°åç§ï¼è¯·å
ç»å®ååç§è¢ç "); |
| | | itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, candidate); |
| | | } |
| | | // 请æ±éçåç§åªè½ä¸å·²ç»å®/å¾
ç»å®æçä¸è´ï¼ä¸è½éè¿ç¼è¾æ¹åæ¹æ¬¡åç§ã |
| | | if (req.getItemId() != null && !Objects.equals(req.getItemId(), itemId)) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "åç§ç±ç»å®æç带å
¥ï¼ä¸å¯ä¿®æ¹ä¸ºå
¶ä»åç§"); |
| | | return itemId; |
| | | } |
| | | |
| | | private void bindPalletsToBatch(Long batchId, List<Long> palletIds) { |
| | | if (CollUtil.isEmpty(palletIds)) { |
| | | return; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.pallet; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; |
| | | import java.util.*; |
| | | |
| | | /** æçåç§ä»¥æçè®°å½åææè¢ç å
±åæ ¡éªï¼å
¼å®¹å岿çã */ |
| | | public final class MesProPalletItemResolver { |
| | | private MesProPalletItemResolver() {} |
| | | public static Long resolve(MesProPalletDO pallet, List<MesProBagCodeDO> bags) { |
| | | Set<Long> items = new HashSet<>(); |
| | | if (pallet.getItemId() != null) items.add(pallet.getItemId()); |
| | | for (MesProBagCodeDO bag : bags) { |
| | | if (Integer.valueOf(30).equals(bag.getStatus())) continue; |
| | | if (bag.getItemId() == null) throw new ServiceException(400, "æçå
è¢ç 缺å°åç§ï¼è¯·å
å®ååç§ä¿¡æ¯"); |
| | | items.add(bag.getItemId()); |
| | | } |
| | | if (items.size() > 1) throw new ServiceException(400, "æçåè¢ç å¿
é¡»å±äºåä¸åç§"); |
| | | return items.isEmpty() ? null : items.iterator().next(); |
| | | } |
| | | public static Long merge(Long current, Long candidate) { |
| | | if (current != null && candidate != null && !current.equals(candidate)) |
| | | throw new ServiceException(400, "çäº§æ¹æ¬¡åªè½ç»å®ç¸ååç§çæçï¼ä¸é¡»ä¸æ¹æ¬¡åç§ä¸è´"); |
| | | return current == null ? candidate : current; |
| | | } |
| | | } |
| | |
| | | */ |
| | | MesProPalletDO getPalletByCode(String code); |
| | | |
| | | /** å
鍿«ç è¯å«ä¸å¡ç åå岿£å¼/临æ¶ç ã */ |
| | | MesProPalletDO getPalletByScanCode(String code); |
| | | |
| | | int generateBusinessCodes(java.util.List<Long> ids); |
| | | |
| | | /** |
| | | * è·å¾æçç å页 |
| | | * |
| | |
| | | |
| | | @Resource |
| | | private MesCodeRedisDAO codeRedisDao; |
| | | @Resource |
| | | private cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService integrityService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | int palletNo = startNo + i; |
| | | pallets.add(new MesProPalletDO() |
| | | .setCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setItemId(batch.getItemId()) |
| | | .setBatchId(batch.getId()) |
| | | .setBatchCode(batch.getCode()) |
| | | .setPalletNo(palletNo) |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public List<MesProPalletDO> preGeneratePallet(MesProPalletPreGenerateReqVO reqVO) { |
| | | // 1. çæä¸´æ¶æçç ï¼å
¨å± 8 ä½åºå·ï¼æ ¼å¼ PL-00000001ï¼ |
| | | int startSeq = palletMapper.selectMaxTempCodeSeq() + 1; |
| | | // 1. éè¿ç»ä¸ç¼ç çæå¨åé
临æ¶ç åä¸å¡ç ï¼ä¸åè§£æåå²ä¸´æ¶ç åºå·ã |
| | | List<MesProPalletDO> list = new ArrayList<>(reqVO.getCount()); |
| | | for (int i = 0; i < reqVO.getCount(); i++) { |
| | | list.add(new MesProPalletDO() |
| | | .setTempCode(codeRedisDao.generate()) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | | .setBagCount(0) |
| | | .setAutoGenerated(false) |
| | | .setStatus(MesProPalletStatusEnum.IN_USE.getStatus()) |
| | |
| | | } |
| | | pallets.add(pallet); |
| | | } |
| | | Long itemId = batch.getItemId(); |
| | | for (Long existing : bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId())) |
| | | itemId = MesProPalletItemResolver.merge(itemId, existing); |
| | | for (MesProPalletDO pallet : pallets) { |
| | | Long palletItem = MesProPalletItemResolver.resolve(pallet, bagCodeMapper.selectListByPalletId(pallet.getId())); |
| | | if (palletItem == null) throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "æç缺å°åç§ï¼è¯·å
ç»å®ååç§è¢ç "); |
| | | itemId = MesProPalletItemResolver.merge(itemId, palletItem); |
| | | } |
| | | // 3. 级èåå¤ï¼æç䏿ªç»æ¹æ¬¡çè¢ç éæçä¸èµ·å½å
¥æ¹æ¬¡ï¼åç§é¡»ä¸æ¹æ¬¡å·²æè¢ç ä¸è´ |
| | | List<MesProBagCodeDO> bags = bagCodeMapper.selectUnboundListByPalletIds(ids); |
| | | List<Long> bindBagIds = new ArrayList<>(); |
| | |
| | | } |
| | | if (!bindBagIds.isEmpty()) { |
| | | bagItemIds.addAll(bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId())); |
| | | if (itemId != null) bagItemIds.add(itemId); |
| | | if (bagItemIds.size() > 1) { |
| | | throw exception(PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH); |
| | | } |
| | |
| | | int startNo = palletMapper.selectMaxPalletNo(batch.getId()) + 1; |
| | | for (int i = 0; i < pallets.size(); i++) { |
| | | int palletNo = startNo + i; |
| | | String palletCode = codeRedisDao.generate(); |
| | | palletMapper.updateById(new MesProPalletDO() |
| | | .setId(pallets.get(i).getId()) |
| | | .setCode(codeRedisDao.generate()) |
| | | .setItemId(itemId) |
| | | .setCode(palletCode) |
| | | .setBatchId(batch.getId()) |
| | | .setBatchCode(batch.getCode()) |
| | | .setPalletNo(palletNo)); |
| | | // æçä¸è¢ç çå使çç ç±ä¸´æ¶ç æ´æ°ä¸ºæ£å¼ç |
| | | bagCodeMapper.updatePalletCodeByPalletId(pallets.get(i).getId(), |
| | | batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0')); |
| | | palletCode); |
| | | } |
| | | // 5. æçä¸è¢ç æ¹éç»å®æ¬æ¹æ¬¡ |
| | | if (!bindBagIds.isEmpty() && bagCodeMapper.bindBatchByIds(bindBagIds, batch.getId(), batch.getCode(), |
| | |
| | | MesProBagCodeStatusEnum.PRINTED.getStatus())) != bindBagIds.size()) { |
| | | throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED); |
| | | } |
| | | // 6. ååæ¹æ¬¡æçç æ°é䏿æè¢ç æ°é |
| | | batchMapper.updateById(new MesProBatchDO().setId(batch.getId()) |
| | | // 6. ååæ¹æ¬¡åç§ãæçç æ°é䏿æè¢ç æ°é |
| | | batchMapper.updateById(new MesProBatchDO().setId(batch.getId()).setItemId(itemId) |
| | | .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + pallets.size()) |
| | | .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId()))); |
| | | integrityService.recordBatchIntegrity(batch.getId()); |
| | | return pallets.size(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Override |
| | | public MesProPalletDO getPalletByScanCode(String code) { |
| | | return palletMapper.selectByScanCode(code); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int generateBusinessCodes(java.util.List<Long> ids) { |
| | | if (ids == null || ids.isEmpty() || ids.size() > 200 || ids.stream().anyMatch(java.util.Objects::isNull)) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "è¯·éæ© 1 è³ 200 æ¡è®°å½"); |
| | | java.util.List<MesProPalletDO> records = new java.util.ArrayList<>(); |
| | | for (Long id : ids.stream().distinct().sorted().toList()) { |
| | | MesProPalletDO record = palletMapper.selectByIdForUpdate(id); |
| | | if (record == null) throw exception(PRO_PALLET_NOT_EXISTS); |
| | | if (MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus())) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "å·²ä½åºè®°å½ä¸å¯è¡¥çæä¸å¡ç "); |
| | | records.add(record); |
| | | } |
| | | int count = 0; |
| | | for (MesProPalletDO record : records) { |
| | | if (StrUtil.isNotBlank(record.getBusinessCode())) continue; |
| | | palletMapper.updateById(new MesProPalletDO().setId(record.getId()).setBusinessCode(codeRedisDao.generate())); |
| | | count++; |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | @Override |
| | | public MesProPalletDO getPalletByCode(String code) { |
| | | MesProPalletDO pallet = palletMapper.selectByCode(code); |
| | | if (pallet == null) { |
| | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum; |
| | | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | |
| | | @Slf4j |
| | | @Service |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesProScanEventDO createInbound(MesProScanEventCreateReqVO req) { |
| | | String scanType = (req.getScanType() == null || req.getScanType().isBlank()) ? SCAN_TYPE_BAG : req.getScanType(); |
| | | if (!List.of(SCAN_TYPE_BAG, SCAN_TYPE_PALLET, SCAN_TYPE_BATCH).contains(scanType)) { |
| | | throw new ServiceException(400, "䏿¯æçæ«ç ç±»å"); |
| | | } |
| | | MesProScanEventDO existing = eventMapper.selectByEventIdForUpdate(req.getEventId()); |
| | | if (existing != null) { |
| | | boolean contentMatch = req.getBagCode().equals(existing.getBagCode()) |
| | | && (req.getDeviceCode() == null || req.getDeviceCode().equals(existing.getDeviceCode())); |
| | | && Objects.equals(req.getDeviceCode(), existing.getDeviceCode()) |
| | | && Objects.equals(req.getLineCode(), existing.getLineCode()) |
| | | && Objects.equals(scanType, existing.getScanType()) |
| | | && Objects.equals(req.getLocationId(), existing.getLocationId()) |
| | | && Objects.equals(req.getAreaId(), existing.getAreaId()) |
| | | && (!SCAN_TYPE_BAG.equals(scanType) || (existing.getQuantity() != null |
| | | && req.getQuantity().compareTo(existing.getQuantity()) == 0)) |
| | | && Objects.equals(String.valueOf(SecurityFrameworkUtils.getLoginUserId()), existing.getCreator()); |
| | | if (!contentMatch) { |
| | | return failed(existing, "MES_SCAN_EVENT_ID_CONFLICT", "äºä»¶å·å·²åå¨ä½å
容ä¸ä¸è´"); |
| | | throw new ServiceException(409, "äºä»¶å·å·²åå¨ä½å
容ææä½äººä¸ä¸è´ï¼è¯·æ ¸å®åæä½"); |
| | | } |
| | | return existing; |
| | | } |
| | | String scanType = (req.getScanType() == null || req.getScanType().isBlank()) ? SCAN_TYPE_BAG : req.getScanType(); |
| | | MesProScanEventDO event = new MesProScanEventDO(); |
| | | event.setEventId(req.getEventId()).setBagCode(req.getBagCode()).setScanType(scanType) |
| | | .setQuantity(req.getQuantity()).setDeviceCode(req.getDeviceCode()).setLineCode(req.getLineCode()) |
| | | .setRemark(req.getRemark()).setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now()); |
| | | .setRemark(req.getRemark()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId()) |
| | | .setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now()); |
| | | eventMapper.insert(event); |
| | | // 1. ææ«ç ç±»åè§£ææ«ç ç®æ ä¸å
¥åºæ°éï¼æçæå·²è£
è¢æ°ãæ¹æ¬¡ææ¹æ¬¡è¢æ°æ´æ¹/æ´æå
¥åºï¼ |
| | | Long targetId; |
| | |
| | | Long bagCodeId = null; |
| | | String bagCode = null; |
| | | if (SCAN_TYPE_PALLET.equals(scanType)) { |
| | | MesProPalletDO pallet = palletMapper.selectByCode(req.getBagCode()); |
| | | if (pallet == null) { |
| | | pallet = palletMapper.selectByTempCode(req.getBagCode()); |
| | | } |
| | | MesProPalletDO pallet = palletMapper.selectByScanCodeForUpdate(req.getBagCode()); |
| | | if (pallet == null) return failed(event, "MES_SCAN_PALLET_NOT_EXISTS", "æçç ä¸åå¨"); |
| | | if (MesProPalletStatusEnum.CANCELED.getStatus().equals(pallet.getStatus())) |
| | | return failed(event, "MES_SCAN_PALLET_CANCELED", "æçå·²ä½åºï¼ä¸å¯å
¥åº"); |
| | | if (pallet.getInboundEventId() != null) return failed(event, "MES_SCAN_PALLET_ALREADY_INBOUND", "æçå·²å
¥åº"); |
| | | batch = batchMapper.selectByIdForUpdate(pallet.getBatchId()); |
| | | if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "çäº§æ¹æ¬¡ä¸åå¨"); |
| | | batchId = batch.getId(); |
| | | targetId = pallet.getId(); |
| | | quantity = pallet.getBagCount() != null && pallet.getBagCount() > 0 |
| | | ? BigDecimal.valueOf(pallet.getBagCount()) |
| | | : BigDecimal.valueOf(bagCodeMapper.selectCountByPalletId(pallet.getId())); |
| | | quantity = BigDecimal.valueOf(bagCodeMapper.selectPendingInboundCountByPalletId(pallet.getId())); |
| | | } else if (SCAN_TYPE_BATCH.equals(scanType)) { |
| | | batch = batchMapper.selectByCode(req.getBagCode()); |
| | | if (batch == null) { |
| | | batch = batchMapper.selectByTempCode(req.getBagCode()); |
| | | } |
| | | batch = batchMapper.selectByScanCodeForUpdate(req.getBagCode()); |
| | | if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "çäº§æ¹æ¬¡ä¸åå¨"); |
| | | if (batch.getInboundEventId() != null) return failed(event, "MES_SCAN_BATCH_ALREADY_INBOUND", "æ¹æ¬¡å·²æ´æ¹å
¥åº"); |
| | | batchId = batch.getId(); |
| | | targetId = batch.getId(); |
| | | quantity = batch.getBagQuantity() != null && batch.getBagQuantity() > 0 |
| | | ? BigDecimal.valueOf(batch.getBagQuantity()) |
| | | : BigDecimal.valueOf(bagCodeMapper.selectCountByBatchId(batch.getId())); |
| | | quantity = BigDecimal.valueOf(bagCodeMapper.selectPendingInboundCountByBatchId(batch.getId())); |
| | | } else { |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByCodeForUpdate(req.getBagCode()); |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(req.getBagCode()); |
| | | if (bag == null) return failed(event, "MES_SCAN_BAG_NOT_EXISTS", "è¢ç ä¸åå¨"); |
| | | if (MesProBagCodeStatusEnum.CANCELED.getStatus().equals(bag.getStatus())) |
| | | return failed(event, "MES_SCAN_BAG_CANCELED", "è¢ç å·²ä½åºï¼ä¸å¯å
¥åº"); |
| | | event.setBagCodeId(bag.getId()); |
| | | bagCodeId = bag.getId(); |
| | | bagCode = req.getBagCode(); |
| | | // åºåæµæ°´ä¿æå追溯ç 为è§èç ï¼ä¸å¡ç ä»
ç¨äºæ¥æ¾åä¸è¢ï¼é¿å
åç ææä¸¤ä»½åºåã |
| | | bagCode = bag.getCode(); |
| | | if (bag.getInboundEventId() != null) return failed(event, "MES_SCAN_BAG_ALREADY_INBOUND", "è¢ç å·²å
¥åº"); |
| | | batch = batchMapper.selectByIdForUpdate(bag.getBatchId()); |
| | | if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "çäº§æ¹æ¬¡ä¸åå¨"); |
| | |
| | | targetId = bag.getId(); |
| | | quantity = req.getQuantity(); |
| | | } |
| | | event.setBatchId(batchId).setBatchCode(batch.getCode()).setItemId(batch.getItemId()); |
| | | if (MesProBatchStatusEnum.CANCELED.getStatus().equals(batch.getStatus())) |
| | | return failed(event, "MES_SCAN_BATCH_CANCELED", "çäº§æ¹æ¬¡å·²ä½åºï¼ä¸å¯å
¥åº"); |
| | | if (quantity.signum() <= 0) |
| | | return failed(event, "MES_SCAN_NO_PENDING_BAGS", "没æå¾
å
¥åºçææè¢ç ï¼è¯·å¿éå¤å
¥åº"); |
| | | event.setBatchId(batchId).setBatchCode(batch.getCode()).setItemId(batch.getItemId()).setQuantity(quantity); |
| | | MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); |
| | | if (warehouse == null) return failed(event, "MES_SCAN_WIP_WAREHOUSE_NOT_EXISTS", "WIPèæä»ä¸åå¨"); |
| | | event.setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId()); |
| | |
| | | palletMapper.markInbound(targetId, event.getId()); |
| | | bagCodeMapper.markInboundByPalletId(targetId, event.getId(), req.getLineCode()); |
| | | } else if (SCAN_TYPE_BATCH.equals(scanType)) { |
| | | batchMapper.markBatchInbound(targetId, event.getId(), req.getLineCode(), batch.getBagQuantity()); |
| | | bagCodeMapper.markInboundByBatchId(targetId, event.getId(), req.getLineCode()); |
| | | } else { |
| | | bagCodeMapper.markInbound(targetId, event.getId(), req.getDeviceCode(), req.getLineCode()); |
| | | batchMapper.markInbound(batchId, event.getId(), req.getDeviceCode(), req.getLineCode()); |
| | | } |
| | | palletMapper.markCompletedInboundByBatchId(batchId, event.getId()); |
| | | batchMapper.refreshInboundProgress(batchId, event.getId(), req.getDeviceCode(), req.getLineCode()); |
| | | return event; |
| | | } catch (Exception ex) { |
| | | log.error("[createInbound] åºåäºå¡å¤ç失败ï¼eventId={}, bagCode={}, batchId={}, itemId={}", |
| | |
| | | |
| | | Long submit(MesQcSeedQualitySubmitReqVO reqVO); |
| | | |
| | | java.util.Map<String, Object> resolveScan(String code); |
| | | java.util.Map<String, Object> resolveScan(String code, String scanType); |
| | | java.util.Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO); |
| | | |
| | | PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO); |
| | | |
| | | /** |
| | |
| | | @Resource private MesQcSeedQualityMapper qualityMapper; |
| | | @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper; |
| | | @Resource private MesProBagCodeMapper bagCodeMapper; |
| | | @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper; |
| | | @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper batchMapper; |
| | | @Resource private MesProBatchService proBatchService; |
| | | @Resource private MesWmBatchMapper mesWmBatchMapper; |
| | | @Resource private MesWmMaterialStockMapper materialStockMapper; |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO) { |
| | | if (StrUtil.isNotBlank(reqVO.getCode())) { |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(reqVO.getCode()); |
| | | if (bag != null) reqVO.setCode(bag.getCode()); |
| | | } |
| | | return qualityMapper.selectPage(reqVO); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveScan(String code) { |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code); |
| | | if (bag == null) throw new IllegalArgumentException("æªè§£æå°ææè¢ç ï¼è¯·æ«æè¢ç "); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("code", bag.getCode()); |
| | | result.put("businessCode", bag.getBusinessCode()); |
| | | result.put("batchCode", bag.getBatchCode()); |
| | | result.put("inbound", bag.getInboundEventId() != null); |
| | | result.put("canceled", Integer.valueOf(30).equals(bag.getStatus())); |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByCode(bag.getCode()); |
| | | result.put("qualityId", quality == null ? null : quality.getId()); |
| | | result.put("result", quality == null ? null : quality.getResult()); |
| | | return result; |
| | | } |
| | | |
| | | private record Scope(String code, String businessCode, String batchCode, boolean canceled, List<MesProBagCodeDO> bags) { } |
| | | |
| | | private Scope scope(String code, String type, boolean lock) { |
| | | if ("BAG".equals(type)) { |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code); |
| | | if (bag == null) throw new IllegalArgumentException("æªæ¾å°è¢ç ï¼è¯·æ ¸å¯¹æ«ç ç±»å"); |
| | | return new Scope(bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(), Integer.valueOf(30).equals(bag.getStatus()), List.of(bag)); |
| | | } |
| | | if ("PALLET".equals(type)) { |
| | | var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code); |
| | | if (pallet == null) throw new IllegalArgumentException("æªæ¾å°æçç ï¼è¯·æ ¸å¯¹æ«ç ç±»å"); |
| | | if (pallet.getBatchId() == null) throw new IllegalArgumentException("æçå°æªç»å®çäº§æ¹æ¬¡ï¼æ æ³è´¨æ£"); |
| | | return new Scope(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(), |
| | | Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId())); |
| | | } |
| | | if ("BATCH".equals(type)) { |
| | | var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code); |
| | | if (batch == null) throw new IllegalArgumentException("æªæ¾å°çäº§æ¹æ¬¡ç ï¼è¯·æ ¸å¯¹æ«ç ç±»å"); |
| | | return new Scope(StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(), |
| | | Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId())); |
| | | } |
| | | throw new IllegalArgumentException("䏿¯æçè´¨æ£æ«ç ç±»å"); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveScan(String code, String scanType) { |
| | | Scope scope = scope(code, scanType, false); |
| | | List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList(); |
| | | Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream() |
| | | .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet()); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("scanType", scanType); result.put("code", scope.code()); result.put("businessCode", scope.businessCode()); |
| | | result.put("batchCode", scope.batchCode()); result.put("canceled", scope.canceled()); |
| | | result.put("totalCount", effective.size()); |
| | | result.put("inbound", !effective.isEmpty() && effective.stream().allMatch(b -> b.getInboundEventId() != null)); |
| | | result.put("pendingBagIds", effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList()); |
| | | result.put("completedBagIds", effective.stream().filter(b -> inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList()); |
| | | if ("BAG".equals(scanType)) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByCode(scope.code()); |
| | | result.put("qualityId", quality == null ? null : quality.getId()); result.put("result", quality == null ? null : quality.getResult()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) { |
| | | Scope scope = scope(req.getCode(), req.getScanType(), true); |
| | | if (scope.canceled()) throw new IllegalArgumentException("è´¨æ£å¯¹è±¡å·²ä½åº"); |
| | | List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList(); |
| | | if (effective.isEmpty()) throw new IllegalArgumentException("æ²¡æææè¢ç "); |
| | | List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(effective.stream().map(MesProBagCodeDO::getId).toList()); |
| | | if (locked.size() != effective.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null)) |
| | | throw new IllegalArgumentException("è¢ç å·²ååæå°æªå
¨é¨å
¥åºï¼è¯·éæ°æ«ææ ¸å¯¹"); |
| | | // å¨è¢ç éä¹åéæ°æ ¸å¯¹å½å±ï¼å
¶ä»ç»å®æä½ä¸å¾æ¹åå·²ç¡®è®¤çæ£éªèå´ã |
| | | Scope current = scope(req.getCode(), req.getScanType(), false); |
| | | Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet()); |
| | | if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList()))) |
| | | throw new IllegalArgumentException("è¢ç å½å±å·²ååï¼è¯·éæ°æ«ææ ¸å¯¹"); |
| | | Set<String> inspected = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).stream() |
| | | .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet()); |
| | | List<MesProBagCodeDO> pending = locked.stream().filter(b -> !inspected.contains(b.getCode())).sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList(); |
| | | List<Long> confirmed = req.getBagIds().stream().sorted().toList(); |
| | | if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesProBagCodeDO::getId).toList())) |
| | | throw new IllegalArgumentException("å¾
è´¨æ£æ¸
åå·²ååæå·²å
¨é¨è´¨æ£ï¼è¯·éæ°æ«æï¼ç¦æ¢éå¤å¤ç"); |
| | | for (MesProBagCodeDO bag : pending) { |
| | | MesQcSeedQualitySubmitReqVO single = new MesQcSeedQualitySubmitReqVO(); |
| | | single.setCode(bag.getCode());single.setIndicators(req.getIndicators());single.setReason(req.getReason()); |
| | | single.setQualifiedWarehouseId(req.getQualifiedWarehouseId()); |
| | | // å¤å±äºå¡è¦çå
¨é¨è¢ç ï¼ä»»ä½ä¸è¢åºåæµè½¬å¤±è´¥é½ä¼åæ»æ´æ¬¡æä½ã |
| | | submit(single); |
| | | } |
| | | return Map.of("processedCount", pending.size()); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public Long submit(MesQcSeedQualitySubmitReqVO reqVO) { |
| | | if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("å¿
é¡»æäº¤åé¡¹ææ "); |
| | | if (qualityMapper.selectByCode(reqVO.getCode()) != null) throw new IllegalArgumentException("该è¢ç 已质æ£ï¼ç¦æ¢éå¤æäº¤"); |
| | | // ç§åè´¨æ£å¿
须以è¢ç ï¼åè¢ï¼æäº¤ï¼ç©æä¿¡æ¯ç±è¢ç èªå¨è§£æï¼å端ä¸ååç¬éæ©ç©æ |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByCode(reqVO.getCode()); |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(reqVO.getCode()); |
| | | if (bag == null) { |
| | | throw new IllegalArgumentException("æªè§£æå°ææè¢ç ï¼ç§åè´¨æ£å¿
须以è¢ç æäº¤"); |
| | | } |
| | | MesProBatchDO batch = resolveBatch(reqVO.getCode(), reqVO.getBatchCode()); |
| | | if (Integer.valueOf(30).equals(bag.getStatus()) || bag.getInboundEventId() == null) |
| | | throw new IllegalArgumentException("è¢ç å·²ä½åºæå°æªå
¥åºï¼æ æ³è´¨æ£"); |
| | | // åç éå®åä¸è¢ï¼å¹¶ä»¥è¿½æº¯ç è®°å½ç»æï¼é²æ¢æ¢ç æå¹¶åé夿£ååºåã |
| | | if (qualityMapper.selectByCodeForUpdate(bag.getCode()) != null) throw new IllegalArgumentException("该è¢ç 已质æ£ï¼ç¦æ¢éå¤æäº¤"); |
| | | MesProBatchDO batch = bag.getBatchId() == null ? null : proBatchService.getBatch(bag.getBatchId()); |
| | | if (batch == null || batch.getItemId() == null) { |
| | | throw new IllegalArgumentException("æªæ ¹æ®è¢ç è§£æå°ç©æä¿¡æ¯ï¼è¯·ç¡®è®¤æ«ç å
å®¹ææ¹æ¬¡å·æ£ç¡®"); |
| | | } |
| | | if (Integer.valueOf(40).equals(batch.getStatus())) throw new IllegalArgumentException("çäº§æ¹æ¬¡å·²ä½åºï¼æ æ³è´¨æ£"); |
| | | // è¢ç å¯¹åºæ¹æ¬¡çåºåå¿
é¡»å¤äºæååºï¼å¾
è´¨æ£ï¼ä¸ï¼æè½è¿è¡ç§åè´¨æ£ |
| | | MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode()); |
| | | if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0) { |
| | |
| | | } |
| | | Set<String> codes = new HashSet<>(); |
| | | boolean passed = true; |
| | | MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(batch.getItemId()) |
| | | MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId()) |
| | | .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0) |
| | | .inspectedAt(LocalDateTime.now()).build(); |
| | | qualityMapper.insert(quality); |
| | |
| | | .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); |
| | | } |
| | | if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("åé¡¹ææ ä¸å®æ´"); |
| | | if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("è´¨æ£ä¸åæ ¼å¿
须填ååå "); |
| | | quality.setResult(passed ? 1 : 2); |
| | | qualityMapper.updateById(quality); |
| | | // æè¢ç ç²åº¦æµè½¬åºåï¼éå®è¯¥è¢å¨æååºä¸çæ´»å¨æç»ï¼ç¦æ¢ææ¹æ¬¡ä»»ææ£å |
| | |
| | | @Override |
| | | public void updateProductSalesDetail(MesWmProductSalesDetailSaveReqVO updateReqVO) { |
| | | // æ ¡éªåå¨ |
| | | validateProductSalesDetailExists(updateReqVO.getId()); |
| | | MesWmProductSalesDetailDO existing = validateProductSalesDetailExists(updateReqVO.getId()); |
| | | if (existing.getBagCodeId() != null) throw new IllegalArgumentException("æ«ç è¢ç æç»ä¸è½æå·¥ä¿®æ¹æ°éæåºåï¼è¯·å é¤åéæ°æ«ç éæ©"); |
| | | // æ ¡éªæ°æ® |
| | | validateProductSalesDetailSaveData(updateReqVO); |
| | | |
| | |
| | | |
| | | MesWmProductSalesScanRespVO scanProductSales(@Valid MesWmProductSalesScanReqVO reqVO); |
| | | |
| | | MesWmProductSalesScanRespVO previewScan(String code); |
| | | MesWmProductSalesScanRespVO previewScan(String code, String scanType, Long salesId); |
| | | |
| | | /** |
| | | * å¡«åè¿åï¼å¾
å¡«åè¿å â å¾
æ§è¡åºåºï¼ |
| | | * |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | |
| | | |
| | | @Resource |
| | | private MesTraceIntegrityService integrityService; |
| | | @Resource private MesProBagCodeMapper pickingBagMapper; |
| | | @Resource private MesProPalletMapper pickingPalletMapper; |
| | | @Resource private MesProBatchMapper pickingBatchMapper; |
| | | @Resource private MesWmMaterialStockBagMapper pickingStockBagMapper; |
| | | |
| | | @Override |
| | | public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) { |
| | |
| | | return true; |
| | | } |
| | | |
| | | private record PickingScope(String type, Long id, MesProBatchDO batch, List<MesProBagCodeDO> bags) { } |
| | | |
| | | private PickingScope pickingScope(String code, String type, boolean lock) { |
| | | if (type == null || type.isBlank()) { |
| | | type = palletService.getPalletByScanCode(code) != null ? "PALLET" |
| | | : bagCodeService.getBagCodeByScanCode(code) != null ? "BAG" : "BATCH"; |
| | | } |
| | | MesProBatchDO batch; |
| | | List<MesProBagCodeDO> bags; |
| | | Long id; |
| | | switch (type) { |
| | | case "BAG" -> { |
| | | MesProBagCodeDO bag = lock ? pickingBagMapper.selectByScanCodeForUpdate(code) : bagCodeService.getBagCodeByScanCode(code); |
| | | if (bag == null || Integer.valueOf(30).equals(bag.getStatus())) throw new IllegalArgumentException("è¢ç ä¸å卿已ä½åº"); |
| | | id = bag.getId(); batch = proBatchService.getBatch(bag.getBatchId()); bags = List.of(bag); |
| | | } |
| | | case "PALLET" -> { |
| | | MesProPalletDO pallet = lock ? pickingPalletMapper.selectByScanCodeForUpdate(code) : palletService.getPalletByScanCode(code); |
| | | if (pallet == null || Integer.valueOf(20).equals(pallet.getStatus())) throw new IllegalArgumentException("æçä¸å卿已ä½åº"); |
| | | if (pallet.getBatchId() == null) throw exception(PRO_PALLET_NOT_BIND_BATCH); |
| | | id = pallet.getId(); batch = proBatchService.getBatch(pallet.getBatchId()); |
| | | bags = lock ? pickingBagMapper.selectListByPalletIdForUpdate(id) : bagCodeService.getBagCodeListByPalletId(id); |
| | | } |
| | | case "BATCH" -> { |
| | | batch = lock ? pickingBatchMapper.selectByScanCodeForUpdate(code) : proBatchService.getBatchByScanCode(code); |
| | | if (batch == null) throw new IllegalArgumentException("çäº§æ¹æ¬¡ç ä¸åå¨"); |
| | | id = batch.getId(); bags = lock ? pickingBagMapper.selectListByBatchIdForUpdate(id) : bagCodeService.getBagCodeListByBatchId(id); |
| | | } |
| | | default -> throw new IllegalArgumentException("䏿¯æçæ£è´§æ«ç ç±»å"); |
| | | } |
| | | if (batch == null || batch.getItemId() == null || Integer.valueOf(40).equals(batch.getStatus())) |
| | | throw new IllegalArgumentException("æªç»å®ææåç§æçäº§æ¹æ¬¡å·²ä½åº"); |
| | | return new PickingScope(type, id, batch, bags.stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())) |
| | | .sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList()); |
| | | } |
| | | |
| | | private String unavailable(MesProBagCodeDO bag, MesWmMaterialStockBagDO stockBag, |
| | | Map<Long, MesWmMaterialStockDO> stocks, List<MesWmProductSalesDetailDO> occupied, MesProBatchDO batch) { |
| | | if (!Objects.equals(bag.getBatchId(), batch.getId())) return "è¢ç æ¹æ¬¡å½å±å¼å¸¸"; |
| | | if (bag.getItemId() != null && !Objects.equals(bag.getItemId(), batch.getItemId())) return "è¢ç åç§å½å±å¼å¸¸"; |
| | | if (!MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(bag.getQcStatus())) return "å°æªè´¨æ£åæ ¼"; |
| | | if (stockBag == null || !Integer.valueOf(1).equals(stockBag.getStatus())) return "ä¸å¨åºæå·²åºåº"; |
| | | if (Boolean.TRUE.equals(stockBag.getFrozen())) return "è¢ç å·²å»ç»"; |
| | | if (!Integer.valueOf(20).equals(stockBag.getStockState()) || !Objects.equals(stockBag.getItemId(), batch.getItemId()) |
| | | || !stocks.containsKey(stockBag.getMaterialStockId()) || stockBag.getQuantity() == null || BigDecimal.ONE.compareTo(stockBag.getQuantity()) != 0) |
| | | return "è¢ç ä¸å¨ææåæ ¼åºåä¸"; |
| | | if (occupied.stream().anyMatch(d -> Objects.equals(d.getBagCodeId(), bag.getId()) |
| | | || d.getBagCodeId() == null && bag.getPalletId() != null && Objects.equals(d.getPalletCodeId(), bag.getPalletId()))) |
| | | return "å·²å å
¥åºåºåæ£è´§"; |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public MesWmProductSalesScanRespVO previewScan(String code) { return previewScan(code, null, null); } |
| | | |
| | | /** åªè¯»è¯å«ï¼å¹¶è¿åæ¯è¢çå¯éç¶æï¼ä¸å建åºåæ¹æ¬¡ææ£è´§è®°å½ã */ |
| | | @Override |
| | | public MesWmProductSalesScanRespVO previewScan(String code, String scanType, Long salesId) { |
| | | PickingScope scope = pickingScope(code, scanType, false); |
| | | if (salesId != null) { |
| | | MesWmProductSalesDO sales = validateProductSalesExists(salesId); |
| | | if (!Integer.valueOf(2).equals(sales.getStatus())) throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | if (productSalesLineService.getProductSalesLineListBySalesId(salesId).stream() |
| | | .noneMatch(l -> Objects.equals(l.getItemId(), scope.batch().getItemId()))) throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | } |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scope.batch().getCode()); |
| | | Map<Long, MesWmMaterialStockDO> stocks = wmBatch == null ? Map.of() : materialStockService |
| | | .getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()).stream() |
| | | .collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); |
| | | Map<Long, MesWmMaterialStockBagDO> stockBags = pickingStockBagMapper.selectByBagIds(scope.bags().stream().map(MesProBagCodeDO::getId).toList(), false) |
| | | .stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); |
| | | List<MesWmProductSalesDetailDO> occupied = productSalesDetailMapper.selectPickingByBatchCode(scope.batch().getCode()); |
| | | List<MesWmProductSalesScanRespVO.BagOption> options = scope.bags().stream().map(b -> { |
| | | String reason = unavailable(b, stockBags.get(b.getId()), stocks, occupied, scope.batch()); |
| | | return new MesWmProductSalesScanRespVO.BagOption().setId(b.getId()).setCode(b.getCode()).setBusinessCode(b.getBusinessCode()) |
| | | .setPalletId(b.getPalletId()).setSelectable(reason == null).setUnavailableReason(reason); |
| | | }).toList(); |
| | | return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()).setScanContent(code) |
| | | .setBatchCode(scope.batch().getCode()).setItemId(scope.batch().getItemId()).setBagCount(options.size()).setBags(options) |
| | | .setScanQuantity(BigDecimal.valueOf(options.stream().filter(o -> Boolean.TRUE.equals(o.getSelectable())).count())); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesWmProductSalesScanRespVO scanProductSales(MesWmProductSalesScanReqVO reqVO) { |
| | | MesWmProductSalesDO sales = validateProductSalesExists(reqVO.getSalesId()); |
| | | if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVING.getStatus(), sales.getStatus())) { |
| | | throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | MesWmProductSalesDO sales = productSalesMapper.selectByIdForUpdate(reqVO.getSalesId()); |
| | | if (sales == null) throw exception(WM_PRODUCT_SALES_NOT_EXISTS); |
| | | if (!Integer.valueOf(2).equals(sales.getStatus())) throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | PickingScope scope = pickingScope(reqVO.getScanContent(), reqVO.getScanType(), true); |
| | | List<Long> ids = reqVO.getBagIds(); |
| | | if (ids == null && "BAG".equals(scope.type())) ids = List.of(scope.id()); |
| | | if (ids == null || ids.isEmpty() || new HashSet<>(ids).size() != ids.size()) |
| | | throw new IllegalArgumentException("è¯·éæ©å
·ä½è¢ç ï¼ä¸å
许éå¤è¢ç "); |
| | | BigDecimal quantity = BigDecimal.valueOf(ids.size()); |
| | | if (reqVO.getQuantity() != null && quantity.compareTo(reqVO.getQuantity()) != 0) |
| | | throw new IllegalArgumentException("æ£è´§æ°éå¿
须䏿éè¢ç æ°éä¸è´"); |
| | | Set<Long> selectedIds = new HashSet<>(ids); |
| | | List<MesProBagCodeDO> selected = scope.bags().stream().filter(b -> selectedIds.contains(b.getId())).toList(); |
| | | if (selected.size() != ids.size()) throw new IllegalArgumentException("æéè¢ç ä¸å±äºå½åæ«ç 对象æå·²ä½åºï¼è¯·éæ°æ ¸å¯¹"); |
| | | // éå®å®é
è¢ç ååºåè¢æç»ï¼è·¨åºåºåçæ«ç ä¹ä¸è½é夿£éåä¸è¢ã |
| | | List<MesWmMaterialStockBagDO> stockBagRows = pickingStockBagMapper.selectByBagIds(ids, true); |
| | | Map<Long, MesWmMaterialStockBagDO> stockBags = stockBagRows.stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); |
| | | List<MesWmProductSalesDetailDO> occupied = productSalesDetailMapper.selectPickingByBatchCodeForUpdate(scope.batch().getCode()); |
| | | List<MesWmProductSalesDetailDO> own = occupied.stream().filter(d -> Objects.equals(d.getSalesId(), sales.getId())).toList(); |
| | | long already = selected.stream().filter(b -> own.stream().anyMatch(d -> Objects.equals(d.getBagCodeId(), b.getId()))).count(); |
| | | if (already == selected.size()) return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()) |
| | | .setScanContent(reqVO.getScanContent()).setSelectedBagIds(ids).setAlreadyScanned(true).setOutboundQuantity(quantity) |
| | | .setDetailId(own.stream().filter(d -> selectedIds.contains(d.getBagCodeId())).findFirst().orElseThrow().getId()).setMessage("æéè¢ç 已卿¬åæ£è´§æç»ä¸"); |
| | | if (already > 0) throw new IllegalArgumentException("æéè¢ç é¨åå·²å å
¥æ¬åï¼è¯·éæ°éæ©æªæ£è¢ç "); |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scope.batch().getCode()); |
| | | if (wmBatch == null) throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | Map<Long, MesWmMaterialStockDO> stocks = materialStockService.getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()) |
| | | .stream().collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); |
| | | for (MesProBagCodeDO bag : selected) { |
| | | String reason = unavailable(bag, stockBags.get(bag.getId()), stocks, occupied, scope.batch()); |
| | | if (reason != null) throw new IllegalArgumentException("è¢ç " + bag.getCode() + "ï¼" + reason); |
| | | } |
| | | MesProPalletDO pallet = palletService.getPalletByCode(reqVO.getScanContent()); |
| | | MesProBagCodeDO bag = pallet == null ? bagCodeService.getBagCodeByCode(reqVO.getScanContent()) : null; |
| | | if (pallet == null && bag == null) { |
| | | throw exception(WM_MATERIAL_STOCK_NOT_EXISTS); |
| | | } |
| | | // æªç»å®çäº§æ¹æ¬¡çä¸´æ¶æçä¸å
许åä¸éå®åºåº |
| | | if (pallet != null && pallet.getBatchId() == null) { |
| | | throw exception(PRO_PALLET_NOT_BIND_BATCH); |
| | | } |
| | | // ç§åè´¨æ£æè¢ç ç²åº¦æµè½¬åï¼åºåºåªå
许已质æ£åæ ¼çè¢ç /æç |
| | | if (pallet != null) { |
| | | List<MesProBagCodeDO> palletBags = bagCodeService.getBagCodeListByPalletId(pallet.getId()); |
| | | if (palletBags.stream().anyMatch(b -> !MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(b.getQcStatus()))) { |
| | | throw exception(WM_PRODUCT_SALES_SCAN_PALLET_NOT_ALL_QUALIFIED); |
| | | } |
| | | } else if (!MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(bag.getQcStatus())) { |
| | | throw exception(WM_PRODUCT_SALES_SCAN_BAG_NOT_QUALIFIED); |
| | | } |
| | | Long codeId = pallet != null ? pallet.getId() : bag.getId(); |
| | | MesWmProductSalesDetailDO existed = pallet != null |
| | | ? productSalesDetailMapper.selectBySalesIdAndPalletCodeId(reqVO.getSalesId(), codeId) |
| | | : productSalesDetailMapper.selectBySalesIdAndBagCodeId(reqVO.getSalesId(), codeId); |
| | | if (existed != null) { |
| | | return new MesWmProductSalesScanRespVO().setScanType(pallet != null ? "PALLET" : "BAG") |
| | | .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setAlreadyScanned(true) |
| | | .setDetailId(existed.getId()).setOutboundQuantity(existed.getQuantity()).setMessage("该ç å·²æ«æ"); |
| | | } |
| | | Long proBatchId = pallet != null ? pallet.getBatchId() : bag.getBatchId(); |
| | | MesProBatchDO batch = proBatchService.getBatch(proBatchId); |
| | | if (batch == null) { |
| | | throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | } |
| | | // çäº§æ¹æ¬¡æ¡¥æ¥ä»åºæ¹æ¬¡ï¼æ«ç å
¥åºåå
¥çæ¯ä»åºæ¹æ¬¡ï¼mes_wm_batchï¼ï¼æ¤å¤å¿
é¡»ç¨ä»åºæ¹æ¬¡ id æ¥åºå |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(batch); |
| | | if (wmBatch == null) { |
| | | throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | } |
| | | List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(reqVO.getSalesId()); |
| | | MesWmProductSalesLineDO line = lines.stream().filter(item -> ObjUtil.equal(item.getItemId(), batch.getItemId())).findFirst().orElse(null); |
| | | if (line == null) { |
| | | throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | } |
| | | BigDecimal quantity = reqVO.getQuantity(); |
| | | if (quantity == null) { |
| | | quantity = line.getQuantity(); |
| | | } |
| | | // åªä»åæ ¼åºè®°å½ä¸éåºåï¼åæ¹æ¬¡æååºè®°å½ä¸å¯åºåºï¼ |
| | | List<MesWmMaterialStockDO> stocks = materialStockService.getQualifiedMaterialStockListByItemAndBatch(batch.getItemId(), wmBatch.getId()); |
| | | BigDecimal outboundQuantity = quantity; |
| | | MesWmMaterialStockDO stock = stocks.stream().filter(item -> item.getQuantity().compareTo(outboundQuantity) >= 0).findFirst().orElse(null); |
| | | if (stock == null) { |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | | MesWmProductSalesLineDO line = productSalesLineService.getProductSalesLineListBySalesId(sales.getId()).stream() |
| | | .filter(l -> Objects.equals(l.getItemId(), scope.batch().getItemId())).findFirst().orElse(null); |
| | | if (line == null) throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | BigDecimal scanned = productSalesDetailService.getProductSalesDetailListByLineId(line.getId()).stream() |
| | | .map(MesWmProductSalesDetailDO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (scanned.add(quantity).compareTo(line.getQuantity()) > 0) { |
| | | throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); |
| | | if (line.getQuantity() == null || scanned.add(quantity).compareTo(line.getQuantity()) > 0) throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); |
| | | Map<Long, Long> byStock = selected.stream().collect(Collectors.groupingBy(b -> stockBags.get(b.getId()).getMaterialStockId(), Collectors.counting())); |
| | | for (var allocation : byStock.entrySet()) { |
| | | BigDecimal ownStockQty = own.stream().filter(d -> Objects.equals(d.getMaterialStockId(), allocation.getKey())) |
| | | .map(MesWmProductSalesDetailDO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (stocks.get(allocation.getKey()).getQuantity().compareTo(ownStockQty.add(BigDecimal.valueOf(allocation.getValue()))) < 0) |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | | MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setLineId(line.getId()) |
| | | .setSalesId(reqVO.getSalesId()).setItemId(batch.getItemId()).setQuantity(quantity) |
| | | .setMaterialStockId(stock.getId()).setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()) |
| | | .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()) |
| | | .setBagCodeId(pallet == null ? codeId : null).setPalletCodeId(pallet != null ? codeId : null); |
| | | productSalesDetailMapper.insert(detail); |
| | | return new MesWmProductSalesScanRespVO().setScanType(pallet != null ? "PALLET" : "BAG") |
| | | .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setItemId(batch.getItemId()) |
| | | .setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()).setMaterialStockId(stock.getId()) |
| | | .setAvailableQuantity(stock.getQuantity()).setScanQuantity(quantity).setOutboundQuantity(quantity) |
| | | .setAlreadyScanned(false).setDetailId(detail.getId()).setWarehouseId(stock.getWarehouseId()) |
| | | .setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()).setMessage("æ«ç æå"); |
| | | Long firstDetail = null; |
| | | for (MesProBagCodeDO bag : selected) { |
| | | MesWmMaterialStockDO stock = stocks.get(stockBags.get(bag.getId()).getMaterialStockId()); |
| | | MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setSalesId(sales.getId()).setLineId(line.getId()) |
| | | .setItemId(scope.batch().getItemId()).setQuantity(BigDecimal.ONE).setBagCodeId(bag.getId()) |
| | | .setPalletCodeId(bag.getPalletId()).setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()).setMaterialStockId(stock.getId()) |
| | | .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()) |
| | | .setRemark("æ«ç ç±»å " + scope.type() + "ï¼æ¥æºç " + reqVO.getScanContent() + "ï¼è¢ç " + bag.getCode()); |
| | | productSalesDetailMapper.insert(detail); |
| | | if (firstDetail == null) firstDetail = detail.getId(); |
| | | } |
| | | return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()).setScanContent(reqVO.getScanContent()) |
| | | .setItemId(scope.batch().getItemId()).setBatchCode(wmBatch.getCode()).setSelectedBagIds(ids) |
| | | .setScanQuantity(quantity).setOutboundQuantity(quantity).setAlreadyScanned(false).setDetailId(firstDetail).setMessage("å·²ææéè¢ç å å
¥æ£è´§"); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (detail.getBagCodeId() != null) { |
| | | bagCodeService.markOutbound(detail.getBagCodeId(), id); |
| | | } |
| | | if (detail.getPalletCodeId() != null) { |
| | | // é¨åæçæ£è´§å¯è½åç»å¤ä¸ªç»éåï¼ä¸è½ææ´ä¸ªæçæ 记为æ¬ååºåºã |
| | | if (detail.getPalletCodeId() != null && detail.getBagCodeId() == null) { |
| | | palletService.markOutbound(detail.getPalletCodeId(), id); |
| | | } |
| | | } |
| | |
| | | .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId()) |
| | | .setQuantity(detail.getQuantity().negate()) // åºåºæ°éä¸ºè´æ° |
| | | .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) |
| | | .setBagCodeId(detail.getBagCodeId()) // è¢ç åæç»åºåºæ¶ç½®è¢ç å·²åºåºï¼æçåæç»ä¸éè¢å¤ç |
| | | .setBagCodeId(detail.getBagCodeId()) // æ°æ«ç æç»éè¢å
³èï¼æ§è¡åºåºéè¢æ´æ°åºåç¶æ |
| | | .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId()) |
| | | .setBizType(MesBizTypeConstants.WM_PRODUCT_SALES).setBizId(sales.getId()) |
| | | .setBizCode(sales.getCode()).setBizLineId(detail.getLineId()) |
| | |
| | | return wmBatch; |
| | | } |
| | | // 2. å½ä¸è¢ç |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByCode(scanCode); |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(scanCode); |
| | | if (bag != null && bag.getBatchId() != null) { |
| | | return bridgeProBatch(bag.getBatchId()); |
| | | } |
| | | // 3. å½ä¸æçç |
| | | MesProPalletDO pallet = palletMapper.selectByCode(scanCode); |
| | | MesProPalletDO pallet = palletMapper.selectByScanCode(scanCode); |
| | | if (pallet != null && pallet.getBatchId() != null) { |
| | | return bridgeProBatch(pallet.getBatchId()); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.controller.admin.pro.scan; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| | | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO; |
| | | import cn.iocoder.yudao.module.mes.service.pro.scan.MesProScanEventService; |
| | | import org.junit.jupiter.api.*; |
| | | import org.springframework.security.core.context.SecurityContextHolder; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | class MobileScanAccessTest { |
| | | private final MesProScanEventService service=mock(MesProScanEventService.class); |
| | | private final MesProScanEventController controller=new MesProScanEventController(); |
| | | @BeforeEach void setup(){ |
| | | SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(7L),new org.springframework.mock.web.MockHttpServletRequest()); |
| | | ReflectionTestUtils.setField(controller,"service",service); |
| | | } |
| | | @AfterEach void cleanup(){SecurityContextHolder.clearContext();} |
| | | @Test void overridesForgedCreatorOnOwnPage(){ |
| | | MesProScanEventPageReqVO req=new MesProScanEventPageReqVO();req.setCreator("8"); |
| | | when(service.getPage(req)).thenReturn(new PageResult<>(List.of(),0L)); |
| | | controller.myPage(req); |
| | | assertEquals("7",req.getCreator());verify(service).getPage(req); |
| | | } |
| | | @Test void doesNotExposeAnotherUsersEvent(){ |
| | | MesProScanEventDO event=new MesProScanEventDO();event.setCreator("8"); |
| | | when(service.getByEventId("other")).thenReturn(event); |
| | | assertNull(controller.myEvent("other").getData()); |
| | | event.setCreator("7");event.setEventId("other"); |
| | | assertEquals("other",controller.myEvent("other").getData().getEventId()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.bagcode; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi; |
| | | import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.*; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeReplaceMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO; |
| | | import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder; |
| | | import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils; |
| | | import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; |
| | | import cn.hutool.extra.qrcode.QrCodeUtil; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.Collection; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class BagBusinessCodeTest { |
| | | private final MesProBagCodeServiceImpl service = new MesProBagCodeServiceImpl(); |
| | | private final MesProBagCodeMapper bags = mock(MesProBagCodeMapper.class); |
| | | private final MesCodeRedisDAO codes = mock(MesCodeRedisDAO.class); |
| | | private final MesMdmItemService items = mock(MesMdmItemService.class); |
| | | private final MesProBatchMapper batches = mock(MesProBatchMapper.class); |
| | | private final MesProBagCodeReplaceMapper replacements = mock(MesProBagCodeReplaceMapper.class); |
| | | private final MdmUnitMeasureApi units = mock(MdmUnitMeasureApi.class); |
| | | |
| | | @BeforeEach void setup() { |
| | | ReflectionTestUtils.setField(service, "bagCodeMapper", bags); |
| | | ReflectionTestUtils.setField(service, "codeRedisDao", codes); |
| | | ReflectionTestUtils.setField(service, "mdmItemService", items); |
| | | ReflectionTestUtils.setField(service, "batchMapper", batches); |
| | | ReflectionTestUtils.setField(service, "replaceMapper", replacements); |
| | | ReflectionTestUtils.setField(service, "unitMeasureApi", units); |
| | | when(items.validateItemExists(1L)).thenReturn(new MesMdmItemDO().setId(1L).setUnitMeasureId(2L)); |
| | | AtomicLong sequence = new AtomicLong(1000000000000000L); |
| | | when(codes.generate()).thenAnswer(invocation -> String.valueOf(sequence.incrementAndGet())); |
| | | when(codes.allocateBagNoStart(anyInt(), any())).thenReturn(1); |
| | | } |
| | | |
| | | @Test void rawGenerationCreatesDistinctPairsInSameRecords() { |
| | | MesProBagCodeRawGenerateReqVO req = new MesProBagCodeRawGenerateReqVO(); |
| | | req.setItemId(1L); req.setCount(2); |
| | | service.generateRawBagCodes(req); |
| | | assertDistinctPairs(2); |
| | | } |
| | | |
| | | @Test void dataPackageGenerationCreatesBusinessCodes() { |
| | | when(units.getUnitMeasure(2L)).thenReturn(CommonResult.success(new MdmUnitMeasureRespDTO().setType("å¨"))); |
| | | MesProBagCodeGenerateReqVO req = new MesProBagCodeGenerateReqVO(); |
| | | req.setItemId(1L); req.setBagCount(2); |
| | | service.generateBagCodes(req); |
| | | assertDistinctPairs(2); |
| | | } |
| | | |
| | | @Test @SuppressWarnings({"unchecked", "rawtypes"}) void automaticPalletCarriesVariety() { |
| | | var pallets=mock(cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper.class); |
| | | ReflectionTestUtils.setField(service,"palletMapper",pallets); |
| | | when(units.getUnitMeasure(2L)).thenReturn(CommonResult.success(new MdmUnitMeasureRespDTO().setType("2åå
"))); |
| | | var req=new MesProBagCodeGenerateReqVO();req.setItemId(1L);req.setPalletCount(1); |
| | | service.generateBagCodes(req); |
| | | ArgumentCaptor<Collection<cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO>> saved=ArgumentCaptor.forClass((Class)Collection.class); |
| | | verify(pallets).insertBatch(saved.capture());assertEquals(1L,saved.getValue().iterator().next().getItemId()); |
| | | } |
| | | |
| | | @Test void replacementIssuesNewPairInsteadOfReusingOldBusinessCode() { |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setStatus(20)); |
| | | when(bags.selectListByIdsForUpdate(List.of(1L))).thenReturn(List.of(new MesProBagCodeDO() |
| | | .setId(1L).setBatchId(2L).setStatus(10).setCode("9999999999999999").setBusinessCode("8888888888888888"))); |
| | | when(bags.cancelById(eq(1L), eq(10), eq(30), any(), any())).thenReturn(1); |
| | | MesProBagCodeReplaceReqVO req = new MesProBagCodeReplaceReqVO(); |
| | | req.setBatchId(2L); req.setOldIds(List.of(1L)); req.setReason("ç ´æ"); |
| | | service.replaceBagCodes(req); |
| | | assertDistinctPairs(1); |
| | | } |
| | | |
| | | @Test void supplementationLocksAndNeverChangesExistingCode() { |
| | | when(bags.selectListByIdsForUpdate(List.of(1L, 2L))).thenReturn(List.of( |
| | | new MesProBagCodeDO().setId(1L).setStatus(10), |
| | | new MesProBagCodeDO().setId(2L).setStatus(20).setBusinessCode("8888888888888888"))); |
| | | assertEquals(1, service.generateBusinessCodes(List.of(2L, 1L, 1L))); |
| | | ArgumentCaptor<MesProBagCodeDO> update = ArgumentCaptor.forClass(MesProBagCodeDO.class); |
| | | verify(bags).updateById(update.capture()); |
| | | assertEquals(1L, update.getValue().getId()); |
| | | assertNull(update.getValue().getCode()); |
| | | assertNotNull(update.getValue().getBusinessCode()); |
| | | when(bags.selectListByIdsForUpdate(List.of(1L))).thenReturn(List.of(update.getValue().setStatus(10))); |
| | | assertEquals(0, service.generateBusinessCodes(List.of(1L))); |
| | | verify(codes, times(1)).generate(); |
| | | } |
| | | |
| | | @Test void canceledOrMissingBagCannotBeSupplemented() { |
| | | when(bags.selectListByIdsForUpdate(List.of(1L))).thenReturn(List.of(new MesProBagCodeDO().setId(1L).setStatus(30))); |
| | | assertThrows(ServiceException.class, () -> service.generateBusinessCodes(List.of(1L))); |
| | | assertThrows(ServiceException.class, () -> service.generateBusinessCodes(List.of(2L))); |
| | | verifyNoInteractions(codes); |
| | | verify(bags, never()).updateById(any(MesProBagCodeDO.class)); |
| | | } |
| | | |
| | | @Test void publicLookupUsesTraceColumnAndInternalLookupUsesBoth() { |
| | | service.getBagCodeByCode("1111111111111111"); |
| | | verify(bags).selectByCode("1111111111111111"); |
| | | service.getBagCodeByScanCode("2222222222222222"); |
| | | verify(bags).selectByScanCode("2222222222222222"); |
| | | } |
| | | |
| | | @Test void businessQrEncodesStoredValueAndRejectsMissingCode() { |
| | | var label = MesProQrLabelBuilder.buildBagBusinessCodeLabel(new MesProBagCodeDO() |
| | | .setCode("9999999999999999").setBusinessCode("8888888888888888")); |
| | | assertEquals("8888888888888888", QrCodeUtil.decode(QrCodeImageUtils.createQrLabel(label.getCode(), label.getFileName()))); |
| | | assertThrows(ServiceException.class, () -> MesProQrLabelBuilder.buildBagBusinessCodeLabel(new MesProBagCodeDO())); |
| | | } |
| | | |
| | | @SuppressWarnings({"unchecked", "rawtypes"}) |
| | | private void assertDistinctPairs(int count) { |
| | | ArgumentCaptor<Collection<MesProBagCodeDO>> saved = ArgumentCaptor.forClass((Class) Collection.class); |
| | | verify(bags).insertBatch(saved.capture()); |
| | | assertEquals(count, saved.getValue().size()); |
| | | var values = new HashSet<String>(); |
| | | for (MesProBagCodeDO bag : saved.getValue()) { |
| | | assertTrue(bag.getBusinessCode().matches("[0-9]{16}")); |
| | | assertTrue(values.add(bag.getCode())); assertTrue(values.add(bag.getBusinessCode())); |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.bagcode; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.*; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO; |
| | | import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletServiceImpl; |
| | | import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchServiceImpl; |
| | | import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import java.util.Collection; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class PalletBatchBusinessCodeTest { |
| | | private final MesProPalletServiceImpl palletService = new MesProPalletServiceImpl(); |
| | | private final MesProBatchServiceImpl batchService = new MesProBatchServiceImpl(); |
| | | private final MesProPalletMapper pallets = mock(MesProPalletMapper.class); |
| | | private final MesProBatchMapper batches = mock(MesProBatchMapper.class); |
| | | private final MesProBagCodeMapper bags = mock(MesProBagCodeMapper.class); |
| | | private final MesCodeRedisDAO codes = mock(MesCodeRedisDAO.class); |
| | | |
| | | @BeforeEach void setup() { |
| | | ReflectionTestUtils.setField(palletService, "palletMapper", pallets); |
| | | ReflectionTestUtils.setField(palletService, "batchMapper", batches); |
| | | ReflectionTestUtils.setField(palletService, "bagCodeMapper", bags); |
| | | ReflectionTestUtils.setField(palletService, "codeRedisDao", codes); |
| | | ReflectionTestUtils.setField(palletService,"integrityService",mock(cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService.class)); |
| | | ReflectionTestUtils.setField(batchService, "batchMapper", batches); |
| | | ReflectionTestUtils.setField(batchService, "codeRedisDao", codes); |
| | | AtomicLong sequence = new AtomicLong(1000000000000000L); |
| | | when(codes.generate()).thenAnswer(invocation -> String.valueOf(sequence.incrementAndGet())); |
| | | } |
| | | |
| | | @Test void preGeneratedPalletsAndBatchesAlreadyHaveIndependentBusinessCodes() { |
| | | var palletReq = new MesProPalletPreGenerateReqVO(); palletReq.setCount(2); |
| | | var batchReq = new MesProBatchPreGenerateReqVO(); batchReq.setCount(2); |
| | | var values = new java.util.HashSet<String>(); |
| | | palletService.preGeneratePallet(palletReq).forEach(record -> { |
| | | assertTrue(values.add(record.getTempCode())); assertTrue(values.add(record.getBusinessCode())); |
| | | }); |
| | | batchService.preGenerateBatch(batchReq).forEach(record -> { |
| | | assertTrue(values.add(record.getTempCode())); assertTrue(values.add(record.getBusinessCode())); |
| | | }); |
| | | assertEquals(8, values.size()); |
| | | assertTrue(values.stream().allMatch(code -> code.matches("[0-9]{16}"))); |
| | | verify(pallets).insertBatch(anyCollection()); |
| | | verify(batches, times(2)).insert(any(MesProBatchDO.class)); |
| | | // é¢çæåªè½åå
¥æ°è®°å½ï¼ä¸åºè¯»åææªåæ°æ®åºä¸çæ§ä¸´æ¶ç ã |
| | | verifyNoMoreInteractions(pallets, batches); |
| | | } |
| | | |
| | | @Test @SuppressWarnings({"unchecked", "rawtypes"}) |
| | | void directPalletCreationCreatesBothCodes() { |
| | | when(batches.selectById(2L)).thenReturn(new MesProBatchDO().setId(2L).setCode("batch").setStatus(20)); |
| | | var req = new MesProPalletBatchCreateReqVO(); req.setBatchId(2L); req.setCount(1); |
| | | palletService.batchCreatePallet(req); |
| | | ArgumentCaptor<Collection<MesProPalletDO>> saved = ArgumentCaptor.forClass((Class) Collection.class); |
| | | verify(pallets).insertBatch(saved.capture()); |
| | | var pallet = saved.getValue().iterator().next(); |
| | | assertNotNull(pallet.getBusinessCode()); assertNotEquals(pallet.getCode(), pallet.getBusinessCode()); |
| | | } |
| | | |
| | | @Test void batchBindingKeepsBusinessCodeAndCopiesActualFormalCodeToBags() { |
| | | var pallet = new MesProPalletDO().setId(1L).setItemId(3L).setTempCode("1111111111111111").setBusinessCode("2222222222222222").setStatus(10); |
| | | when(pallets.selectByIdForUpdate(1L)).thenReturn(pallet); |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setCode("BATCH").setStatus(20)); |
| | | var req = new MesProPalletBindBatchReqVO(); req.setBatchId(2L); req.setIds(List.of(1L)); |
| | | palletService.bindBatch(req); |
| | | var update = ArgumentCaptor.forClass(MesProPalletDO.class); |
| | | verify(pallets).updateById(update.capture()); |
| | | assertNull(update.getValue().getBusinessCode()); // MyBatis ignores null fields, so stable business code is retained. |
| | | assertEquals("2222222222222222", pallet.getBusinessCode()); |
| | | verify(bags).updatePalletCodeByPalletId(1L, update.getValue().getCode()); |
| | | } |
| | | |
| | | @Test void supplementationNeverChangesPreviouslyIssuedPalletOrBatchCode() { |
| | | when(pallets.selectByIdForUpdate(1L)).thenReturn(new MesProPalletDO().setId(1L).setStatus(10).setBusinessCode("1111111111111111")); |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setStatus(20).setBusinessCode("2222222222222222")); |
| | | assertEquals(0, palletService.generateBusinessCodes(List.of(1L, 1L))); |
| | | assertEquals(0, batchService.generateBusinessCodes(List.of(2L))); |
| | | verifyNoInteractions(codes); |
| | | verify(pallets, never()).updateById(any(MesProPalletDO.class)); |
| | | verify(batches, never()).updateById(any(MesProBatchDO.class)); |
| | | } |
| | | |
| | | @Test void supplementationRejectsCanceledAndMissingBeforeWritingAnyCode() { |
| | | when(pallets.selectByIdForUpdate(1L)).thenReturn(new MesProPalletDO().setId(1L).setStatus(10)); |
| | | when(pallets.selectByIdForUpdate(2L)).thenReturn(new MesProPalletDO().setId(2L).setStatus(20)); |
| | | when(batches.selectByIdForUpdate(1L)).thenReturn(new MesProBatchDO().setId(1L).setStatus(40)); |
| | | assertThrows(ServiceException.class, () -> palletService.generateBusinessCodes(List.of(1L, 2L))); |
| | | assertThrows(ServiceException.class, () -> batchService.generateBusinessCodes(List.of(1L))); |
| | | assertThrows(ServiceException.class, () -> batchService.generateBusinessCodes(List.of(3L))); |
| | | verifyNoInteractions(codes); |
| | | verify(pallets, never()).updateById(any(MesProPalletDO.class)); |
| | | } |
| | | |
| | | @Test void publicQueryRemainsSeparateFromInternalBusinessQuery() { |
| | | palletService.getPalletByCode("old"); batchService.getBatchByCode("old"); |
| | | verify(pallets).selectByCode("old"); verify(pallets).selectByTempCode("old"); |
| | | verify(batches).selectByCode("old"); verify(batches).selectByTempCode("old"); |
| | | palletService.getPalletByScanCode("business"); batchService.getBatchByScanCode("business"); |
| | | verify(pallets).selectByScanCode("business"); verify(batches).selectByScanCode("business"); |
| | | assertEquals("1111111111111111", MesProQrLabelBuilder.buildPalletBusinessCodeLabel( |
| | | new MesProPalletDO().setBusinessCode("1111111111111111")).getCode()); |
| | | assertEquals("2222222222222222", MesProQrLabelBuilder.buildBatchBusinessCodeLabel( |
| | | new MesProBatchDO().setBusinessCode("2222222222222222")).getCode()); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.bagcode; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO; |
| | | import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletServiceImpl; |
| | | import org.junit.jupiter.api.*; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class PalletItemBindingTest { |
| | | final MesProPalletServiceImpl service=new MesProPalletServiceImpl(); |
| | | final MesProPalletMapper pallets=mock(MesProPalletMapper.class); |
| | | final MesProBatchMapper batches=mock(MesProBatchMapper.class); |
| | | final MesProBagCodeMapper bags=mock(MesProBagCodeMapper.class); |
| | | final MesCodeRedisDAO codes=mock(MesCodeRedisDAO.class); |
| | | MesProBatchDO batch; |
| | | MesProPalletBindBatchReqVO req; |
| | | @BeforeEach void setup(){ |
| | | ReflectionTestUtils.setField(service,"palletMapper",pallets);ReflectionTestUtils.setField(service,"batchMapper",batches); |
| | | ReflectionTestUtils.setField(service,"bagCodeMapper",bags);ReflectionTestUtils.setField(service,"codeRedisDao",codes); |
| | | ReflectionTestUtils.setField(service,"integrityService",mock(cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService.class)); |
| | | batch=new MesProBatchDO().setId(8L).setCode("BATCH").setStatus(20); |
| | | when(batches.selectByIdForUpdate(8L)).thenReturn(batch);when(codes.generate()).thenReturn("1234567890123456"); |
| | | req=new MesProPalletBindBatchReqVO();req.setBatchId(8L);req.setIds(List.of(1L,2L)); |
| | | when(pallets.selectByIdForUpdate(1L)).thenReturn(new MesProPalletDO().setId(1L).setItemId(5L).setStatus(10)); |
| | | when(pallets.selectByIdForUpdate(2L)).thenReturn(new MesProPalletDO().setId(2L).setItemId(5L).setStatus(10)); |
| | | } |
| | | @Test void sameVarietyFillsBatchAndPallets(){ |
| | | assertEquals(2,service.bindBatch(req)); |
| | | var saved=ArgumentCaptor.forClass(MesProBatchDO.class);verify(batches).updateById(saved.capture());assertEquals(5L,saved.getValue().getItemId()); |
| | | var savedPallet=ArgumentCaptor.forClass(MesProPalletDO.class);verify(pallets,times(2)).updateById(savedPallet.capture()); |
| | | assertTrue(savedPallet.getAllValues().stream().allMatch(p->Long.valueOf(5).equals(p.getItemId()))); |
| | | } |
| | | @Test void mixedPalletVarietiesRejectBeforeAnyWrite(){ |
| | | when(pallets.selectByIdForUpdate(2L)).thenReturn(new MesProPalletDO().setId(2L).setItemId(6L));assertRejected(); |
| | | } |
| | | @Test void existingBatchVarietyCannotBeOverwritten(){batch.setItemId(6L);assertRejected();} |
| | | @Test void existingBatchBagsAlsoConstrainVariety(){when(bags.selectDistinctItemIdsByBatchId(8L)).thenReturn(List.of(6L));assertRejected();} |
| | | @Test void palletMetadataCannotHideMixedBagVarieties(){ |
| | | when(bags.selectListByPalletId(1L)).thenReturn(List.of(new MesProBagCodeDO().setItemId(6L).setStatus(10)));assertRejected(); |
| | | } |
| | | @Test void missingVarietyCannotBeBound(){when(pallets.selectByIdForUpdate(2L)).thenReturn(new MesProPalletDO().setId(2L));assertRejected();} |
| | | @Test void historicalPalletInfersVarietyFromBags(){ |
| | | when(pallets.selectByIdForUpdate(1L)).thenReturn(new MesProPalletDO().setId(1L)); |
| | | when(bags.selectListByPalletId(1L)).thenReturn(List.of(new MesProBagCodeDO().setItemId(5L).setStatus(10),new MesProBagCodeDO().setItemId(6L).setStatus(30))); |
| | | assertEquals(2,service.bindBatch(req)); |
| | | } |
| | | @Test void batchCreateCannotForgeVarietyDifferentFromSelectedPallet(){ |
| | | var batchService=new cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchServiceImpl(); |
| | | ReflectionTestUtils.setField(batchService,"palletMapper",pallets);ReflectionTestUtils.setField(batchService,"bagCodeMapper",bags); |
| | | ReflectionTestUtils.setField(batchService,"adminUserApi",mock(cn.iocoder.yudao.module.system.api.user.AdminUserApi.class)); |
| | | var lines=mock(cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService.class); |
| | | ReflectionTestUtils.setField(batchService,"lineService",lines); |
| | | when(lines.validateLineExistsAndEnable(1L)).thenReturn(new cn.iocoder.yudao.module.mes.dal.dataobject.pro.line.MesProLineDO().setId(1L)); |
| | | var request=new cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO(); |
| | | request.setLineId(1L);request.setItemId(99L);request.setPalletIds(List.of(1L));request.setWorkers(List.of()); |
| | | assertThrows(ServiceException.class,()->batchService.createBatch(request)); |
| | | } |
| | | @Test void editingBatchCannotChangeItsExistingVariety(){ |
| | | var batchService=new cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchServiceImpl(); |
| | | ReflectionTestUtils.setField(batchService,"batchMapper",batches);ReflectionTestUtils.setField(batchService,"palletMapper",pallets); |
| | | ReflectionTestUtils.setField(batchService,"bagCodeMapper",bags); |
| | | ReflectionTestUtils.setField(batchService,"adminUserApi",mock(cn.iocoder.yudao.module.system.api.user.AdminUserApi.class)); |
| | | batch.setItemId(5L); |
| | | var request=new cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO(); |
| | | request.setId(8L);request.setItemId(99L);request.setWorkers(List.of()); |
| | | assertThrows(ServiceException.class,()->batchService.updateBatch(request));verify(batches,never()).updateById(any(MesProBatchDO.class)); |
| | | } |
| | | void assertRejected(){assertThrows(ServiceException.class,()->service.bindBatch(req));verify(pallets,never()).updateById(any(MesProPalletDO.class));verify(batches,never()).updateById(any(MesProBatchDO.class));verifyNoInteractions(codes);} |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.scan; |
| | | |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import org.apache.ibatis.annotations.Update; |
| | | import org.h2.jdbcx.JdbcDataSource; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | /** æ§è¡ Mapper ä¸çå®é
SQLï¼éªè¯æ··æ«æ¶ç¶æä¸è¢æ°çç»æã */ |
| | | class InboundCompletionSqlTest { |
| | | private JdbcTemplate db; |
| | | private NamedParameterJdbcTemplate named; |
| | | @BeforeEach void setup() { |
| | | JdbcDataSource source = new JdbcDataSource(); |
| | | source.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"); |
| | | db = new JdbcTemplate(source); named = new NamedParameterJdbcTemplate(source); |
| | | db.execute("CREATE TABLE mes_pro_batch(id BIGINT PRIMARY KEY,status INT,deleted INT DEFAULT 0,inbound_time TIMESTAMP,inbound_event_id BIGINT,inbound_device_code VARCHAR(64),inbound_line_code VARCHAR(64),inbound_bag_quantity INT,update_time TIMESTAMP)"); |
| | | db.execute("CREATE TABLE mes_pro_pallet(id BIGINT PRIMARY KEY,batch_id BIGINT,status INT,deleted INT DEFAULT 0,inbound_time TIMESTAMP,inbound_event_id BIGINT,update_time TIMESTAMP)"); |
| | | db.execute("CREATE TABLE mes_pro_bag_code(id BIGINT PRIMARY KEY,batch_id BIGINT,pallet_id BIGINT,status INT,deleted INT DEFAULT 0,inbound_time TIMESTAMP,inbound_event_id BIGINT,inbound_device_code VARCHAR(64),inbound_line_code VARCHAR(64),update_time TIMESTAMP)"); |
| | | db.update("INSERT INTO mes_pro_batch(id,status) VALUES(1,20)"); |
| | | db.update("INSERT INTO mes_pro_pallet(id,batch_id,status) VALUES(1,1,10),(2,1,10)"); |
| | | db.update("INSERT INTO mes_pro_bag_code(id,batch_id,pallet_id,status) VALUES(1,1,1,20),(2,1,1,20),(3,1,2,20),(4,1,2,30)"); |
| | | } |
| | | private int run(Class<?> mapper, String method, Map<String,Object> values) { |
| | | var target = java.util.Arrays.stream(mapper.getMethods()).filter(m -> m.getName().equals(method)).findFirst().orElseThrow(); |
| | | String sql = String.join(" ", target.getAnnotation(Update.class).value()).replaceAll("#\\{(\\w+)\\}", ":$1"); |
| | | return named.update(sql, values); |
| | | } |
| | | private Map<String,Object> args(Long id) { |
| | | Map<String,Object> values = new HashMap<>(); |
| | | values.put("id",id);values.put("batchId",1L);values.put("palletId",id); |
| | | values.put("eventId",10L);values.put("deviceCode","PDA");values.put("lineCode","L1"); |
| | | return values; |
| | | } |
| | | private void refresh() { |
| | | run(MesProPalletMapper.class,"markCompletedInboundByBatchId",args(1L)); |
| | | run(MesProBatchMapper.class,"refreshInboundProgress",args(1L)); |
| | | } |
| | | private int batchValue(String column) { return db.queryForObject("SELECT " + column + " FROM mes_pro_batch WHERE id=1",Integer.class); } |
| | | @Test void bagThenPalletThenLastBagCompletesOnlyAtTheEnd() { |
| | | run(MesProBagCodeMapper.class,"markInbound",args(1L));refresh(); |
| | | assertEquals(20,batchValue("status")); assertEquals(1,batchValue("inbound_bag_quantity")); |
| | | assertNull(db.queryForObject("SELECT inbound_event_id FROM mes_pro_pallet WHERE id=1",Long.class)); |
| | | assertEquals(1,run(MesProBagCodeMapper.class,"markInboundByPalletId",args(1L))); |
| | | refresh();assertEquals(20,batchValue("status"));assertEquals(2,batchValue("inbound_bag_quantity")); |
| | | assertEquals(10L,db.queryForObject("SELECT inbound_event_id FROM mes_pro_pallet WHERE id=1",Long.class)); |
| | | run(MesProBagCodeMapper.class,"markInbound",args(3L));refresh(); |
| | | assertEquals(30,batchValue("status"));assertEquals(3,batchValue("inbound_bag_quantity")); |
| | | assertEquals(10L,db.queryForObject("SELECT inbound_event_id FROM mes_pro_pallet WHERE id=2",Long.class)); |
| | | assertNull(db.queryForObject("SELECT inbound_event_id FROM mes_pro_bag_code WHERE id=4",Long.class)); |
| | | assertEquals(0,run(MesProBagCodeMapper.class,"markInboundByBatchId",args(1L))); |
| | | refresh();assertEquals(3,batchValue("inbound_bag_quantity")); |
| | | } |
| | | @Test void wholeBatchExcludesCanceledAndDeletedBags() { |
| | | db.update("UPDATE mes_pro_bag_code SET deleted=1 WHERE id=2"); |
| | | assertEquals(2,run(MesProBagCodeMapper.class,"markInboundByBatchId",args(1L))); |
| | | refresh();assertEquals(30,batchValue("status"));assertEquals(2,batchValue("inbound_bag_quantity")); |
| | | } |
| | | @Test void emptyBatchDoesNotCompleteAndCanceledBatchDoesNotChange() { |
| | | db.update("DELETE FROM mes_pro_bag_code");refresh();assertEquals(20,batchValue("status")); |
| | | db.update("UPDATE mes_pro_batch SET status=40");refresh();assertEquals(40,batchValue("status")); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.scan; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| | | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventCreateReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.scan.MesProScanEventMapper; |
| | | import org.junit.jupiter.api.*; |
| | | import org.springframework.security.core.context.SecurityContextHolder; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | class MobileScanRetryTest { |
| | | private final MesProScanEventMapper mapper=mock(MesProScanEventMapper.class); |
| | | private final MesProScanEventServiceImpl service=new MesProScanEventServiceImpl(); |
| | | private MesProScanEventCreateReqVO req; |
| | | private MesProScanEventDO existing; |
| | | @BeforeEach void setup(){ |
| | | SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(7L),new org.springframework.mock.web.MockHttpServletRequest()); |
| | | ReflectionTestUtils.setField(service,"eventMapper",mapper); |
| | | req=new MesProScanEventCreateReqVO();req.setEventId("original");req.setBagCode("123");req.setQuantity(new BigDecimal("1.00"));req.setLocationId(2L);req.setAreaId(3L); |
| | | existing=new MesProScanEventDO().setEventId("original").setBagCode("123").setScanType("BAG").setQuantity(BigDecimal.ONE).setLocationId(2L).setAreaId(3L).setStatus("SUCCESS"); |
| | | existing.setCreator("7"); |
| | | when(mapper.selectByEventIdForUpdate("original")).thenReturn(existing); |
| | | } |
| | | @AfterEach void cleanup(){SecurityContextHolder.clearContext();} |
| | | @Test void sameOperationReturnsOriginalWithoutAnotherInventoryWrite(){ |
| | | assertSame(existing,service.createInbound(req));verifyNoMoreInteractionsAfterLookup(); |
| | | } |
| | | @Test void conflictNeverOverwritesOriginalSuccess(){ |
| | | req.setAreaId(4L); |
| | | assertThrows(ServiceException.class,()->service.createInbound(req)); |
| | | assertEquals("SUCCESS",existing.getStatus());assertNull(existing.getErrorCode()); |
| | | verifyNoMoreInteractionsAfterLookup(); |
| | | } |
| | | @Test void anotherUserCannotReplayOriginal(){ |
| | | existing.setCreator("8"); |
| | | assertThrows(ServiceException.class,()->service.createInbound(req));verifyNoMoreInteractionsAfterLookup(); |
| | | } |
| | | @Test void changedBagQuantityCannotReuseOriginalOperation(){ |
| | | req.setQuantity(BigDecimal.TEN); |
| | | assertThrows(ServiceException.class,()->service.createInbound(req));verifyNoMoreInteractionsAfterLookup(); |
| | | } |
| | | @Test void palletReplayKeepsServerCalculatedQuantity(){ |
| | | req.setScanType("PALLET");existing.setScanType("PALLET").setQuantity(BigDecimal.TEN); |
| | | assertSame(existing,service.createInbound(req));assertEquals(BigDecimal.TEN,existing.getQuantity()); |
| | | verifyNoMoreInteractionsAfterLookup(); |
| | | } |
| | | private void verifyNoMoreInteractionsAfterLookup(){verify(mapper).selectByEventIdForUpdate("original");verifyNoMoreInteractions(mapper);} |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.scan; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventCreateReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.scan.MesProScanEventMapper; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; |
| | | import org.junit.jupiter.api.*; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class MobileScanValidityTest { |
| | | private final MesProScanEventServiceImpl service=new MesProScanEventServiceImpl(); |
| | | private final MesProScanEventMapper events=mock(MesProScanEventMapper.class); |
| | | private final MesProBagCodeMapper bags=mock(MesProBagCodeMapper.class); |
| | | private final MesProPalletMapper pallets=mock(MesProPalletMapper.class); |
| | | private final MesProBatchMapper batches=mock(MesProBatchMapper.class); |
| | | private final MesWmTransactionService transactions=mock(MesWmTransactionService.class); |
| | | private MesProScanEventCreateReqVO req; |
| | | @BeforeEach void setup(){ |
| | | ReflectionTestUtils.setField(service,"eventMapper",events); |
| | | ReflectionTestUtils.setField(service,"bagCodeMapper",bags); |
| | | ReflectionTestUtils.setField(service,"palletMapper",pallets); |
| | | ReflectionTestUtils.setField(service,"batchMapper",batches); |
| | | ReflectionTestUtils.setField(service,"transactionService",transactions); |
| | | req=new MesProScanEventCreateReqVO();req.setEventId("new");req.setBagCode("123"); |
| | | req.setQuantity(BigDecimal.ONE);req.setLocationId(2L);req.setAreaId(3L); |
| | | } |
| | | @Test void canceledBagDoesNotCreateStock(){ |
| | | when(bags.selectByScanCodeForUpdate("123")).thenReturn(new MesProBagCodeDO().setStatus(30)); |
| | | assertFailed("MES_SCAN_BAG_CANCELED"); |
| | | } |
| | | @Test void canceledPalletDoesNotCreateStock(){ |
| | | req.setScanType("PALLET"); |
| | | when(pallets.selectByScanCodeForUpdate("123")).thenReturn(new MesProPalletDO().setStatus(20)); |
| | | assertFailed("MES_SCAN_PALLET_CANCELED"); |
| | | } |
| | | @Test void canceledBatchDoesNotCreateStock(){ |
| | | req.setScanType("BATCH"); |
| | | when(batches.selectByScanCodeForUpdate("123")).thenReturn(new MesProBatchDO().setId(2L).setStatus(40).setBagQuantity(1)); |
| | | assertFailed("MES_SCAN_BATCH_CANCELED"); |
| | | } |
| | | @Test void bagInCanceledBatchDoesNotCreateStock(){ |
| | | when(bags.selectByScanCodeForUpdate("123")).thenReturn(new MesProBagCodeDO().setId(1L).setBatchId(2L).setStatus(20)); |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setStatus(40)); |
| | | assertFailed("MES_SCAN_BATCH_CANCELED"); |
| | | } |
| | | @Test void invalidTypeCannotFallThroughToBagInbound(){ |
| | | req.setScanType("UNKNOWN"); |
| | | assertThrows(ServiceException.class,()->service.createInbound(req)); |
| | | verifyNoInteractions(events,bags,pallets,batches,transactions); |
| | | } |
| | | @Test void businessScanWritesCanonicalTraceCodeAndSameBagId(){ |
| | | req.setBagCode("8888888888888888"); |
| | | when(bags.selectByScanCodeForUpdate(req.getBagCode())).thenReturn(new MesProBagCodeDO() |
| | | .setId(1L).setCode("9999999999999999").setBusinessCode(req.getBagCode()).setBatchId(2L).setStatus(20)); |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setStatus(20).setCode("BATCH").setItemId(3L)); |
| | | var warehouse=mock(cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService.class); |
| | | var wmBatch=mock(cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService.class); |
| | | ReflectionTestUtils.setField(service,"warehouseService",warehouse); |
| | | ReflectionTestUtils.setField(service,"mesWmBatchService",wmBatch); |
| | | when(warehouse.getWarehouseByCode(anyString())).thenReturn(new cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO().setId(4L)); |
| | | when(wmBatch.getOrCreateByProBatch(any())).thenReturn(new cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO().setId(5L)); |
| | | var result=service.createInbound(req); |
| | | assertEquals("SUCCESS",result.getStatus()); |
| | | var tx=org.mockito.ArgumentCaptor.forClass(cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO.class); |
| | | verify(transactions).createTransaction(tx.capture()); |
| | | assertEquals(1L,tx.getValue().getBagCodeId()); |
| | | assertEquals("9999999999999999",tx.getValue().getBagCode()); |
| | | assertEquals("8888888888888888",result.getBagCode()); |
| | | } |
| | | @Test void palletBusinessScanUsesActualBagCountAndOriginalBatch(){ |
| | | req.setScanType("PALLET");req.setBagCode("7777777777777777"); |
| | | when(pallets.selectByScanCodeForUpdate(req.getBagCode())).thenReturn(new MesProPalletDO().setId(7L).setBatchId(2L).setStatus(0).setBagCount(25)); |
| | | when(bags.selectPendingInboundCountByPalletId(7L)).thenReturn(25); |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setStatus(20).setCode("BATCH").setItemId(3L)); |
| | | assertWholeInbound(7L,25); |
| | | verify(pallets).markInbound(eq(7L),any()); |
| | | } |
| | | @Test void batchBusinessScanUsesActualBagCountAndOriginalBatch(){ |
| | | req.setScanType("BATCH");req.setBagCode("6666666666666666"); |
| | | when(batches.selectByScanCodeForUpdate(req.getBagCode())).thenReturn(new MesProBatchDO().setId(2L).setStatus(20).setCode("BATCH").setItemId(3L).setBagQuantity(50)); |
| | | when(bags.selectPendingInboundCountByBatchId(2L)).thenReturn(50); |
| | | assertWholeInbound(2L,50); |
| | | verify(batches).refreshInboundProgress(eq(2L),any(),any(),any()); |
| | | } |
| | | @Test void partlyInboundPalletWritesOnlyPendingBags(){ |
| | | req.setScanType("PALLET"); |
| | | when(pallets.selectByScanCodeForUpdate("123")).thenReturn(new MesProPalletDO().setId(7L).setBatchId(2L).setStatus(10).setBagCount(25)); |
| | | when(batches.selectByIdForUpdate(2L)).thenReturn(new MesProBatchDO().setId(2L).setStatus(20).setCode("BATCH").setItemId(3L)); |
| | | when(bags.selectPendingInboundCountByPalletId(7L)).thenReturn(24); |
| | | assertWholeInbound(7L,24); |
| | | } |
| | | @Test void partlyInboundBatchIsNotRejectedByEarlierBagEvent(){ |
| | | req.setScanType("BATCH"); |
| | | when(batches.selectByScanCodeForUpdate("123")).thenReturn(new MesProBatchDO().setId(2L).setStatus(20).setInboundEventId(9L).setCode("BATCH").setItemId(3L).setBagQuantity(25)); |
| | | when(bags.selectPendingInboundCountByBatchId(2L)).thenReturn(24); |
| | | assertWholeInbound(2L,24); |
| | | } |
| | | @Test void fullyInboundBatchDoesNotCreateAnotherStockTransaction(){ |
| | | req.setScanType("BATCH"); |
| | | when(batches.selectByScanCodeForUpdate("123")).thenReturn(new MesProBatchDO().setId(2L).setStatus(30).setCode("BATCH")); |
| | | assertFailed("MES_SCAN_NO_PENDING_BAGS"); |
| | | } |
| | | private void assertWholeInbound(Long targetId,int quantity){ |
| | | var warehouse=mock(cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService.class); |
| | | var wmBatch=mock(cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService.class); |
| | | ReflectionTestUtils.setField(service,"warehouseService",warehouse); |
| | | ReflectionTestUtils.setField(service,"mesWmBatchService",wmBatch); |
| | | when(warehouse.getWarehouseByCode(anyString())).thenReturn(new cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO().setId(4L)); |
| | | when(wmBatch.getOrCreateByProBatch(any())).thenReturn(new cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO().setId(5L)); |
| | | var result=service.createInbound(req);assertEquals("SUCCESS",result.getStatus()); |
| | | assertEquals(req.getBagCode(),result.getBagCode());assertEquals(BigDecimal.valueOf(quantity),result.getQuantity()); |
| | | var tx=org.mockito.ArgumentCaptor.forClass(cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO.class); |
| | | verify(transactions).createTransaction(tx.capture());assertEquals(targetId,tx.getValue().getBizLineId()); |
| | | assertEquals("BATCH",tx.getValue().getBatchCode());assertEquals(BigDecimal.valueOf(quantity),tx.getValue().getQuantity()); |
| | | } |
| | | private void assertFailed(String code){ |
| | | MesProScanEventDO result=service.createInbound(req); |
| | | assertEquals("FAILED",result.getStatus());assertEquals(code,result.getErrorCode()); |
| | | verify(events).insert(result);verify(events).updateById(result);verifyNoInteractions(transactions); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.qc.seedquality; |
| | | |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.*; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper; |
| | | import org.junit.jupiter.api.*; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class SeedQualityGroupTest { |
| | | private final MesQcSeedQualityServiceImpl service=spy(new MesQcSeedQualityServiceImpl()); |
| | | private final MesProBagCodeMapper bags=mock(MesProBagCodeMapper.class); |
| | | private final MesProBatchMapper batches=mock(MesProBatchMapper.class); |
| | | private final MesProPalletMapper pallets=mock(MesProPalletMapper.class); |
| | | private final MesQcSeedQualityMapper qualities=mock(MesQcSeedQualityMapper.class); |
| | | private final MesProBagCodeDO first=bag(1L),second=bag(2L),canceled=bag(3L).setStatus(30); |
| | | private MesQcSeedQualityGroupSubmitReqVO req; |
| | | private MesProBagCodeDO bag(Long id) {return new MesProBagCodeDO().setId(id).setCode("B"+id).setBatchId(10L).setPalletId(20L).setStatus(20).setInboundEventId(9L);} |
| | | @BeforeEach void setup() { |
| | | ReflectionTestUtils.setField(service,"bagCodeMapper",bags);ReflectionTestUtils.setField(service,"batchMapper",batches); |
| | | ReflectionTestUtils.setField(service,"palletMapper",pallets);ReflectionTestUtils.setField(service,"qualityMapper",qualities); |
| | | var batch=new MesProBatchDO().setId(10L).setCode("BATCH").setBusinessCode("BATCH-BUS").setStatus(30); |
| | | when(batches.selectByScanCode(anyString())).thenReturn(batch);when(batches.selectByScanCodeForUpdate(anyString())).thenReturn(batch); |
| | | var pallet=new MesProPalletDO().setId(20L).setCode("PALLET").setBusinessCode("PALLET-BUS").setBatchCode("BATCH").setBatchId(10L).setStatus(10); |
| | | when(pallets.selectByScanCode(anyString())).thenReturn(pallet);when(pallets.selectByScanCodeForUpdate(anyString())).thenReturn(pallet); |
| | | when(bags.selectListByBatchId(10L)).thenReturn(List.of(first,second,canceled)); |
| | | when(bags.selectListByBatchIdForUpdate(10L)).thenReturn(List.of(first,second,canceled)); |
| | | when(bags.selectListByPalletId(20L)).thenReturn(List.of(first,second,canceled)); |
| | | when(bags.selectListByPalletIdForUpdate(20L)).thenReturn(List.of(first,second,canceled)); |
| | | when(bags.selectListByIdsForUpdate(any())).thenReturn(List.of(first,second)); |
| | | req=new MesQcSeedQualityGroupSubmitReqVO();req.setCode("BATCH-BUS");req.setScanType("BATCH");req.setBagIds(List.of(1L,2L)); |
| | | req.setQualifiedWarehouseId(8L);req.setIndicators(List.of()); |
| | | doReturn(101L).when(service).submit(any(MesQcSeedQualitySubmitReqVO.class)); |
| | | } |
| | | @Test void batchPreviewCountsEffectiveInspectedAndPendingBags() { |
| | | when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build())); |
| | | var result=service.resolveScan("BATCH-BUS","BATCH");assertEquals(2,result.get("totalCount")); |
| | | assertEquals(List.of(1L),result.get("completedBagIds"));assertEquals(List.of(2L),result.get("pendingBagIds")); |
| | | verify(service,never()).submit(any()); |
| | | } |
| | | @Test void palletProcessesOnlyConfirmedUninspectedBags() { |
| | | req.setScanType("PALLET");req.setCode("PALLET-BUS");req.setBagIds(List.of(2L)); |
| | | when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build())); |
| | | assertEquals(1,service.submitGroup(req).get("processedCount")); |
| | | var capture=org.mockito.ArgumentCaptor.forClass(MesQcSeedQualitySubmitReqVO.class);verify(service).submit(capture.capture()); |
| | | assertEquals("B2",capture.getValue().getCode());assertEquals(8L,capture.getValue().getQualifiedWarehouseId()); |
| | | } |
| | | @Test void changedConfirmedListDoesNotSubmitAnyBag() { |
| | | req.setBagIds(List.of(1L));assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any()); |
| | | } |
| | | @Test void unenteredBagBlocksWholeGroup() { |
| | | second.setInboundEventId(null);assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any()); |
| | | } |
| | | @Test void wrongTypeAndCanceledBatchCannotSubmit() { |
| | | req.setScanType("OTHER");assertThrows(IllegalArgumentException.class,()->service.submitGroup(req)); |
| | | req.setScanType("BATCH");when(batches.selectByScanCodeForUpdate(anyString())).thenReturn(new MesProBatchDO().setId(10L).setCode("BATCH").setStatus(40)); |
| | | assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any()); |
| | | } |
| | | @Test void singleBagUsesSameConfirmedGroupProtocol() { |
| | | req.setScanType("BAG");req.setCode("B1");req.setBagIds(List.of(1L)); |
| | | when(bags.selectByScanCode("B1")).thenReturn(first);when(bags.selectListByIdsForUpdate(any())).thenReturn(List.of(first)); |
| | | assertEquals(1,service.submitGroup(req).get("processedCount"));verify(service).submit(any()); |
| | | } |
| | | @Test void failingSecondBagRollsBackWholeTransaction() { |
| | | var source=new org.h2.jdbcx.JdbcDataSource();source.setURL("jdbc:h2:mem:qcgroup"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1"); |
| | | var jdbc=new org.springframework.jdbc.core.JdbcTemplate(source);jdbc.execute("CREATE TABLE qc_result(id BIGINT)"); |
| | | doAnswer(invocation->{var single=(MesQcSeedQualitySubmitReqVO)invocation.getArgument(0);jdbc.update("INSERT INTO qc_result(id) VALUES(?)",single.getCode().equals("B1")?1:2);if(single.getCode().equals("B2"))throw new IllegalStateException("åºåæµè½¬å¤±è´¥");return 101L;}).when(service).submit(any()); |
| | | var factory=new org.springframework.aop.framework.ProxyFactory(service); |
| | | factory.addAdvice(new org.springframework.transaction.interceptor.TransactionInterceptor(new org.springframework.jdbc.datasource.DataSourceTransactionManager(source),new org.springframework.transaction.annotation.AnnotationTransactionAttributeSource())); |
| | | var proxy=(MesQcSeedQualityService)factory.getProxy(); |
| | | assertThrows(IllegalStateException.class,()->proxy.submitGroup(req));assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM qc_result",Integer.class)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.qc.seedquality; |
| | | |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.util.Collections; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class SeedQualityScanTest { |
| | | private final MesQcSeedQualityServiceImpl service = new MesQcSeedQualityServiceImpl(); |
| | | private final MesProBagCodeMapper bags = mock(MesProBagCodeMapper.class); |
| | | private final MesQcSeedQualityMapper qualities = mock(MesQcSeedQualityMapper.class); |
| | | @BeforeEach void setup() { ReflectionTestUtils.setField(service,"bagCodeMapper",bags); ReflectionTestUtils.setField(service,"qualityMapper",qualities); } |
| | | private MesProBagCodeDO bag() { return new MesProBagCodeDO().setId(1L).setCode("TRACE").setBusinessCode("BUSINESS").setStatus(20).setInboundEventId(9L).setBatchCode("BATCH"); } |
| | | @Test void businessScanReturnsCanonicalCodeAndPreviousResultWithoutWrites() { |
| | | when(bags.selectByScanCode("BUSINESS")).thenReturn(bag()); |
| | | when(qualities.selectByCode("TRACE")).thenReturn(MesQcSeedQualityDO.builder().id(2L).result(1).build()); |
| | | var result = service.resolveScan("BUSINESS"); |
| | | assertEquals("TRACE",result.get("code"));assertEquals(2L,result.get("qualityId"));assertEquals(true,result.get("inbound")); |
| | | verify(qualities,never()).insert(any(MesQcSeedQualityDO.class)); |
| | | } |
| | | @Test void businessCodeRecordQueryUsesCanonicalCode() { |
| | | when(bags.selectByScanCode("BUSINESS")).thenReturn(bag()); |
| | | var req = new MesQcSeedQualityPageReqVO();req.setCode("BUSINESS");service.getQualityPage(req); |
| | | assertEquals("TRACE",req.getCode());verify(qualities).selectPage(req); |
| | | } |
| | | @Test void duplicateQualityIsRejectedAfterLockingSameBagForBothCodes() { |
| | | when(bags.selectByScanCodeForUpdate("BUSINESS")).thenReturn(bag()); |
| | | var req = new MesQcSeedQualitySubmitReqVO();req.setCode("BUSINESS"); |
| | | when(qualities.selectByCodeForUpdate("TRACE")).thenReturn(MesQcSeedQualityDO.builder().id(2L).result(1).build()); |
| | | req.setIndicators(Collections.nCopies(4,new MesQcSeedQualitySubmitReqVO.Indicator())); |
| | | assertThrows(IllegalArgumentException.class,()->service.submit(req)); |
| | | verify(qualities).selectByCodeForUpdate("TRACE");verify(qualities,never()).insert(any(MesQcSeedQualityDO.class)); |
| | | } |
| | | @Test void unenteredAndCanceledBagsCannotSubmit() { |
| | | var req = new MesQcSeedQualitySubmitReqVO();req.setCode("BUSINESS");req.setIndicators(Collections.nCopies(4,new MesQcSeedQualitySubmitReqVO.Indicator())); |
| | | when(bags.selectByScanCodeForUpdate("BUSINESS")).thenReturn(bag().setInboundEventId(null)); |
| | | assertThrows(IllegalArgumentException.class,()->service.submit(req)); |
| | | when(bags.selectByScanCodeForUpdate("BUSINESS")).thenReturn(bag().setStatus(30)); |
| | | assertThrows(IllegalArgumentException.class,()->service.submit(req));verifyNoInteractions(qualities); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.productsales; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO; |
| | | import org.junit.jupiter.api.Test; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | class BagBusinessOutboundTest { |
| | | @Test void businessCodeResolvesCanonicalBagForRepeatPick() { |
| | | var f=new SalesPickingSelectionTest();f.setup(); |
| | | when(f.bags.selectByScanCodeForUpdate("BUS-31")).thenReturn(f.b1); |
| | | when(f.details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(new MesWmProductSalesDetailDO().setId(77L).setSalesId(11L).setBagCodeId(31L))); |
| | | f.req.setScanType("BAG");f.req.setScanContent("BUS-31");f.req.setBagIds(null);f.req.setQuantity(BigDecimal.ONE); |
| | | var result=f.service.scanProductSales(f.req);assertEquals(31L,result.getCodeId());assertTrue(result.getAlreadyScanned());assertEquals(77L,result.getDetailId());f.noInsert(); |
| | | } |
| | | @Test void palletPreviewCountsSelectableEffectiveBags() { |
| | | var f=new SalesPickingSelectionTest();f.setup();f.b3.setStatus(30); |
| | | var result=f.service.previewScan("SOURCE");assertEquals("PALLET",result.getScanType());assertEquals(BigDecimal.valueOf(2),result.getScanQuantity());assertEquals(2,result.getBags().size());f.noInsert(); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.productsales; |
| | | |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.*; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.*; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.*; |
| | | import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; |
| | | import org.junit.jupiter.api.*; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import java.math.BigDecimal; |
| | | import java.util.*; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class SalesPickingSelectionTest { |
| | | final MesWmProductSalesServiceImpl service = new MesWmProductSalesServiceImpl(); |
| | | final MesWmProductSalesMapper sales = mock(MesWmProductSalesMapper.class); |
| | | final MesWmProductSalesDetailMapper details = mock(MesWmProductSalesDetailMapper.class); |
| | | final MesProBagCodeMapper bags = mock(MesProBagCodeMapper.class); |
| | | final MesProPalletMapper pallets = mock(MesProPalletMapper.class); |
| | | final MesProBatchMapper batches = mock(MesProBatchMapper.class); |
| | | final MesWmMaterialStockBagMapper inventory = mock(MesWmMaterialStockBagMapper.class); |
| | | final MesProBagCodeService bagService = mock(MesProBagCodeService.class); |
| | | final MesProPalletService palletService = mock(MesProPalletService.class); |
| | | final MesProBatchService batchService = mock(MesProBatchService.class); |
| | | final MesWmBatchService wmBatches = mock(MesWmBatchService.class); |
| | | final MesWmMaterialStockService stocks = mock(MesWmMaterialStockService.class); |
| | | final MesWmProductSalesLineService lines = mock(MesWmProductSalesLineService.class); |
| | | final MesWmProductSalesDetailService detailService = mock(MesWmProductSalesDetailService.class); |
| | | MesProBatchDO batch; |
| | | MesProBagCodeDO b1, b2, b3; |
| | | List<MesWmMaterialStockBagDO> stockBags; |
| | | MesWmProductSalesScanReqVO req; |
| | | |
| | | @BeforeEach void setup() { |
| | | Object[][] fields = {{"productSalesMapper",sales},{"productSalesDetailMapper",details},{"pickingBagMapper",bags}, |
| | | {"pickingPalletMapper",pallets},{"pickingBatchMapper",batches},{"pickingStockBagMapper",inventory}, |
| | | {"bagCodeService",bagService},{"palletService",palletService},{"proBatchService",batchService}, |
| | | {"mesWmBatchService",wmBatches},{"materialStockService",stocks},{"productSalesLineService",lines},{"productSalesDetailService",detailService}}; |
| | | for (var f : fields) ReflectionTestUtils.setField(service,(String)f[0],f[1]); |
| | | batch = new MesProBatchDO().setId(100L).setCode("PRO-100").setItemId(1L).setStatus(30); |
| | | b1=bag(31L); b2=bag(32L); b3=bag(33L); |
| | | var pallet = new MesProPalletDO().setId(200L).setBatchId(100L).setStatus(10); |
| | | when(sales.selectByIdForUpdate(11L)).thenReturn(new MesWmProductSalesDO().setId(11L).setStatus(2)); |
| | | when(sales.selectById(11L)).thenReturn(new MesWmProductSalesDO().setId(11L).setStatus(2)); |
| | | when(batchService.getBatch(100L)).thenReturn(batch); |
| | | when(batchService.getBatchByScanCode("SOURCE")).thenReturn(batch); |
| | | when(batches.selectByScanCodeForUpdate("SOURCE")).thenReturn(batch); |
| | | when(pallets.selectByScanCodeForUpdate("SOURCE")).thenReturn(pallet); |
| | | when(palletService.getPalletByScanCode("SOURCE")).thenReturn(pallet); |
| | | when(bags.selectListByPalletIdForUpdate(200L)).thenReturn(List.of(b1,b2,b3)); |
| | | when(bags.selectListByBatchIdForUpdate(100L)).thenReturn(List.of(b1,b2,b3)); |
| | | when(bagService.getBagCodeListByPalletId(200L)).thenReturn(List.of(b1,b2,b3)); |
| | | when(bagService.getBagCodeListByBatchId(100L)).thenReturn(List.of(b1,b2,b3)); |
| | | when(wmBatches.getBatchByCode("PRO-100")).thenReturn(new MesWmBatchDO().setId(500L).setCode("PRO-100")); |
| | | stockBags = List.of(stockBag(31L,900L),stockBag(32L,901L),stockBag(33L,900L)); |
| | | when(inventory.selectByBagIds(anyCollection(),anyBoolean())).thenAnswer(i -> stockBags.stream().filter(b -> ((Collection<?>)i.getArgument(0)).contains(b.getBagCodeId())).toList()); |
| | | when(stocks.getQualifiedMaterialStockListByItemAndBatch(1L,500L)).thenReturn(List.of(stock(900L,8L),stock(901L,9L))); |
| | | when(lines.getProductSalesLineListBySalesId(11L)).thenReturn(List.of(new MesWmProductSalesLineDO().setId(21L).setItemId(1L).setQuantity(BigDecimal.TEN))); |
| | | req = new MesWmProductSalesScanReqVO(); req.setSalesId(11L);req.setScanContent("SOURCE");req.setScanType("PALLET"); req.setBagIds(List.of(31L,32L));req.setQuantity(BigDecimal.valueOf(2)); |
| | | } |
| | | MesProBagCodeDO bag(Long id) { return new MesProBagCodeDO().setId(id).setCode("TRACE-"+id).setBusinessCode("BUS-"+id).setBatchId(100L).setPalletId(200L).setStatus(20).setQcStatus(20); } |
| | | MesWmMaterialStockBagDO stockBag(Long bagId,Long stockId) { return MesWmMaterialStockBagDO.builder().bagCodeId(bagId).materialStockId(stockId).itemId(1L).batchId(500L).status(1).stockState(20).frozen(false).quantity(BigDecimal.ONE).build(); } |
| | | MesWmMaterialStockDO stock(Long id,Long warehouse) { return new MesWmMaterialStockDO().setId(id).setWarehouseId(warehouse).setQuantity(BigDecimal.TEN); } |
| | | void noInsert() { verify(details,never()).insert(any(MesWmProductSalesDetailDO.class)); } |
| | | |
| | | @Test void partialPalletWritesExactlySelectedBagsAtTheirActualLocations() { |
| | | var result=service.scanProductSales(req); |
| | | var c=ArgumentCaptor.forClass(MesWmProductSalesDetailDO.class);verify(details,times(2)).insert(c.capture()); |
| | | assertEquals(List.of(31L,32L),c.getAllValues().stream().map(MesWmProductSalesDetailDO::getBagCodeId).toList()); |
| | | assertEquals(List.of(8L,9L),c.getAllValues().stream().map(MesWmProductSalesDetailDO::getWarehouseId).toList()); |
| | | assertTrue(c.getAllValues().stream().allMatch(d -> BigDecimal.ONE.equals(d.getQuantity()) && d.getRemark().contains("TRACE-"))); |
| | | assertEquals(BigDecimal.valueOf(2),result.getOutboundQuantity()); |
| | | } |
| | | @Test void partialBatchAlsoWritesPerBag() { req.setScanType("BATCH");var r=service.scanProductSales(req); assertEquals("BATCH",r.getScanType());verify(details,times(2)).insert(any(MesWmProductSalesDetailDO.class)); } |
| | | @Test void scopeAndQuantityForgeryAreRejected() { |
| | | req.setBagIds(List.of(31L,999L));assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req)); |
| | | req.setBagIds(List.of(31L,32L));req.setQuantity(BigDecimal.ONE);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void duplicateOrMissingSelectionIsRejected() { |
| | | req.setBagIds(List.of(31L,31L));assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req)); |
| | | req.setBagIds(null);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void unqualifiedUnselectedBagDoesNotBlockPartialPick() { b3.setQcStatus(10);assertFalse(service.scanProductSales(req).getAlreadyScanned()); } |
| | | @Test void selectedUnqualifiedFrozenOrOutboundBagBlocksEntireRequest() { |
| | | b2.setQcStatus(10);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));b2.setQcStatus(20); |
| | | stockBags.get(1).setFrozen(true);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));stockBags.get(1).setFrozen(false); |
| | | stockBags.get(1).setStatus(2);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void crossOrderAndLegacyWholePalletOverlapAreRejected() { |
| | | when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(new MesWmProductSalesDetailDO().setSalesId(12L).setBagCodeId(31L))); |
| | | assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req)); |
| | | when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(new MesWmProductSalesDetailDO().setSalesId(11L).setPalletCodeId(200L))); |
| | | assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void sameExactBagsAreIdempotentButPartialRepeatIsRejected() { |
| | | var d1=new MesWmProductSalesDetailDO().setSalesId(11L).setBagCodeId(31L);var d2=new MesWmProductSalesDetailDO().setSalesId(11L).setBagCodeId(32L); |
| | | when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(d1,d2));assertTrue(service.scanProductSales(req).getAlreadyScanned()); |
| | | when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(d1));assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void previewReportsAvailabilityWithoutWrites() { |
| | | b3.setQcStatus(10);stockBags.get(1).setFrozen(true); |
| | | var r=service.previewScan("SOURCE","BATCH",11L);assertEquals(BigDecimal.ONE,r.getScanQuantity());assertEquals(3,r.getBagCount()); |
| | | assertTrue(r.getBags().get(0).getSelectable());assertEquals("è¢ç å·²å»ç»",r.getBags().get(1).getUnavailableReason());noInsert();verify(wmBatches,never()).getOrCreateByProBatch(any()); |
| | | } |
| | | @Test void canceledBatchInvalidTypeAndPlanOverflowRejectWrites() { |
| | | batch.setStatus(40);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));batch.setStatus(30); |
| | | req.setScanType("OTHER");assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));req.setScanType("BATCH"); |
| | | when(lines.getProductSalesLineListBySalesId(11L)).thenReturn(List.of(new MesWmProductSalesLineDO().setId(21L).setItemId(1L).setQuantity(BigDecimal.ONE))); |
| | | assertThrows(RuntimeException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void aggregateStockInsufficientRejectsAllRows() { |
| | | when(stocks.getQualifiedMaterialStockListByItemAndBatch(1L,500L)).thenReturn(List.of(stock(900L,8L).setQuantity(BigDecimal.ZERO),stock(901L,9L))); |
| | | assertThrows(RuntimeException.class,()->service.scanProductSales(req));noInsert(); |
| | | } |
| | | @Test void detailResponseKeepsBagAndPalletAssociationsForRecovery() { |
| | | var source=new MesWmProductSalesDetailDO().setBagCodeId(31L).setPalletCodeId(200L).setQuantity(BigDecimal.ONE); |
| | | var response=cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(source,cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.detail.MesWmProductSalesDetailRespVO.class); |
| | | assertEquals(31L,response.getBagCodeId());assertEquals(200L,response.getPalletCodeId()); |
| | | } |
| | | @Test void secondInsertFailureRollsBackAllSelectedBagDetails() { |
| | | var source=new org.h2.jdbcx.JdbcDataSource();source.setURL("jdbc:h2:mem:pick"+UUID.randomUUID()+";DB_CLOSE_DELAY=-1"); |
| | | var jdbc=new org.springframework.jdbc.core.JdbcTemplate(source);jdbc.execute("CREATE TABLE picking(bag_id BIGINT)"); |
| | | doAnswer(i->{MesWmProductSalesDetailDO d=i.getArgument(0);jdbc.update("INSERT INTO picking VALUES (?)",d.getBagCodeId());if(d.getBagCodeId()==32L)throw new IllegalStateException("第äºè¢åå
¥å¤±è´¥");return 1;}).when(details).insert(any(MesWmProductSalesDetailDO.class)); |
| | | var factory=new org.springframework.aop.framework.ProxyFactory(service); |
| | | factory.addAdvice(new org.springframework.transaction.interceptor.TransactionInterceptor(new org.springframework.jdbc.datasource.DataSourceTransactionManager(source),new org.springframework.transaction.annotation.AnnotationTransactionAttributeSource())); |
| | | var proxy=(MesWmProductSalesService)factory.getProxy(); |
| | | assertThrows(IllegalStateException.class,()->proxy.scanProductSales(req));assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM picking",Integer.class)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.productsales; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesDetailMapper; |
| | | import org.apache.ibatis.annotations.Select; |
| | | import org.h2.jdbcx.JdbcDataSource; |
| | | import org.junit.jupiter.api.*; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; |
| | | import java.util.*; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | /** æ§è¡ç产 Mapper ä¸çå®é
å ç¨æ¥è¯¢ SQLã */ |
| | | class SalesPickingSqlTest { |
| | | JdbcTemplate db;NamedParameterJdbcTemplate named; |
| | | @BeforeEach void setup(){ |
| | | var source=new JdbcDataSource();source.setURL("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1"); |
| | | db=new JdbcTemplate(source);named=new NamedParameterJdbcTemplate(source); |
| | | db.execute("CREATE TABLE mes_wm_product_sales(id BIGINT PRIMARY KEY,status INT,deleted INT DEFAULT 0)"); |
| | | db.execute("CREATE TABLE mes_wm_product_sales_detail(id BIGINT PRIMARY KEY,sales_id BIGINT,bag_code_id BIGINT,pallet_code_id BIGINT,batch_code VARCHAR(64),deleted INT DEFAULT 0)"); |
| | | db.update("INSERT INTO mes_wm_product_sales(id,status) VALUES(1,2),(2,10),(3,3),(4,4),(5,5)"); |
| | | db.update("INSERT INTO mes_wm_product_sales_detail(id,sales_id,bag_code_id,pallet_code_id,batch_code) VALUES(1,1,31,200,'B1'),(2,2,32,200,'B1'),(3,3,NULL,201,'B1'),(4,4,34,202,'B1'),(5,5,35,203,'B1'),(6,1,36,204,'B2'),(7,1,37,205,'B1')"); |
| | | db.update("UPDATE mes_wm_product_sales_detail SET deleted=1 WHERE id=7"); |
| | | } |
| | | List<Long> run(String method)throws Exception { |
| | | String sql=String.join(" ",MesWmProductSalesDetailMapper.class.getMethod(method,String.class).getAnnotation(Select.class).value()).replace("#{batchCode}",":batchCode"); |
| | | return named.query(sql,Map.of("batchCode","B1"),(rs,n)->rs.getLong("id")); |
| | | } |
| | | @Test void onlyLivePickingOrdersAndLegacyPalletsOccupyBags()throws Exception{assertEquals(List.of(1L,2L,3L),run("selectPickingByBatchCode"));} |
| | | @Test void lockedCurrentReadHasSameScopeAndCanceledOrderReleasesBags()throws Exception{ |
| | | assertEquals(List.of(1L,2L,3L),run("selectPickingByBatchCodeForUpdate"));db.update("UPDATE mes_wm_product_sales SET status=5 WHERE id=1"); |
| | | assertEquals(List.of(2L,3L),run("selectPickingByBatchCodeForUpdate")); |
| | | } |
| | | } |
| | |
| | | @GetMapping("/get-permission-info") |
| | | @Operation(summary = "è·åç»å½ç¨æ·çæéä¿¡æ¯") |
| | | @DataPermission(enable = false) // å¿½ç¥æ°æ®æéï¼é¿å
å ä¸ºè¿æ»¤ï¼å¯¼è´æ æ³æ¥è¯¢ç¨æ·ã类似ï¼https://t.zsxq.com/LHnrp |
| | | public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() { |
| | | public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo( |
| | | @RequestParam(defaultValue = "pc") @jakarta.validation.constraints.Pattern(regexp = "pc|mobile") String terminalType) { |
| | | // 1.1 è·å¾ç¨æ·ä¿¡æ¯ |
| | | AdminUserDO user = userService.getUser(getLoginUserId()); |
| | | if (user == null) { |
| | |
| | | menuList = menuService.filterDisableMenus(menuList); |
| | | |
| | | // 2. æ¼æ¥ç»æè¿å |
| | | return success(AuthConvert.INSTANCE.convert(user, roles, menuList)); |
| | | AuthPermissionInfoRespVO result = AuthConvert.INSTANCE.convert(user, roles, menuList); |
| | | if ("mobile".equals(terminalType)) { |
| | | result.setMenus(MobileMenuAdapter.adapt(result.getMenus())); |
| | | } |
| | | return success(result); |
| | | } |
| | | |
| | | @PostMapping("/register") |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.controller.admin.auth; |
| | | |
| | | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO.MenuVO; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** Maps already-authorized, visible menus to implemented mobile pages. Does not grant permissions. */ |
| | | public final class MobileMenuAdapter { |
| | | private MobileMenuAdapter() { } |
| | | private static final Map<String, String> PAGES = Map.of( |
| | | "mes/pro/scan-in/index", "inbound", |
| | | "mes/pro/bagCode/index", "bags", |
| | | "mes/pro/pallet/index", "pallets", |
| | | "mes/pro/batch/index", "batches", |
| | | "wls/materialstock/index", "stock", |
| | | "mes/qc/seedQuality/index", "quality", |
| | | "mes/qc/seedQuality/record", "quality-records", |
| | | "wls/productsales/index", "sales"); |
| | | |
| | | public static List<MenuVO> adapt(List<MenuVO> menus) { |
| | | List<MenuVO> result = new ArrayList<>(); |
| | | collect(menus, result); |
| | | return result; |
| | | } |
| | | |
| | | private static void collect(List<MenuVO> menus, List<MenuVO> result) { |
| | | if (menus == null) return; |
| | | for (MenuVO menu : menus) { |
| | | if (!Boolean.TRUE.equals(menu.getVisible())) continue; |
| | | String component = menu.getComponent(); |
| | | String key = component == null ? null : PAGES.get(component.replaceFirst("^/", "")); |
| | | if (key != null) { |
| | | result.add(MenuVO.builder().id(menu.getId()).parentId(0L).name(menu.getName()) |
| | | .path("/mobile/" + key).component("mobile/" + key).visible(true) |
| | | .keepAlive(false).alwaysShow(false).build()); |
| | | } |
| | | collect(menu.getChildren(), result); |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.controller.admin.auth; |
| | | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO.MenuVO; |
| | | import org.junit.jupiter.api.Test; |
| | | import java.util.List; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | class MobileMenuAdapterTest { |
| | | @Test void qualityAndOutboundRetainOnlyAlreadyAuthorizedVisibleMenus() { |
| | | var root=menu(1L,null,true); |
| | | root.setChildren(List.of(menu(2L,"mes/qc/seedQuality/index",true),menu(3L,"mes/qc/seedQuality/record",true),menu(4L,"wls/productsales/index",true),menu(5L,"mes/qc/iqc/index",true))); |
| | | assertEquals(List.of("/mobile/quality","/mobile/quality-records","/mobile/sales"),MobileMenuAdapter.adapt(List.of(root)).stream().map(MenuVO::getPath).toList()); |
| | | root.setVisible(false);assertTrue(MobileMenuAdapter.adapt(List.of(root)).isEmpty()); |
| | | } |
| | | @Test void mapsOnlyVisibleAuthorizedSupportedMenusWithoutChangingPcTree() { |
| | | MenuVO bag=menu(2L,"/mes/pro/bagCode/index",true); |
| | | MenuVO stock=menu(3L,"wls/materialstock/index",true); |
| | | MenuVO hidden=menu(4L,null,false);hidden.setChildren(List.of(stock)); |
| | | MenuVO unsupported=menu(5L,"system/user/index",true); |
| | | MenuVO root=menu(1L,null,true);root.setChildren(List.of(bag,hidden,unsupported)); |
| | | List<MenuVO> result=MobileMenuAdapter.adapt(List.of(root)); |
| | | assertEquals(1,result.size());assertEquals(2L,result.get(0).getId()); |
| | | assertEquals("/mobile/bags",result.get(0).getPath()); |
| | | assertEquals("/mes/pro/bagCode/index",bag.getComponent()); |
| | | assertEquals(3,root.getChildren().size()); |
| | | assertTrue(MobileMenuAdapter.adapt(null).isEmpty()); |
| | | } |
| | | private MenuVO menu(Long id,String component,boolean visible) { |
| | | return MenuVO.builder().id(id).name("menu").component(component).visible(visible).build(); |
| | | } |
| | | } |