Compare commits

...

765 Commits

Author SHA1 Message Date
zty 2ae7dc2635 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-22 16:10:28 +08:00
zty 7cd95bd1b5 feat: ofm-models 修改专利patent 字段 2025-10-22 16:10:22 +08:00
caoqianming 97026d86c6 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-22 15:57:08 +08:00
caoqianming 54999cce73 fix: mlogbw defect 分配 bug 2025-10-22 15:57:08 +08:00
zty 303252e7a5 feat: ofm-models 修改专利model patentInfo 字段 2025-10-22 10:12:04 +08:00
zty 388e225108 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-21 14:10:44 +08:00
zty 25ee92602b add : ofm-create-model-patent 新建专利申请 curd 2025-10-21 14:10:43 +08:00
caoqianming 241df0beca feat: mio 添加查询条件 2025-10-20 16:33:46 +08:00
caoqianming 5ea7980a1b feat: mloguser添加字段 2025-10-20 10:09:38 +08:00
caoqianming a3416cfc0d feat: 修改mroombooking表约束 2025-10-17 14:51:13 +08:00
caoqianming 2ca47b8949 feat: base 获取流转时排序按attribute_type倒序 2025-10-17 12:32:18 +08:00
caoqianming 10792d090c Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-16 16:30:15 +08:00
caoqianming bfc8454ac7 feat: base workflow添加view_path2字段 2025-10-16 16:30:14 +08:00
zty 02b14ec2c6 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-16 11:18:26 +08:00
zty acb4c802e4 feat: ofm-services 修改绑定反存的接口 2025-10-16 11:18:25 +08:00
caoqianming 260c9893eb feat: base 获取流转时排序按attribute_type2 2025-10-16 10:55:33 +08:00
caoqianming 666a9c169c Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-16 10:05:13 +08:00
caoqianming b869521221 feat: base 获取流转时排序按attribute_type 2025-10-16 10:05:12 +08:00
zty 99d8144bdf feat: ofm-views 修改文件列表排序 2025-10-15 15:09:09 +08:00
zty 8a87ba356e feat: ofm-BorrowRecord - 档案借阅反存ticket-data 2025-10-15 14:10:33 +08:00
zty 830bf18132 feat: ofm-service-vehicle 用车管理反存 实际归还里程树 2025-10-14 16:08:16 +08:00
zty b52e90a11f Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-14 14:58:11 +08:00
zty ee67e6896a feat: ofm-service 反向存储ticket_data 到 Lendingseal 2025-10-14 14:58:11 +08:00
caoqianming 54f8b82c98 feat: mio增加筛选条件inout_date 2025-10-14 14:18:27 +08:00
caoqianming 6125139fbf Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-14 13:46:53 +08:00
caoqianming 2169bbea68 feat: do_out和do_in在处理into_wm不应直接忽略 2025-10-14 13:46:53 +08:00
zty 6dfab46b4d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-14 10:21:52 +08:00
zty 477976f86c feat: ofm-views 修改时间倒叙 2025-10-14 10:21:50 +08:00
caoqianming c37ff77eda Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-13 15:45:04 +08:00
caoqianming 1aa7d51769 feat: material添加create_time的排序条件 2025-10-13 15:45:04 +08:00
zty e06cc8c38e fix : ofm-models-publicity 修改记录编号自动生成 2025-10-13 14:26:40 +08:00
zty b93024ca44 fix : ofm-models-publicity 修改记录编号自动生成 2025-10-13 14:18:03 +08:00
zty 7d87c79dd1 feat: ofm-service-pulicity 反存ticket_data 到 obj 2025-10-13 14:04:40 +08:00
zty 9f030ece6d feat: ofm-service-pulicity 反存ticket_data 到 obj 2025-10-13 13:50:04 +08:00
zty 67f9cbb700 feat: ofm--Alter field publicity_opinion on publicity 2025-10-13 09:01:44 +08:00
zty 8fe2b8ca48 feat: ofm-models 修改字段属性 2025-10-11 14:28:09 +08:00
zty 7732ddc88e Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-11 11:32:06 +08:00
zty 6713693c6c feat: ofm-models -pulicity 修改字段属性 2025-10-11 11:32:05 +08:00
caoqianming 20604ef7cb fix: work_start_time 对光子的兼容 2025-10-11 10:28:07 +08:00
caoqianming eb2deb02c2 fix: work_start_time 可不填2 2025-10-11 10:22:19 +08:00
caoqianming f5f6c136d9 fix: work_start_time 可不填 2025-10-11 10:21:22 +08:00
caoqianming 8eee09678a fix: handle_date和shift可传 2025-10-11 10:16:13 +08:00
caoqianming efd40d1d32 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-11 10:11:19 +08:00
caoqianming 3ab9682b07 fix: work_start_time 可不填 2025-10-11 10:11:18 +08:00
zty 1983f7b121 feat: 修改 ofm-moedels - pulicity字段为空值 2025-10-11 09:23:33 +08:00
zty cec6837d00 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-10 16:32:21 +08:00
zty e214c6115a feat: ofm-models 增加字段 2025-10-10 16:32:20 +08:00
caoqianming 6fb415a9f0 release: 2.8.2025101011 2025-10-10 11:38:43 +08:00
caoqianming 727d190610 feat: Ptest val_xj可为空 2025-10-10 11:12:59 +08:00
caoqianming a6e0fb4f0d feat: quick调用serializer时传入request 2025-10-09 14:44:26 +08:00
caoqianming 9cf900d2ef doc: 添加一些注释 2025-10-09 11:17:14 +08:00
caoqianming 69e8e7b025 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-30 11:05:25 +08:00
caoqianming 647603f986 fix: p_create_after 自动创建mlogbw时关于exclude语句导致的查询错误 2025-09-30 11:05:24 +08:00
zty d21c1dc55d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-30 09:58:56 +08:00
zty 7f206bd0a7 feat: ofm-修改model 2025-09-30 09:58:53 +08:00
caoqianming bd763be83a feat: Mroombooking添加字段 2025-09-29 15:53:34 +08:00
caoqianming fe499ffac5 feat: base add_info_for_item 可复用list逻辑 2025-09-29 15:52:54 +08:00
caoqianming 0c1e93bf0b feat: 添加wpr查询参数 2025-09-29 14:50:02 +08:00
caoqianming 5d3c4137fe feat: base cquery支持annotate 2025-09-29 14:44:08 +08:00
caoqianming dbaf121685 feat: mroombooking 返回slots 2025-09-29 11:09:09 +08:00
caoqianming fe524e389c release: 2.8.2025092816 2025-09-28 16:51:44 +08:00
caoqianming bf8b886a2d feat: handover revert撤回时做校验2 2025-09-28 16:34:30 +08:00
caoqianming 7b8ec7f9d6 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-28 16:33:40 +08:00
caoqianming 7620122c2d feat: handover revert撤回时做校验 2025-09-28 16:33:39 +08:00
zty de99f85259 feat: ofm-models 修改车辆表的字段信息 2025-09-28 14:16:39 +08:00
zty e7c121de15 feat:iofm-serializer 增加ticket_ 2025-09-28 11:07:08 +08:00
caoqianming bdd686f50b feat: 会议室预定修改 2025-09-28 10:24:29 +08:00
caoqianming 54140ba742 fix: mroombooking 创建时未create_by 2025-09-26 16:26:04 +08:00
caoqianming c186b7d296 feat: p_create_after 优化 2025-09-26 15:14:03 +08:00
caoqianming 3a03cd76ff feat: p_create_after 优化 2025-09-26 14:58:03 +08:00
caoqianming f9c9a592e3 fix: mlogbinserializer 2025-09-26 14:30:51 +08:00
caoqianming 419b52f9be feat: p_create_after 可报当前产品都不可使用 2025-09-26 14:19:11 +08:00
caoqianming fb71f0697a feat: mlog quick增加wprs_in传参 2025-09-26 14:07:14 +08:00
caoqianming 34e217e468 feat: mlog quick跳过创建mlogbw2 2025-09-26 10:46:04 +08:00
caoqianming f34356057d feat: mlog quick跳过创建mlogbw 2025-09-26 10:33:25 +08:00
caoqianming b35015b58d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-26 09:11:00 +08:00
caoqianming c434c76605 feat: 车间领料时完善提示添加物料名 2025-09-26 09:10:59 +08:00
zty 9909596cdb Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-25 15:42:45 +08:00
zty 6f4a1c4c88 feat: ofm-修改会议室信息 2025-09-25 15:42:24 +08:00
caoqianming 2445ae53f1 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-25 09:11:00 +08:00
caoqianming 0fc57a454f feat: 改版需提供新批次号 2025-09-25 09:11:00 +08:00
zty d26e066769 feat: ofm 修改publicity 的model 2025-09-24 14:08:51 +08:00
zty f347fa0d23 feat: 修改ofm-services.py 2025-09-24 14:04:06 +08:00
zty 362a5ef725 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-24 13:59:57 +08:00
zty ad98280458 add : ofm-新增宣传报道保密模块 2025-09-24 13:59:55 +08:00
caoqianming 3c0fa9f244 feat: base send_sms auto_log send_mail使用False 2025-09-23 16:26:07 +08:00
caoqianming b520b69f95 feat: 优化bind_routepack 2025-09-23 15:08:22 +08:00
caoqianming 899a314f5d feat: base 优化wf通知发送 2025-09-23 14:59:50 +08:00
caoqianming dbb0d6ae75 feat: routepack_ticket_change 变为创建中状态 2025-09-23 13:34:34 +08:00
caoqianming bc4953893b Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-22 11:31:22 +08:00
caoqianming a8722724e1 feat: route增加查询条件以支持获取后续工段的信息 2025-09-22 11:31:21 +08:00
caoqianming 1c447a86ef feat: 导入物料优化一下 2025-09-20 14:14:26 +08:00
caoqianming 820f814766 feat: 优化的mplogxview 2025-09-19 13:02:31 +08:00
caoqianming 1689683aa3 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-19 10:52:23 +08:00
caoqianming cdb201a0ce feat: base sql querydict可传入是否格式化时间参数 2025-09-19 10:52:22 +08:00
zty d8ad57fa7e Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-19 09:22:49 +08:00
zty e32d4b816b add: ofm-增加宣传保密model 2025-09-19 09:22:47 +08:00
caoqianming 47ca272cee feat: base workflow list 返回view_path 2025-09-19 09:20:02 +08:00
caoqianming 4390992a14 feat: base workflow添加view_path 2025-09-19 09:09:44 +08:00
caoqianming 085978a6c4 fix: 优化handover list接口 2025-09-18 15:06:45 +08:00
caoqianming f6668b5d38 feat: 优化handover list接口 2025-09-18 14:48:28 +08:00
caoqianming f6720ad7ce feat: 玻纤拉丝采集问题 2025-09-18 13:20:55 +08:00
caoqianming e5a9f77f3d feat: ana_batch_thread 优化一下 2025-09-18 10:44:23 +08:00
caoqianming c2deb0ee45 feat: base 优化system事务处理 2025-09-17 12:51:20 +08:00
caoqianming 94f87df707 feat: 多个app优化事务处理 2025-09-17 12:50:19 +08:00
caoqianming 694dca27cc feat: base system和wf优化事务处理2 2025-09-17 12:23:08 +08:00
caoqianming 0284809933 feat: base system和wf优化事务处理 2025-09-17 12:15:26 +08:00
caoqianming f60250bb21 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-17 11:15:17 +08:00
caoqianming 0b4524aa85 feat: 优化cd.py 2025-09-17 11:15:15 +08:00
caoqianming f7e27c290f feat: wpr list 返回process_name 2025-09-17 11:14:49 +08:00
caoqianming 2c9c74131c feat: 优化cd.py 2025-09-16 18:51:38 +08:00
caoqianming 70cdf0d1c6 release: 2.8.2025091616 2025-09-16 16:19:15 +08:00
caoqianming bac046530e fix: wpm取消事务时出现的缩进bug 2025-09-16 15:27:25 +08:00
caoqianming 7a3988d6bd feat: mioitem处理事务处理 2025-09-16 14:51:06 +08:00
caoqianming f151f4f2ec feat: base 移除基础model层事务 2025-09-16 10:33:44 +08:00
caoqianming 7a82844842 feat: inm和wpm关于事务处理的修改 2025-09-16 10:25:52 +08:00
caoqianming 9bf44c4211 fix: 次批作为输入不会影响输出批次号 2025-09-15 15:48:21 +08:00
caoqianming ed2804c098 feat: 批次的DAG数据只获取直接的上下级 2025-09-15 10:26:36 +08:00
caoqianming 00d0b1ea00 feat: wpm优化事务和悲观锁 2025-09-15 09:41:20 +08:00
caoqianming aa80c1b00a feat: cdview优化校验 2025-09-15 09:40:39 +08:00
caoqianming 67b92f0dd4 feat: base 优化get_object 2025-09-15 09:40:12 +08:00
caoqianming 06e86330bd fix: lock_and_check_can_update 2025-09-12 17:02:06 +08:00
caoqianming 34ca36aced fix: lock_and_check_can_update 2025-09-12 17:00:10 +08:00
caoqianming 649022eb57 feat: mlogbout serializer update校验mlog 2025-09-12 16:52:53 +08:00
caoqianming 5709bc3a47 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-12 16:48:34 +08:00
caoqianming cdcb02d4d5 feat: 生产日志子表全部加mlog锁 2025-09-12 16:48:33 +08:00
zty e7ea16ece6 feat: ofm-serializer fix 2025-09-12 16:06:21 +08:00
zty 6b08200016 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-12 15:01:12 +08:00
zty 8ef9852e27 add: ofm-档案管理增加审批 2025-09-12 15:01:11 +08:00
caoqianming e9246cc47f Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-12 14:47:50 +08:00
caoqianming 5ece330457 feat: base 优化get_object事务 2025-09-12 14:47:50 +08:00
zty 643c45882f feat: ofm-修改字段 2025-09-12 14:43:35 +08:00
zty 8bcd9b7d03 feat:ofm - 档案管理修改 serializer 2025-09-12 14:32:36 +08:00
zty aff39f3e31 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-12 14:18:57 +08:00
zty 53117c838b feat: ofm-档案管理-serializer 更新 2025-09-12 14:18:56 +08:00
caoqianming 57a61daa66 feat: wpm修改为自动事务 2025-09-12 13:48:34 +08:00
caoqianming e5008c8412 fix: base 在create update destroy添加自动事务 2025-09-12 13:48:10 +08:00
caoqianming 5e8e72cee9 feat: 修改wpm事务 2025-09-12 12:41:00 +08:00
caoqianming 674f62a05a fix: base 修改_should_use_transaction 2025-09-12 12:40:42 +08:00
caoqianming 527e6c0fc2 feat: wpm 取消 transaction 2025-09-12 12:16:47 +08:00
caoqianming 90b7e2087b Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-12 12:14:29 +08:00
caoqianming 412398d461 feat: base CustomGenericViewSet 添加自动事务 2025-09-12 12:14:29 +08:00
zty ebd125ca1d feat: ofm-修改 -档案借 filterset_class 2025-09-12 10:32:12 +08:00
zty 6d36f3fa7d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-12 10:19:50 +08:00
zty e8b914d556 feat: ofm 增加档案台账模糊查询 2025-09-12 10:19:49 +08:00
caoqianming c9a2daaa48 feat: 默认提交时间为结束时间 2025-09-12 10:11:25 +08:00
caoqianming c4c61ff737 fix: 产生编号时存在bug 2025-09-12 09:19:57 +08:00
caoqianming aa72b0780a feat: 快速报工的bug 2025-09-12 09:11:35 +08:00
caoqianming 9d692b4d5d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-11 15:53:36 +08:00
caoqianming 42a4332b87 feat: base 日志默认记录耗时大于2s的 2025-09-11 15:53:36 +08:00
zty 51fb42d597 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-11 14:43:04 +08:00
zty 19c7e7aad1 feat: ofm 增加档案台账 2025-09-11 14:43:04 +08:00
caoqianming b4cfdd693a fix: 交接记录已提交不可变动 2025-09-11 14:14:47 +08:00
caoqianming 448bdb9ee6 fix: mlog change 导致的bug问题 2025-09-11 14:09:27 +08:00
caoqianming 839fc2af82 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-11 13:26:37 +08:00
caoqianming cecffbdb78 feat: mlog submit/revert 改用乐观锁 2025-09-11 13:26:37 +08:00
zty a0c3443d9e feat: ofm 修改车辆model 2025-09-11 09:53:35 +08:00
zty b380c3805b Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-11 09:51:42 +08:00
zty 80fa245b58 feat: ofm 修改 车辆model 2025-09-11 09:51:41 +08:00
caoqianming d828bb76d5 feat: wpr list 返回wm_material_ofrom_name 2025-09-11 09:13:37 +08:00
caoqianming e4a2e7c4f5 feat: wpr list 返回wm_material_name 2025-09-11 09:08:48 +08:00
caoqianming 53858cac94 feat: wpr list 返回material_start_name 2025-09-10 17:01:53 +08:00
caoqianming a5f32dbeda Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-10 15:35:18 +08:00
caoqianming e491d7b4fe fix: wpr list filter bug 2025-09-10 15:35:17 +08:00
zty 1ea9ef48a6 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-10 15:21:19 +08:00
zty 9db173cdb9 feat: vehicle 增加serializer 返回字段 2025-09-10 15:21:19 +08:00
caoqianming 2369c1469e feat: wpr list返回batch 2025-09-10 15:03:24 +08:00
caoqianming 27416dfeaa Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-10 14:49:35 +08:00
caoqianming ec8881fb02 feat: 增加wpr tag查询条件以支持todo等 2025-09-10 14:49:35 +08:00
zty f6f842c17f feat: 修改车辆的字段 2025-09-10 14:36:30 +08:00
zty 172e2397be feat: ofm 车辆管理迁移文件 2025-09-10 14:27:38 +08:00
zty 71bc4e76f0 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-10 14:26:25 +08:00
zty ba8b258ee7 feat: ofm 车辆审批 2025-09-10 14:26:23 +08:00
caoqianming 8b7a87abb6 fix: 出入库记录和交接记录提交处理时都进行单个的强校验 2025-09-10 13:11:51 +08:00
caoqianming d42bc29d2c feat: 出入库记录和交接记录提交处理时都进行单个的强校验 2025-09-10 11:26:37 +08:00
caoqianming 6b423c80eb feat: 添加物料明细与批次不匹配的校验2 2025-09-09 16:49:51 +08:00
caoqianming df4b062209 feat: 添加物料明细与批次不匹配的校验 2025-09-09 16:38:58 +08:00
caoqianming d84b8e94fd feat: gen_number_with_rule 优化一下 2025-09-09 16:19:05 +08:00
caoqianming 6d09e5e4f3 feat: handover_submit增强校验 2025-09-09 15:39:13 +08:00
caoqianming bc555b7bea feat: mlog list 支持返回mlogbw number 2025-09-09 14:05:59 +08:00
caoqianming 3906b0f744 feat: gen_number_with_rule按生产日志中的工段过滤 2025-09-09 13:37:56 +08:00
caoqianming d19fee8e6f feat: gen_number_with_rule 完善 2025-09-09 11:28:04 +08:00
caoqianming 81a16fd37e feat: 工段未配置班次提醒 2025-09-09 11:23:09 +08:00
caoqianming ddb4a6930d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-09 10:37:56 +08:00
caoqianming cbc5d558d9 feat: 板段号生成逻辑修改 2025-09-09 10:37:56 +08:00
zty 0183234497 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-08 16:31:24 +08:00
zty 068e391845 feat: ofm-印章绑定审批修改 2025-09-08 16:31:20 +08:00
caoqianming 6b6c4b7e57 release: 2.8.2025090815 2025-09-08 16:23:34 +08:00
caoqianming 0a12927518 feat: batch bxerp含缺陷统计 2025-09-08 15:18:10 +08:00
caoqianming 3be13dd920 feat: mlog list 添加update_time 2025-09-08 14:51:11 +08:00
caoqianming 8cc9c46a95 feat: 工艺步骤中辅料使用的校验 2025-09-08 14:29:19 +08:00
caoqianming 2e2ac78bad feat: mlogb添加parent isnull查询 2025-09-08 13:33:48 +08:00
caoqianming 0b1f71e652 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-08 11:18:10 +08:00
caoqianming 94a218c09a feat: 次批触发输出产生 2025-09-08 11:18:09 +08:00
zty de1e5b4d41 feat: 修改印章的model 和 serializer 2025-09-08 11:13:06 +08:00
zty 0e783a92e0 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-08 09:58:00 +08:00
zty 3e90dd6820 feat: 修改 ofm sevice 印章模块 2025-09-08 09:57:58 +08:00
caoqianming 10d4a64c3a feat: base 优化safe_get_or_create 2025-09-08 09:28:04 +08:00
zty f97f51e72c feat: 修改ofm seal 过滤查询功能 2025-09-05 14:49:50 +08:00
caoqianming 6b246f147d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-05 14:32:43 +08:00
caoqianming fdb49e1147 feat: mioitemcreate 批次号可不填 2025-09-05 14:32:42 +08:00
zty ca18fece21 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-05 11:10:16 +08:00
zty 6a6d46583b feat: 行政管理 -印章管理 2025-09-05 11:10:15 +08:00
caoqianming 41c1653d13 feat: ftest默认为合格 2025-09-05 09:47:20 +08:00
caoqianming c20580e263 fix: mlogbwstarttest bug4 2025-09-04 17:23:00 +08:00
caoqianming a6fcdab836 fix: mlogbwstarttest bug3 2025-09-04 17:20:31 +08:00
caoqianming 026ebccef5 fix: mlogbwstarttest bug2 2025-09-04 17:18:38 +08:00
caoqianming f1aa922946 fix: mlogbwstarttest bug 2025-09-04 17:08:19 +08:00
caoqianming a7fd4b9448 feat: MlogbDefect 添加count_has 的处理 2025-09-04 16:59:20 +08:00
caoqianming 65d47008a4 feat: mlogbdefect添加count_has 字段 2025-09-04 16:48:41 +08:00
caoqianming 65726e967f feat: update_mb_item defect处理更严谨 2025-09-04 16:48:10 +08:00
caoqianming 2524feca72 feat: 取消最后一步产出与工艺包不一致 的校验 2025-09-04 15:07:07 +08:00
caoqianming 01b30e21d3 feat: 出入库记录添加乐观锁 2025-09-04 14:42:14 +08:00
caoqianming 4cffb8a563 feat: 出入库明细可默认批次为无 2025-09-04 10:55:56 +08:00
caoqianming 0c08543e9c feat: 导入物料明细时可默认批次号为 无批次 2025-09-04 10:44:32 +08:00
caoqianming e6a1363b43 feat: 导入物料明细时可默认批次号为 无批次 2025-09-04 10:40:52 +08:00
caoqianming 3d188a72f3 feat: 导入物料明细时可默认批次号3 2025-09-04 10:25:42 +08:00
caoqianming 6104a2e6be feat: 导入物料明细时可默认批次号2 2025-09-04 10:08:44 +08:00
caoqianming abd1ac9d56 feat: batch_bxerp考虑mlogbw_from 2025-09-04 08:54:21 +08:00
caoqianming cc45163775 feat: mlog_submit mlogbdefect增加筛选条件 2025-09-04 08:53:37 +08:00
caoqianming f82db7bb6f feat: 导入物料明细时可默认批次号 2025-09-03 11:27:58 +08:00
caoqianming c4539260a8 feat: 光芯批次统计增加班次返回 2025-09-02 16:45:40 +08:00
caoqianming 872e59ffe1 feat: route update 时from_route存在则不可修改关键信息3 2025-09-02 15:53:24 +08:00
caoqianming 7d2f11f194 feat: route update 时from_route存在则不可修改关键信息2 2025-09-02 15:22:29 +08:00
caoqianming bf3803a0e8 feat: mlog_submit 进行操作时间校验 2025-09-02 15:17:59 +08:00
caoqianming a6b320bad6 feat: route update 时from_route存在则不可修改关键信息 2025-09-02 15:16:55 +08:00
caoqianming ba0e86d834 fix: ftestwork submit 校验wm和mb bug 2025-09-02 14:30:25 +08:00
caoqianming 28671451b0 feat: mlogchange支持work_start_time 2025-09-02 12:15:29 +08:00
caoqianming 45af751350 feat: MlogSerializer 处理 work_start_time 的 bug2 2025-09-02 11:32:05 +08:00
caoqianming c997cba6a5 feat: route采用引用方式允许重复创建 2025-09-02 11:23:37 +08:00
caoqianming 9af23216b6 feat: MlogSerializer 处理 work_start_time 的 bug 2025-09-02 11:09:12 +08:00
caoqianming 81561b5238 fix: MlogbInUpdateSerializer 联动count_use 和 count_real 2025-09-02 09:10:52 +08:00
zty f7cc3b438f Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-02 08:48:03 +08:00
zty 5c163cdcbb feat: 修改能管采集的点位 2025-09-02 08:48:01 +08:00
caoqianming 67f7afd3fc Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-02 08:40:20 +08:00
caoqianming 1929fd30a6 feat: mlog work_start_time必填 2025-09-02 08:40:20 +08:00
zty c38457e947 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-01 17:05:21 +08:00
zty 8f6e401fdd feat: ftestwork submit支持mb 2025-09-01 17:05:20 +08:00
caoqianming 6582a4dae1 feat: toggle_state 使用 routepack.update权限 2025-09-01 16:06:00 +08:00
caoqianming 30f8d484d1 fix: batchlog batches_to 优化 2025-09-01 13:17:35 +08:00
caoqianming 31d0dc4829 feat: ftestwork支持对materialbatch检查 2025-08-29 13:11:58 +08:00
caoqianming 9d1c5415e1 feat: batchst返回material_start相关信息 2025-08-29 09:48:09 +08:00
caoqianming a249258ba6 feat: 添加定时任务以标记mtask 状态2 2025-08-28 16:43:10 +08:00
caoqianming 4fa0b33254 feat: 添加定时任务以标记mtask 状态 2025-08-28 16:37:23 +08:00
caoqianming 22a310906a release: 2.7.2025082816 2025-08-28 16:12:48 +08:00
caoqianming 5e9c15c4a5 feat: 添加定时任务以标记mtask为已完成 2025-08-28 16:08:18 +08:00
caoqianming 786e885071 feat: update_mtask 不再触发状态改变 2025-08-28 16:07:41 +08:00
caoqianming 41485d7143 feat: 子任务提交会触发大任务提交 2025-08-28 15:40:24 +08:00
caoqianming 73b225fa19 feat: 优化mlog list查询2 2025-08-28 10:52:11 +08:00
caoqianming c47b66af6c feat: 优化mlog list查询 2025-08-28 09:54:56 +08:00
caoqianming 1d0861e0e7 feat: 增加直接userid获取token的接口 2025-08-27 15:47:57 +08:00
caoqianming fa243faf34 feat: mlog 添加索引 2025-08-27 15:35:04 +08:00
caoqianming ebe0e4cc6a fix: 增加mlogbw start_test接口以优化批量操作 2025-08-27 15:10:40 +08:00
caoqianming 894142712f feat: mlog view 暂时性能优化 2025-08-27 14:23:56 +08:00
caoqianming cd4b686f11 feat: 增加mlogbw start_test接口以优化批量操作 2025-08-27 11:11:04 +08:00
caoqianming 9ce5d29fa6 feat: 优化mlogbw bulk update 3 2025-08-27 10:07:35 +08:00
caoqianming 3e8bce688a feat: 优化mlogbw bulk update 2 2025-08-27 10:04:45 +08:00
caoqianming fa7d3095de feat: 优化mlogbw bulk update 2025-08-27 10:02:50 +08:00
caoqianming c7514836cb release: 2.7.2025082616 2025-08-26 16:51:51 +08:00
caoqianming 316931fb40 feat: 添加mlog和handover的锁 2025-08-26 16:34:49 +08:00
caoqianming 8a039094cf feat: base 添加悲观锁及其装饰器 2025-08-26 16:34:25 +08:00
caoqianming f0b24da22f feat: number_to_batch 支持结合工序 2025-08-26 10:35:21 +08:00
caoqianming 5496058226 feat: 物料清单的导出 2025-08-25 15:37:05 +08:00
caoqianming 3810860f13 feat: mlogbw create 优化 2025-08-25 11:17:50 +08:00
caoqianming 0a6959a36c fix: mlogbw create 调换一下if顺序 2025-08-25 09:44:32 +08:00
caoqianming 4823acb004 fix: mlogbw create cal_count_notok bug 2025-08-25 08:58:47 +08:00
caoqianming 8b6d6fd668 fix: handover revert 可传入handler2 2025-08-21 20:17:02 +08:00
caoqianming c58c79a18a fix: handover revert 可传入handler 2025-08-21 20:13:09 +08:00
caoqianming 41a3418110 feat: material添加img字段 2025-08-21 17:34:39 +08:00
caoqianming 05955abd9c fix: base ComplexQueryMixin 默认null值排最后 2025-08-21 09:24:14 +08:00
caoqianming 0cf04c50b3 feat: 过滤来料未完成的优化 2025-08-20 17:39:43 +08:00
caoqianming 01e7d73bee feat: mlog quick添加mlogb 2025-08-20 15:03:58 +08:00
caoqianming 58024cc33d feat: batch_gzerp添加库存等字段 2025-08-19 16:01:18 +08:00
caoqianming 4921383de1 feat: check_sql_safe 优化一下 2025-08-19 15:11:03 +08:00
caoqianming bbfa512bb1 feat: quick返回mlogId 2025-08-19 11:32:48 +08:00
caoqianming d019e708cd feat: mlog submit revert支持放置到车间 2025-08-19 11:22:21 +08:00
caoqianming d9326bc264 feat: 可根据work_start_time 推出handle_date 2025-08-19 10:39:46 +08:00
caoqianming a8ed1c3083 feat: MlogInitSerializer 可传入handle_date 2025-08-19 10:35:45 +08:00
caoqianming 2bb1cf2844 feat: 去除commands命令权限 2025-08-19 08:56:29 +08:00
caoqianming 0dab1714f3 feat: 交接记录在审批状态下对撤销的处理 2025-08-18 16:52:23 +08:00
caoqianming fbce5ec64f feat: base retreat 撤回功能提到wfservice里 2025-08-18 16:51:23 +08:00
caoqianming 69e606bccc release: 2.7.2025081813 2025-08-18 13:45:47 +08:00
caoqianming efd25b040b fix: 导入出入库明细 2025-08-18 13:34:21 +08:00
caoqianming ee9fce675d feat: 导入出入库明细 2025-08-18 11:04:24 +08:00
caoqianming 7f619a36b0 feat: mlogbw增加type查询区分inout 2025-08-14 14:58:50 +08:00
caoqianming 84c701bdeb fix: 快速创建日志接口 2025-08-14 11:09:42 +08:00
caoqianming ca3e811b1b feat: 快速创建日志接口 2025-08-14 11:05:33 +08:00
caoqianming 1d778dbdf0 feat: 添加wamterial ava_qs方法 2025-08-14 09:48:23 +08:00
caoqianming c69c33e85a feat: mlogbw添加排序以支持直接展示 2025-08-14 08:44:23 +08:00
caoqianming 8ecd7d25b0 fix: mlogb__batch bug 2025-08-13 14:45:21 +08:00
caoqianming e03faeffc4 feat: WMaterialFilter filter tag优化 2025-08-13 10:03:00 +08:00
caoqianming c7b1c8a8b8 feat: mlogbw添加筛选条件2 2025-08-13 09:15:46 +08:00
caoqianming ddefbd49b2 feat: mlogbw添加筛选条件 2025-08-13 08:44:56 +08:00
caoqianming 10745394e3 feat: MIOItemCreateSerializer 添加pack_index4 2025-08-08 16:44:59 +08:00
caoqianming 82f6f4ee5e feat: MIOItemCreateSerializer 添加pack_index3 2025-08-08 14:34:57 +08:00
caoqianming 46c9c5abab feat: MIOItemCreateSerializer 添加pack_index2 2025-08-08 13:53:21 +08:00
caoqianming 09e390b37b feat: MIOItemCreateSerializer 添加pack_index 2025-08-08 13:47:25 +08:00
caoqianming afb27246d5 release: 2.7.2025080716 2025-08-07 16:06:17 +08:00
caoqianming 98a9806c4b feat: MaterialBatch返回can_mio2 2025-08-07 15:44:04 +08:00
caoqianming d3f83605aa feat: MaterialBatch返回can_mio 2025-08-07 15:39:43 +08:00
caoqianming 783f288926 feat: wpr 不做删除 2025-08-07 15:07:03 +08:00
caoqianming 40eb07eb12 feat: number_out_last 改为原始sql查询 2025-08-07 14:38:06 +08:00
caoqianming 835c5f78dd feat: base query_one_dict优化 2025-08-07 14:37:41 +08:00
caoqianming e30241dc22 feat: mioitemw 返回wpr_number_out 2025-08-07 14:25:55 +08:00
caoqianming b9376cc2d6 feat: 捕获number_out_last异常 2025-08-07 13:47:57 +08:00
caoqianming 05fcc4a00a feat: mlogbw返回wpr_number_out 2025-08-07 13:44:17 +08:00
caoqianming cf25d0d280 feat: 获取物料的缺陷项列表 2025-08-07 11:08:29 +08:00
caoqianming c7f6abf6a6 feat: route添加params_json字段 2025-08-07 10:37:59 +08:00
caoqianming a42d48775c feat: wpr增加can_use筛选条件2 2025-08-07 10:05:33 +08:00
caoqianming aa72953d17 feat: wpr增加can_use筛选条件 2025-08-07 09:31:53 +08:00
caoqianming 925666d3ef fix: number_out_last获取bug2 2025-08-06 14:29:08 +08:00
caoqianming b7823d56a8 fix: number_out_last获取bug 2025-08-06 08:59:29 +08:00
caoqianming 0a6de249ee feat: 光子批次统计增加组合件信息7 2025-08-05 16:02:58 +08:00
caoqianming d130f75076 feat: 光子批次统计增加组合件信息6 2025-08-05 16:01:01 +08:00
caoqianming dbde5a1327 feat: mlog 创建和编辑时可变更team字段 2025-08-05 15:45:30 +08:00
caoqianming 9b4a44be51 feat: get_qct 未有检验模板 2025-08-05 15:10:00 +08:00
caoqianming 05ea28f334 fix: wpr number_out_last 前缀参数 2025-08-05 14:31:34 +08:00
caoqianming c14229f6f5 feat: 光子批次统计增加组合件信息4 2025-08-05 13:19:45 +08:00
caoqianming 94b0719f38 feat: 光子批次统计增加组合件信息3 2025-08-05 11:24:36 +08:00
caoqianming 24a389d566 feat: 光子批次统计增加组合件信息2 2025-08-05 11:17:27 +08:00
caoqianming 8a2244a0cc feat: ftestwork_submit 触发批次统计分析 2025-08-05 10:09:46 +08:00
caoqianming 1c24e9948b feat: 光子批次统计增加组合件信息 2025-08-04 15:33:22 +08:00
caoqianming 763c64d5d0 feat: wmaterial添加筛选条件defect__isnull 2025-08-04 15:18:38 +08:00
caoqianming 3251c87eb3 fix: mioitem 返回信息时展示组合件信息2 2025-08-04 11:32:50 +08:00
caoqianming c790b3bd4a fix: mioitem 返回信息时展示组合件信息 2025-08-04 11:30:46 +08:00
caoqianming 884b10d6bc feat: mioitem 返回信息时展示组合件信息 2025-08-04 11:21:35 +08:00
caoqianming 7d2fee8409 feat: 仅合格品支持退回 2025-08-04 10:09:23 +08:00
caoqianming 6a93ca31af release: 2.7.2025080409 2025-08-04 09:19:45 +08:00
caoqianming 7f89e8a72d feat: ftestwork_submit 关于不合格逻辑的bug 2025-08-01 17:11:09 +08:00
caoqianming 011577f9a5 fix: mioitem 检索bug 2025-08-01 15:31:43 +08:00
caoqianming 9b5e1bd9a7 feat: 出入库记录支持对子件批次
的搜索
2025-08-01 15:05:45 +08:00
caoqianming 28900d3218 feat: 装箱字段简化 2025-08-01 14:16:54 +08:00
caoqianming 393e528434 feat: 注册装箱功能路由2 2025-08-01 13:50:43 +08:00
caoqianming 2f0ce5c171 feat: 注册装箱功能路由 2025-08-01 13:47:46 +08:00
caoqianming 04d4c560ef feat: 装箱操作 2025-08-01 11:31:02 +08:00
caoqianming 5514e8b561 feat: 支持消耗物料的检验3 2025-08-01 11:12:46 +08:00
caoqianming 264083ebd5 feat: 支持消耗物料的检验2 2025-08-01 10:57:18 +08:00
caoqianming 808b8ac229 feat: 支持消耗物料的检验 2025-08-01 10:44:03 +08:00
caoqianming 70069a5a07 feat: WproutSerializer 支持传入none 2025-07-31 15:12:15 +08:00
caoqianming 33709bbb60 feat: assgin_number_out 支持置空 2025-07-31 15:02:57 +08:00
caoqianming bbef553bd5 fix: assgin_number_out bug 2025-07-31 14:40:53 +08:00
caoqianming 1fccdd042c feat: 添加发货单 2025-07-31 14:05:02 +08:00
caoqianming de953085cd fix: 销售发货时update_mb_item的bug 2025-07-31 13:46:39 +08:00
caoqianming 54345d2bce release: 2.7.2025073110 2025-07-31 10:54:58 +08:00
caoqianming ad9ef9efa5 feat: 通过fmlog查询batch 2025-07-31 10:41:16 +08:00
caoqianming 06de49f335 feat: 优化number_out_last 2025-07-31 09:31:10 +08:00
caoqianming eebc5238ae feat: MIOItem序列化器添加note 2025-07-30 16:02:18 +08:00
caoqianming 5755c508d7 feat: wpr分配出库对外编号 序列化器改动 2025-07-30 15:55:04 +08:00
caoqianming 84198f4d91 feat: wpr分配出库对外编号 2025-07-30 15:35:06 +08:00
caoqianming b5889374eb feat: 来料未完成的筛选控制物料类型为半成品、成品 2025-07-30 13:27:35 +08:00
caoqianming c98ce36c16 feat: 添加number_out_last的接口说明 2025-07-29 10:03:59 +08:00
caoqianming 5c21d6042b feat: 销售发货时可以设置对外编号 2025-07-28 16:56:55 +08:00
caoqianming 7f31ec8add feat: 物料导入模板及逻辑修改 2025-07-28 14:58:27 +08:00
caoqianming 739c4b0d65 fix: purout操作后续的bug 2025-07-28 14:31:33 +08:00
caoqianming b45eadc53c fix: 出入库类型错误bug 2025-07-28 14:25:55 +08:00
caoqianming 5d335fdd6a feat: mio创建时type得传入并校验 2025-07-28 13:50:28 +08:00
caoqianming 382a5a827c feat: 支持领用出库和退还入库 2025-07-28 13:41:16 +08:00
caoqianming b3b77db0c1 feat: wm todo排除repaired 2025-07-27 13:15:35 +08:00
caoqianming fa38cfdbdd feat: 交接记录关联审批暂不支持撤销 2025-07-25 22:53:49 +08:00
caoqianming 45c224c10e feat: handover返回ticket_字段 2025-07-25 16:18:53 +08:00
caoqianming b1f6798c65 feat: dataset添加enabled字段 2025-07-25 11:34:56 +08:00
caoqianming 0c7c1e4f17 feat: 支持采购退货 2025-07-24 10:50:11 +08:00
caoqianming 2fbe1620fb feat: wpr_bxerp 返工的不纳入统计 2025-07-23 13:43:11 +08:00
caoqianming 66a1d6df7f feat: 交接记录添加审批工单 2025-07-23 09:20:38 +08:00
caoqianming 3cacea1b06 feat: 批次统计分析时logerror后return 2025-07-22 15:56:26 +08:00
caoqianming a91b75ae7a feat: 获取qct type参数获取错误 2025-07-22 13:38:48 +08:00
caoqianming c35cdb6d0a feat: qct 增加筛选条件 2025-07-18 16:32:47 +08:00
caoqianming d732652c9f feat: qctmat添加字段以支持消耗物料参数填写 2025-07-18 16:01:32 +08:00
caoqianming 3985401361 feat: mlogbw编辑时可删除ftest3 2025-07-18 10:15:43 +08:00
caoqianming ec83674c6a feat: mlogbw编辑时可删除ftest2 2025-07-18 10:10:10 +08:00
caoqianming f955c8bd8c feat: mlogbw编辑时可删除ftest 2025-07-18 10:07:47 +08:00
caoqianming 357b1fdfa0 feat: wpr_bxerp只统计提交的日志 2025-07-18 09:53:43 +08:00
caoqianming 9c391add48 feat: wpr_bxerp调整 2025-07-18 09:23:22 +08:00
caoqianming 4e1c33317c fix: mlogbw返回equip相关信息 2025-07-17 15:04:17 +08:00
caoqianming 80b265b243 feat: mlogbw返回equip相关信息 2025-07-17 14:56:26 +08:00
caoqianming 1bc9cd2c60 feat: 批次统计分析中关于decimal的处理 2025-07-17 10:26:17 +08:00
caoqianming c0d504ecfe feat: batch_bxerp优化 2025-07-17 10:21:08 +08:00
caoqianming 7fd47c2a97 feat: 修改batchst data默认为dict 2025-07-17 10:20:48 +08:00
caoqianming e2a8da9eb4 feat: 优化cd.py 2025-07-17 09:58:56 +08:00
caoqianming 47cfa7ce17 feat: cd.py清空缓冲区后再执行 2025-07-17 09:45:41 +08:00
caoqianming 17996a0b86 feat: 单个统计优化 2025-07-17 09:01:49 +08:00
caoqianming 69c7da883f feat: gx bx的批次统计添加大小日期 2025-07-16 16:29:20 +08:00
caoqianming 811e971b4b fix: wpr_data 副本 2025-07-16 16:25:33 +08:00
caoqianming e8eeb14766 feat: 批次统计分析重构 2025-07-16 14:53:40 +08:00
caoqianming bdf69a6635 release: 2.7.2025071516 2025-07-15 16:04:20 +08:00
caoqianming dbfe308154 fix: 生成dag时为防止边重复给edge设置ID 2025-07-15 15:39:47 +08:00
caoqianming 18abcc5c9f feat: ftestwork添加cbatch查询条件 2025-07-15 14:23:44 +08:00
caoqianming 5eac4240fb feat: 出入库记录缺少明细不让操作 2025-07-15 13:47:01 +08:00
caoqianming e6850c92cb feat: 光子batch统计增加大小日期 2025-07-15 13:46:41 +08:00
caoqianming c3337cfa2a feat: 导入daoru_mioitem_test单元格公式的处理 2025-07-15 10:21:29 +08:00
caoqianming fc572d30e3 feat: ftestprocess update 支持新的检测项和缺陷项 2025-07-15 09:49:58 +08:00
caoqianming 528604d24f fix: ftestprocess update 更详细的报错信息 2025-07-15 09:43:49 +08:00
caoqianming 5285bf0252 feat: ftestprocess update 更详细的报错信息 2025-07-15 09:42:03 +08:00
caoqianming 8ba233f2fd feat: 数采做成单独服务3 2025-07-14 16:39:11 +08:00
caoqianming 9e62bf8d41 feat: 数采做成单独服务2 2025-07-14 13:50:50 +08:00
caoqianming 3fe723b397 feat: 数采做成单独服务 2025-07-14 13:42:29 +08:00
caoqianming b8c799b938 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-07-14 10:53:24 +08:00
caoqianming 5a98d1bea7 feat: 测试cd 2025-07-14 10:48:31 +08:00
caoqianming 4b52c28b5e fix: batchst并发创建的处理 2025-07-10 13:24:00 +08:00
caoqianming e2c7847c74 feat: gen_number_with_rule 偏离6小时 2025-07-08 16:41:25 +08:00
caoqianming 024cae6fa3 feat: base complexquery value支持多种类型 2025-07-07 15:38:59 +08:00
caoqianming 523b4b2a42 feat: complexquery 支持isnull 2025-07-07 15:27:04 +08:00
caoqianming d76d77fa49 feat: wpr 增加复杂查询接口 2025-07-07 14:49:57 +08:00
caoqianming 3858b3b9f9 feat: 返工不限制物料选择 2025-07-07 14:49:38 +08:00
caoqianming 05ebb25dc7 feat: 按个选入消耗时统计mlog数据优化2 2025-07-07 11:16:55 +08:00
caoqianming 45647d2149 feat: 按个选入消耗时统计mlog数据优化 2025-07-07 11:08:29 +08:00
caoqianming 28acc48768 feat: mlog添加操作时间校验 2025-07-07 10:44:31 +08:00
caoqianming 30bb96cf6c feat: 按个选入消耗时统计mlog数据 2025-07-07 10:34:40 +08:00
caoqianming e57e087006 fix: 优化ana_wpr4 2025-07-04 22:17:02 +08:00
caoqianming 33f79b5cc5 feat: 优化ana_wpr3 2025-07-04 20:14:58 +08:00
caoqianming 14f3b215c4 feat: BatchSt.g_create取消传入check_mat 2025-07-04 13:22:45 +08:00
caoqianming e9059c93bd feat: 优化ana_wpr2 2025-07-04 11:02:34 +08:00
caoqianming 662bef04ab feat: 优化ana_wpr 2025-07-04 10:51:22 +08:00
caoqianming 403477afd0 feat: wpr_bxerp返回批号 2025-07-04 10:08:16 +08:00
caoqianming 1e128041cd fix: 日志多步骤导致的返工填写bug2 2025-07-04 08:50:03 +08:00
caoqianming 433218feed fix: 日志多步骤导致的返工填写bug 2025-07-04 08:35:46 +08:00
caoqianming e3067e115a feat: 获取该批次的DAG图数据 支持仅直接关系 2025-07-03 14:54:20 +08:00
caoqianming 79f1322c27 feat: 批次追踪链优化2 2025-07-03 14:38:16 +08:00
caoqianming 17284dcf4f feat: 批次追踪链优化 2025-07-03 13:58:31 +08:00
caoqianming 89aca50f44 fix: 正常交接收料工段与送料工段不能相同 2025-07-03 11:36:18 +08:00
caoqianming 0284117297 release: 2.7.2025070310 2025-07-03 11:01:01 +08:00
caoqianming dd5ef95fa4 feat: ana_wpr忽略模块导入错误 2025-07-03 10:57:10 +08:00
caoqianming 1a621aca34 feat: 生产日志返回提交人姓名 2025-07-02 16:41:54 +08:00
caoqianming 4b2bcc6d29 feat: 修改mlog_submit和mlog_revert以支持日志多个工艺步骤 2025-07-02 15:40:59 +08:00
caoqianming 1d840e7c9d feat: 正常交接收料工段与送料工段不能相同 2025-07-02 10:46:11 +08:00
caoqianming e2f3c95748 feat: handover_revert自己交给自己无需处理 2025-07-02 10:38:01 +08:00
caoqianming 864fb783dc fix: base ticket list 添加create_by_name 2025-07-01 09:34:30 +08:00
caoqianming 8a4ddd0307 feat: base ticket list 添加create_by_name 2025-07-01 09:09:18 +08:00
caoqianming 620e547da5 feat: 单日志支持多工艺步骤 2025-06-30 16:17:09 +08:00
caoqianming a3b0455fb4 feat: 单个统计的触发 2025-06-30 15:07:00 +08:00
caoqianming fdf717aecb fix: 针对未知bug先添加拦截校验 2025-06-30 09:41:08 +08:00
caoqianming a5e8fe3dde feat: wpr data字段数据库同步 2025-06-27 17:14:02 +08:00
caoqianming 5ae9030152 feat: 优化wpr change_or_new 2025-06-27 16:52:15 +08:00
caoqianming e86f4e79d4 feat: mlog添加cnumber查询条件 2025-06-27 16:49:50 +08:00
caoqianming 76cdd08e9a fix: wpr change_or_new时根据number获取最后一个 2025-06-27 15:09:27 +08:00
caoqianming a0ed107491 fix: 出入库明细撤回bug 2025-06-27 15:02:01 +08:00
caoqianming e6d8509971 feat: 出入库明细支持撤回并删除-在一个事务2 2025-06-27 13:34:38 +08:00
caoqianming c054095f94 feat: 出入库明细支持撤回并删除-在一个事务 2025-06-27 13:33:34 +08:00
caoqianming 10b63cdad5 feat: 出入库明细支持撤回并删除2 2025-06-27 13:31:54 +08:00
caoqianming 30379bdab5 feat: 出入库明细支持撤回并删除 2025-06-27 13:29:39 +08:00
caoqianming 1bb449c0db feat: batch_gxerp添加count_notok 2025-06-26 13:56:39 +08:00
caoqianming e25f392175 feat: handover_revert尝试支持全部撤回2 2025-06-26 11:02:29 +08:00
caoqianming c4cde1c5ea feat: handover_revert尝试支持全部撤回 2025-06-26 10:54:32 +08:00
caoqianming a6d53471fc fix: batch_gxerp添加count_pn_jgqbl 2 2025-06-26 09:14:52 +08:00
caoqianming d03f106055 feat: batch_gxerp 添加need_inout查询条件 2025-06-26 09:07:12 +08:00
caoqianming b500ad6615 feat: wpr添加对外编号字段 2025-06-25 18:19:16 +08:00
caoqianming f3263caed0 feat:: batch_gxerp添加count_pn_jgqbl 2025-06-25 17:33:50 +08:00
caoqianming ab22415e0f feat: ofm 完善1 2025-06-25 17:29:46 +08:00
caoqianming 31f4e2869d feat: sql查询有风险提示更完善 2025-06-25 11:23:59 +08:00
caoqianming 43e9642917 fix: 光子get_alldata_with_batch 其他入库获取错误2 2025-06-24 14:34:14 +08:00
caoqianming c338509854 fix: 光子get_alldata_with_batch 人员获取错误2 2025-06-24 14:28:22 +08:00
caoqianming 5dddbfef81 release: 2.7.2025062414 2025-06-24 14:08:15 +08:00
caoqianming 037008d7c9 fix: 光子get_alldata_with_batch 人员获取错误 2025-06-24 13:52:44 +08:00
caoqianming 9ff72b2da1 fix: 光子get_alldata_with_batch优化2 2025-06-24 11:23:45 +08:00
caoqianming 53972fd22a feat: 光子get_alldata_with_batch优化 2025-06-24 09:30:09 +08:00
caoqianming cb09cee753 feat: 添加行政管理App 2025-06-24 08:47:22 +08:00
caoqianming b0df18f01a feat: cd 设置清空缓冲区2 2025-06-23 17:49:02 +08:00
caoqianming c5e4cc48a4 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-23 17:00:41 +08:00
caoqianming 8e33e084b4 feat: cd 设置清空缓冲区 2025-06-23 17:00:39 +08:00
zty ce54f326ee Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-23 13:55:55 +08:00
zty 1242b3d43a feat:enm/service修改排班记录生成时间 2025-06-23 13:55:53 +08:00
caoqianming 108fe0ae9f feat: mlogbout单个产品不支持直接修改 2025-06-23 13:44:28 +08:00
caoqianming f0cacf6777 feat: mioitem添加material__type查询条件 2025-06-20 15:28:49 +08:00
caoqianming 50bf3e78d4 release: 2.6.2025062015 2025-06-20 15:20:49 +08:00
caoqianming 307935cb16 fix: wpr不合格的话先save再创建wm.defect 2025-06-20 13:28:55 +08:00
caoqianming 3683ddbdeb Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-20 11:11:32 +08:00
caoqianming fec244765a fix: 该交接单不支持撤销的bug 2025-06-20 11:11:31 +08:00
zty 6a5ec34ef8 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-20 09:44:57 +08:00
zty 7942a5168b fix: wpm/service 优化煤粉热值获取方式 2025-06-20 09:44:52 +08:00
caoqianming db2e8934ba feat: 增加交接撤回功能 2025-06-19 17:25:33 +08:00
caoqianming 4f08374ca6 feat: mlogbin的bug 2025-06-19 15:43:02 +08:00
caoqianming dce573f892 fix: mioitem price 计算bug 2025-06-19 14:23:49 +08:00
caoqianming 3b04bdf067 feat: mioitem返回mio数据 2025-06-19 14:11:24 +08:00
caoqianming 741b64142b feat: mioitem添加单价字段 2025-06-19 10:37:09 +08:00
caoqianming 25c1cce41a feat: materialsimpleserializer返回bin_number_main 2025-06-18 16:30:41 +08:00
caoqianming 8805237a44 feat: 物料增加主库位号 2025-06-18 16:29:38 +08:00
caoqianming a819566c23 fix: wmaterial添加can_handover字段 2025-06-18 10:56:39 +08:00
caoqianming 6af708d4f0 feat: wmaterial添加can_handover字段 2025-06-18 10:31:35 +08:00
caoqianming 39c86a14d5 release: 2.6.2025061715 2025-06-17 15:39:39 +08:00
caoqianming 8d6f2bfd20 fix: mlogbinupdate时的bug 2025-06-17 08:36:48 +08:00
caoqianming 2b039dd4f7 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-16 17:43:18 +08:00
caoqianming 1366f432ee feat: mlogbin的编辑与mlogbout的联动 2025-06-16 17:42:43 +08:00
caoqianming 31421ca0aa feat: mlogb首次创建需要更新count_ok_full 2025-06-15 22:19:52 +08:00
caoqianming 95b10371b9 feat: 支持从wpr处获取切片数4 2025-06-13 16:36:51 +08:00
caoqianming a820d6803e feat: 支持从wpr处获取切片数3 2025-06-13 16:25:06 +08:00
caoqianming e774992d3a feat: workchain通过线程执行 2025-06-13 16:23:35 +08:00
caoqianming 4955ad1f11 feat: 支持从wpr处获取切片数2 2025-06-13 16:23:07 +08:00
caoqianming ab0c962053 feat: mlogbin联动mlogbout 2025-06-13 16:22:50 +08:00
caoqianming 4bd6eab9f4 feat: mlogbin 和 update时cal_count_notok cal_mlog传False 2025-06-13 15:56:43 +08:00
caoqianming b9a7138e72 feat: mlogbin 和 update时cal_count_notok 2025-06-13 15:24:53 +08:00
caoqianming cf7673cd98 feat: 支持从wpr处获取切片数 2025-06-13 14:31:48 +08:00
caoqianming 680608e16f fix: mlogserializer需要同步count_real 2025-06-13 14:29:31 +08:00
caoqianming 6d87a69d8d fix: cal_mlog_count_from_mlogb 需要传参触发 2025-06-13 11:23:02 +08:00
caoqianming c02bb6d3db fix: mlog 数值计算bug 2025-06-13 09:03:43 +08:00
caoqianming dbad152218 fix: wmaterial get在传入query参数时完善can_do的逻辑 2025-06-12 10:37:19 +08:00
caoqianming bb56becb5d feat: ichat 添加workchain接口 2025-06-12 09:07:43 +08:00
caoqianming b3000b013f feat: utask添加优先级字段 2025-06-11 11:32:07 +08:00
caoqianming 0719ef8f42 feat: gen_number_with_rule bug2 2025-06-11 11:02:50 +08:00
caoqianming fea3c320a9 feat: gen_number_with_rule bug 2025-06-11 10:58:31 +08:00
caoqianming bfc37f69af feat: 优化get_tyy_data 2 2025-06-10 13:35:38 +08:00
caoqianming 8efd28633c feat: 优化get_tyy_data 2025-06-10 12:42:02 +08:00
caoqianming 5e0c515929 feat: batch_gxerp添加加工前缺陷 2025-06-10 10:05:53 +08:00
caoqianming 438d06af2f feat: 增加check_sql_safe 稳定性 2025-06-10 08:48:13 +08:00
caoqianming 84690e7b54 feat: 放开出入库的物料匹配 2025-06-09 15:50:08 +08:00
caoqianming 2e3e8f997c feat: 采购入库节点可复用2 2025-06-09 15:44:31 +08:00
caoqianming 7d29b9c4eb feat: 采购入库节点可复用 2025-06-09 15:37:17 +08:00
caoqianming 016ef53517 release: 2.6.2025060913 2025-06-09 13:33:13 +08:00
caoqianming 77c4c54eb1 feat: inm 和 wpm添加wpr number的查询条件 2025-06-09 13:31:46 +08:00
caoqianming 11f06c36f7 feat: 优化设备采集cd 2025-06-09 13:03:03 +08:00
caoqianming b9cad6bbd8 feat: get_tyy_data 不更新的bug 2025-06-09 10:09:32 +08:00
caoqianming 27b225face feat: mlogb cal_count_pn_jgqbl 2025-06-06 17:44:19 +08:00
caoqianming b102b78b02 release: 2.6.2025060617 2025-06-06 17:27:20 +08:00
caoqianming be5dc161de feat: mlogbw也进行mlogb.cal_count_notok() 2025-06-06 17:25:39 +08:00
caoqianming 53c9afbca9 fix: mlog 的cal_count_notok_full的bug 4 2025-06-06 17:11:00 +08:00
caoqianming 519672ef3f fix: mlog 的cal_count_notok_full的bug 3 2025-06-06 16:50:24 +08:00
caoqianming 6e93d7cd68 fix: mlog 的cal_count_notok_full的bug 2 2025-06-06 16:46:41 +08:00
caoqianming c2770eed57 fix: mlog 的cal_count_notok_full的bug 2025-06-06 16:45:38 +08:00
caoqianming 4cc28b3583 feat: cal_mlog_count_from_mlogb 添加count_ok_full 2025-06-06 16:39:16 +08:00
caoqianming ff2b098210 feat: 优化batches_to 2025-06-06 16:26:52 +08:00
caoqianming fa221218f9 feat: cal_count_notok 完全合格数有bug 2025-06-06 15:58:33 +08:00
caoqianming bb9e2f337a feat: batch_gxerp 全工段查询 2025-06-06 10:48:19 +08:00
caoqianming 7d0491ac24 fix: 光芯批次统计"尺寸检验_完全合格率" 2025-06-05 14:16:41 +08:00
caoqianming dff602abd1 fix: 光芯批次统计错误 2025-06-05 14:01:01 +08:00
caoqianming 60c484ee7f feat: cd BrokenPipeError时尝试再建立连接3 2025-06-05 13:39:24 +08:00
caoqianming facee9342c feat: cd BrokenPipeError时尝试再建立连接2 2025-06-05 11:15:16 +08:00
caoqianming 370c905781 feat: cd BrokenPipeError时尝试再建立连接 2025-06-05 11:05:55 +08:00
caoqianming bd9d5a5dc2 feat: 获取到重复的缺陷项 打日志 2025-06-05 10:50:17 +08:00
caoqianming f06ad961f7 feat: can_fix 排除维修完成品 2025-06-05 08:40:55 +08:00
caoqianming 9d1af24994 fix: handover_submit batches获取bug2 2025-06-05 08:24:20 +08:00
caoqianming 014b48e135 fix: update_mb_item在检验时只做负数校验 2025-06-04 16:40:33 +08:00
caoqianming 265a15c9fc fix: handover_submit batches获取bug 2025-06-04 15:23:19 +08:00
caoqianming 9ede95a5d6 feat: batchst 添加first_time isnull筛选 2025-06-04 10:19:29 +08:00
caoqianming a34514160d feat: 获取first_time, last_time独立出一个方法 2025-06-04 10:05:06 +08:00
caoqianming e1eaa78b11 release: 2.6.2025060409 2025-06-04 09:25:49 +08:00
caoqianming 1f9800db16 fix: 自动生成物料bug 2025-05-30 15:39:13 +08:00
caoqianming 9e9501a065 fix: 优化 gen_material_out 防止自引用 2025-05-30 14:04:35 +08:00
caoqianming ec42e70118 fix: 优化 gen_number_with_rule 2025-05-30 11:11:06 +08:00
caoqianming 7429b7d5fb feat: 修改gen_number_with_rule 以获取的最后编号为准 2025-05-30 10:13:57 +08:00
caoqianming 8994b39917 fix: batches_to_limit 获取bug 2025-05-30 09:56:07 +08:00
caoqianming f689b7609c feat: handover拆批时校验批次号是否可用 2025-05-30 09:07:17 +08:00
caoqianming 45e02eae5f feat: 优化batches_to以达到自然排序功能 2025-05-29 15:30:49 +08:00
caoqianming 5d33301389 feat: batches_to 获取已指向的批次号 2025-05-29 15:08:42 +08:00
caoqianming ad5d8db283 feat: get_qct接口进行校验 2025-05-29 10:55:52 +08:00
caoqianming 4cd4ba3c9d feat: get_tyy_data优化保持socket连接 2025-05-29 10:20:46 +08:00
caoqianming 725a471fd8 fix: batchst reuse_node逻辑完善2 2025-05-29 10:16:23 +08:00
caoqianming 49efafcaff fix: batchst reuse_node逻辑完善 2025-05-29 09:39:04 +08:00
caoqianming 82958e047b fix: base ordering排序错误 2025-05-29 08:39:59 +08:00
caoqianming 70d80dafd2 feat: wmaterial提供canfix的tag查询 2025-05-28 16:30:50 +08:00
caoqianming 37dc922c34 feat: base ComplexQueryMixin 支持order查询2 2025-05-26 16:35:52 +08:00
caoqianming f4fd214110 feat: base ComplexQueryMixin 支持order查询 2025-05-26 16:31:17 +08:00
caoqianming 42ba894875 release: 2.6.2025052615 2025-05-26 15:35:19 +08:00
caoqianming a4f2b6eb55 fix: batchlog创建时避免回环 2025-05-26 15:26:22 +08:00
caoqianming e47464805e feat: bachst添加复杂查询接口 2025-05-26 13:28:43 +08:00
caoqianming d385fefc4f feat: base 独立出ComplexQueryMixin 2025-05-26 13:23:32 +08:00
caoqianming ed6b6c2776 feat: base with_children查询去除限制 2025-05-26 13:11:15 +08:00
caoqianming f41068e923 feat: handover根据wm完善batch字段 2025-05-26 12:10:47 +08:00
caoqianming 6539bc2e61 feat: handoverfilter完善cbatch查询 2025-05-26 11:18:36 +08:00
caoqianming 3a69075b9f feat: 处理dag时返回edge更多信息 2025-05-26 11:07:08 +08:00
caoqianming eefd3691e6 fix: batch_gxerp 优化 2025-05-26 10:33:09 +08:00
caoqianming 74e176ce97 feat: base 性能优化调整位置 2025-05-23 09:54:52 +08:00
caoqianming 9c1ba20d36 perf: 优化mlog和handover的查询 2025-05-23 09:54:24 +08:00
caoqianming 62f261fd75 feat: 所有batch设为textfield并添加索引 2025-05-23 09:24:34 +08:00
caoqianming 359dd8f898 feat: mb添加batch查询条件 2025-05-23 08:44:44 +08:00
caoqianming 1445a14a9a feat: ana batch时等待10s 2025-05-22 16:50:15 +08:00
caoqianming 0735b5d2b7 fix: BatchLog创建时获取source再创建target 2025-05-22 16:48:15 +08:00
caoqianming 6b7088997f feat: batchst复用节点仅限mio创建的 2025-05-22 15:22:49 +08:00
caoqianming 8a7c3830c9 feat: 各工段批次生产进度统计 2025-05-22 13:31:41 +08:00
caoqianming 68d065a2e3 fix: 取消提交日志时时间校验 2025-05-22 09:16:53 +08:00
caoqianming fdb454d6e5 feat: 修改服务器时间时传递密码 2025-05-22 09:09:17 +08:00
caoqianming 36790a72b4 feat: 导入棒管检验测试通过 2025-05-21 14:56:50 +08:00
caoqianming a2e45d6954 feat: 修改导入权限 2025-05-21 13:56:14 +08:00
zty c254d99c3c feat: enm 修改采集保存时间到秒 2025-05-21 09:52:38 +08:00
zty 446f705eb3 feat: auth1 utils enm 修改阿里云发送短信引入方式 2025-05-21 09:51:46 +08:00
caoqianming dbea38f82b feat: daoru棒管test 2025-05-20 16:44:36 +08:00
caoqianming a672a76eee feat: mlog和handover添加cbatch查询条件2 2025-05-20 16:03:36 +08:00
caoqianming 8fb204a93d feat: mlog和handover添加cbatch查询条件 2025-05-20 14:26:58 +08:00
caoqianming 5011ef8076 fix: is_fix支持个到个的bug 2025-05-20 10:09:09 +08:00
caoqianming 126ef8a1b8 feat: mloginit支持handle_users 2025-05-20 08:41:59 +08:00
caoqianming 3500cd25c8 feat: batch_gxerp 完全总合格数 2025-05-19 16:18:31 +08:00
caoqianming 4f9157e19a release: 2.6.2025051913 2025-05-19 13:44:53 +08:00
caoqianming d0027679e6 feat: batch_gxerp调整 2025-05-19 13:37:10 +08:00
caoqianming 5fe2b4db48 feat: qct排序支持number 2025-05-19 11:35:23 +08:00
caoqianming b1487957b5 feat: 完善变更服务器时间的接口2 2025-05-19 10:16:49 +08:00
caoqianming 1a1f64f9fe feat: 完善变更服务器时间的接口 2025-05-19 10:08:58 +08:00
caoqianming 0f0b055101 feat: 添加变更服务器时间的接口 2025-05-19 09:34:14 +08:00
caoqianming 2e0727a2c0 feat: wpr detail 2025-05-16 16:50:57 +08:00
caoqianming b9ca9b802a feat: mlogbw添加wpr等查询条件 2025-05-16 16:42:22 +08:00
caoqianming 05bf1def23 feat: wpr增加自动编号逻辑2 2025-05-16 16:04:37 +08:00
caoqianming 88a5d55425 feat: wpr增加自动编号逻辑 2025-05-16 15:53:33 +08:00
caoqianming 9f3b3a52a7 feat: get_tyy_data优化3 2025-05-16 11:18:55 +08:00
caoqianming 518379ea35 feat: get_tyy_data优化2 2025-05-16 11:12:07 +08:00
caoqianming 1d7c2ebb35 feat: get_tyy_data优化 2025-05-16 10:59:10 +08:00
caoqianming d1ccc9043f feat: mlog添加b_mlog__batch查询条件 2025-05-16 10:12:39 +08:00
caoqianming 1f96162de9 release: 2.6.2025051609 2025-05-16 09:12:10 +08:00
caoqianming 767cfb18e2 feat: 车间库存不足提示更准确2 2025-05-16 09:05:19 +08:00
caoqianming 5a08b5d22c feat: 车间库存不足提示更准确 2025-05-16 08:55:31 +08:00
caoqianming 3075d8ab8e feat: batchst追加查询data__has_key条件 2025-05-15 16:29:22 +08:00
caoqianming 156f6b4844 feat: mlogb_summary选用need_inout为True的数据 2025-05-15 15:35:03 +08:00
caoqianming 0057e4c181 fix: count_ok_full为None的批次处理 2025-05-15 15:19:37 +08:00
caoqianming cbeecea2dc fix: need_inout 传true时也得处理 2025-05-15 14:49:40 +08:00
caoqianming ca9d46c8d5 feat: equip_last_mlog增加报错提示 2025-05-15 10:39:33 +08:00
caoqianming bd7bbbef0e feat: wpr获取material_start 2025-05-15 10:03:14 +08:00
caoqianming 9c69711f38 feat: mlog返回子级详情 2025-05-15 09:50:37 +08:00
caoqianming 8a61ca9eb1 fix: 检验表消失的bug 2025-05-14 16:21:38 +08:00
caoqianming eb8087fdf9 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-05-14 16:16:35 +08:00
caoqianming c50e4bf3a7 fix: batch_gxerp bug 2025-05-14 16:16:33 +08:00
zty 01539a32da Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-05-14 13:58:03 +08:00
zty 8834d66066 feat: ichat 修改大模型的提问 2025-05-14 13:58:01 +08:00
caoqianming 356310db3f feat: 触发批次统计分析在线程里sleep下 2025-05-14 12:57:11 +08:00
caoqianming f32adf3702 fix: 获取检验模板只用单个2 2025-05-14 11:04:36 +08:00
caoqianming 57dfab3bb9 fix: 获取检验模板只用单个 2025-05-14 11:01:21 +08:00
caoqianming d9f66d2970 feat: 获取检验模板只用单个 2025-05-14 10:49:13 +08:00
caoqianming 6fcda8c0a7 fix: wpr获取mb的state 2025-05-14 09:27:46 +08:00
caoqianming a4a9dd257f feat: qct获取通用检验表 2025-05-14 08:59:26 +08:00
caoqianming f6a99f542f feat: develop添加mylogger 2025-05-13 12:57:56 +08:00
caoqianming be6d51af7d feat: wpr添加初始物料字段 2025-05-13 11:00:45 +08:00
caoqianming 98bf81f5e0 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-05-12 16:47:06 +08:00
caoqianming 56afccf0a4 feat: wpr获取新编号 2025-05-12 16:47:04 +08:00
zty 88c029f66d Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-05-12 09:54:18 +08:00
zty f3ab4476a4 feat: ichat /增加ichat模块,优化对话流处理 2025-05-12 09:54:13 +08:00
caoqianming c744c07326 release: 2.6.2025051208 2025-05-12 08:25:23 +08:00
caoqianming 09072f7998 feat: 光芯质检表格 2025-05-09 16:51:55 +08:00
caoqianming ad1bbaa32e feat: base 短信发送功能启动抛出异常2 2025-05-09 09:13:35 +08:00
caoqianming 1c8489a0b4 feat: base 短信发送功能启动抛出异常 2025-05-09 09:10:39 +08:00
caoqianming 06e04858df feat: batchst字段同步数据库 2025-05-08 16:24:06 +08:00
caoqianming 0d3d4160ef release: 2.6.2025050816 2025-05-08 16:18:19 +08:00
caoqianming 954f5d262f feat: 完善清空数据的接口 2025-05-08 16:13:50 +08:00
caoqianming 8a501c2681 feat: batchst添加字段并更新光子的统计2 2025-05-08 15:13:04 +08:00
caoqianming c29d1e99d8 feat: batchst添加字段并更新光子的统计 2025-05-08 14:56:40 +08:00
caoqianming 20384ca33c feat: 由输入转输出需考虑到加工前不良的影响 2025-05-08 13:53:59 +08:00
caoqianming 5e2fd57b89 feat: labeltemplate获取标签指令接口 2025-05-08 13:22:02 +08:00
caoqianming 8a75fcbc42 feat: labeltemplate添加name查询条件 2025-05-08 13:12:32 +08:00
caoqianming cdb4e65159 feat: 初步添加光芯的批次分析函数 2025-05-08 13:12:07 +08:00
caoqianming dae8d50d2c fix: gen_commands bug 2025-05-08 10:45:25 +08:00
caoqianming a9f55eb61c feat: 支持传入label_template_name 2025-05-08 10:37:58 +08:00
caoqianming 7aafa5d9f7 fix: labeltemplate filter bug 2025-05-07 10:25:40 +08:00
caoqianming cc12d80c92 feat: myjsonfield优化 2025-05-07 10:25:07 +08:00
caoqianming 2128b4e847 feat: mioitem添加排序字段 2025-05-06 10:54:09 +08:00
caoqianming adf21e716d fix: 标签模板筛选bug 2025-05-06 10:35:08 +08:00
caoqianming c05432ce56 feat: 标签物料根据模板打印功能 2025-05-06 10:32:50 +08:00
zty a4ba33550e Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-30 14:33:05 +08:00
zty a5a862f7fb feat:ichat 修改接口 去掉langchain 2025-04-30 14:33:01 +08:00
caoqianming 8e35443433 release: 2.6.2025043014 2025-04-30 14:27:57 +08:00
caoqianming 725dd4d554 fix: mio_saleout缺少import 2025-04-30 14:20:15 +08:00
caoqianming da69e654bc feat: 生产入库和领料支持b类合格品 2025-04-30 14:19:49 +08:00
caoqianming a2cba2128f feat: 添加标签模板接口 2025-04-30 13:43:17 +08:00
caoqianming dd6dcad74a feat: 完善光子批次统计数据 2025-04-30 08:51:21 +08:00
caoqianming f35d2d7d9e fix: getattr(item, field) is not None 2025-04-29 15:14:02 +08:00
caoqianming ac610a53ce feat: 组合件和入库检验追加统计分析 2025-04-29 14:43:09 +08:00
caoqianming 7c752ec39b feat: get_alldata_with_batch添加内容 2025-04-29 14:42:29 +08:00
caoqianming 325fad0a6e release: 2.6.2025042816 2025-04-28 16:22:40 +08:00
caoqianming e59c83aa3f feat: 支持个号转批号6 2025-04-28 15:59:29 +08:00
caoqianming ced3e7a16c feat: 支持个号转批号5 2025-04-28 15:50:36 +08:00
caoqianming 6b8fa4e58c feat: 支持个号转批号4 2025-04-28 15:45:05 +08:00
caoqianming 7131697802 feat: 支持个号转批号3 2025-04-28 15:32:04 +08:00
caoqianming 7338d09f5d feat: 支持个号转批号的配置2 2025-04-28 15:25:41 +08:00
caoqianming 0ff9aa1e5a feat: 支持个号转批号的配置 2025-04-28 14:43:22 +08:00
caoqianming c5a7a19f74 feat: 尝试从个号追踪到历史记录的个号2 2025-04-28 14:12:00 +08:00
caoqianming 25a111bc6b feat: 同步数据库 2025-04-28 13:51:00 +08:00
caoqianming 849e63da3e feat: 尝试从个号追踪到历史记录的个号 2025-04-28 13:48:22 +08:00
caoqianming 76a48d46ee feat: 个到批时支持若是一个还是沿用原有批号 2025-04-28 11:28:45 +08:00
caoqianming 76adab1571 feat: 批次追踪链条还是可以复用批次 2025-04-28 11:08:47 +08:00
caoqianming 9d634940d3 feat: mlog结合工序支持批到个 2025-04-28 10:24:28 +08:00
caoqianming d7dfb8c48f fix: url暂时不导入ichat 2025-04-28 09:24:55 +08:00
caoqianming a0346f00bf fix: wpr复用number的bug2 2025-04-28 09:17:01 +08:00
caoqianming 5d94f9615c fix: wpr复用number的bug 2025-04-28 08:17:15 +08:00
caoqianming cd0045c561 feat: mlogbw支持批量创建 2025-04-27 14:12:37 +08:00
caoqianming 8a83f8b7d8 fix: cal_count_notok锁定mlogb以防止并发修改 2025-04-27 13:41:45 +08:00
caoqianming 2ca9b3ce02 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-27 12:17:24 +08:00
caoqianming 8697288d69 feat: mlogbwcreate校验增加wm所属 2025-04-27 12:17:13 +08:00
zty 89c8cac7c1 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-27 09:21:23 +08:00
zty c0277ad9a6 feat: ichat 修改LLM 的接口 2025-04-27 09:21:19 +08:00
caoqianming 139aa6de27 feat: wmaterial筛选条件优化来料已完成 2025-04-26 00:07:44 +08:00
caoqianming cdb9083def fix: batchst恢复成batch单一校验 2025-04-25 23:41:59 +08:00
caoqianming c8d05bed68 feat: 获取该批次的dag数据需要传入version 2025-04-25 21:35:37 +08:00
caoqianming be57fec29f fix: 获取batchst时默认使用version=1 2025-04-25 16:27:48 +08:00
caoqianming d6fe5af39c feat: 单填写mlog支持返工 2025-04-25 15:24:38 +08:00
caoqianming 6033216869 fix: mloginit在返工时不接收route 2025-04-25 14:21:24 +08:00
caoqianming 000ea6fc27 fix: 生产入库时存入生产车间 2025-04-25 13:34:58 +08:00
caoqianming 295edf65d2 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-25 11:27:15 +08:00
caoqianming 439bfeb7d6 fix: 返工校验输入物料选择错误 2025-04-25 11:27:14 +08:00
zty 8f6a6eb973 feat:ichat 修改大模型接口 2025-04-25 10:56:17 +08:00
caoqianming 84380931fd fix: 完善负数校验2 2025-04-24 16:12:38 +08:00
caoqianming ca7d56a462 fix: 完善负数校验 2025-04-24 16:07:55 +08:00
caoqianming 9df6294617 feat: mlogbupdate支持变更批号 2025-04-24 13:44:30 +08:00
caoqianming 1cb4128be2 fix: mlogbupdate时attrs遍历时修改的bug 2025-04-24 13:36:18 +08:00
caoqianming 0f728d8b29 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-24 09:18:05 +08:00
caoqianming 960787d7d8 feat: 6车间合格率统计decimal invalid 2025-04-24 09:18:03 +08:00
zty 7e6ca8cf70 feat:修改大模型文件 2025-04-23 16:54:49 +08:00
zty 8183073026 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server
合并
2025-04-23 16:49:00 +08:00
zty b66157edb8 feat: model add note 2025-04-23 16:48:52 +08:00
caoqianming aa0add371a feat: ichat添加表 2025-04-23 16:47:01 +08:00
caoqianming e2d63ed056 feat: 缺陷项分类字段数据库约束放开 2025-04-23 13:46:03 +08:00
caoqianming dd4d958f86 fix: batchst的version字段bug 2025-04-23 13:02:11 +08:00
caoqianming 2987853afb feat: 通过django settings延迟获取BASE_PROJECT_CODE 2025-04-23 12:42:50 +08:00
caoqianming 74581dc906 feat: base 将配置文件放到单独的config文件夹中防止误操作 2025-04-23 12:39:16 +08:00
caoqianming cbc667d50c release: 2.6.2025042311 2025-04-23 12:24:27 +08:00
caoqianming ba9d7d041d release: 2.6.2025042311 2025-04-23 11:03:38 +08:00
caoqianming 75503fc7e8 feat: 切分工序切分数量支持1 2025-04-23 10:59:59 +08:00
caoqianming 433d85456e fix: mlog的need_inout逻辑3 2025-04-23 09:28:45 +08:00
caoqianming 32185abffd fix: mlog的need_inout逻辑2 2025-04-22 16:33:52 +08:00
caoqianming c2539185e2 feat: mlog增加team字段 2025-04-22 15:49:42 +08:00
caoqianming 3811c055f9 feat: mlog的need_inout逻辑 2025-04-22 15:27:02 +08:00
caoqianming 6e12ca3137 feat: 批次关系链时创建新批次支持使用已有批次号2 2025-04-22 14:30:19 +08:00
caoqianming f22ba2752f feat: 批次关系链时创建新批次支持使用已有批次号 2025-04-22 14:29:33 +08:00
caoqianming ea00a38406 feat: mlogb返回test_user_name 2025-04-22 10:16:10 +08:00
caoqianming 3d0536745a fix: count_working获取逻辑优化2 2025-04-22 09:56:23 +08:00
caoqianming e329c8cbc8 fix: count_working获取逻辑优化 2025-04-22 09:34:34 +08:00
caoqianming 8ac3ad3477 feat: mlogb添加test_user/need_inout字段用于处理抽检逻辑 2025-04-22 09:19:19 +08:00
caoqianming 42c249330e fix: mlogbin解决负值校验存在的bug 2025-04-22 09:14:11 +08:00
caoqianming f12d8ce03c feat: 工艺步骤返回组合而成的name 2025-04-22 09:08:44 +08:00
caoqianming 311e9e9e04 feat: 校验只有合并时才能提供新批次号 2025-04-22 08:43:05 +08:00
caoqianming 5cab0d018b feat: 交接查询可查询子批次号 2025-04-21 16:27:31 +08:00
caoqianming ced7515d80 feat: 日志完善负值校验 2025-04-21 15:43:54 +08:00
caoqianming 2ea3dc79cb feat: 批号追加工段标识2 2025-04-21 13:22:39 +08:00
caoqianming 82587b1736 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-21 11:33:11 +08:00
caoqianming 786d178edc feat: 批号追加工段标识 2025-04-21 11:32:57 +08:00
caoqianming 181d477425 fix: 日志和交接记录操作数正值校验 2025-04-19 23:40:42 +08:00
caoqianming 0489a545e8 fix: inm校验非正数 2025-04-19 23:24:43 +08:00
caoqianming e24d92fe9b feat: base增加PositiveDecimalField 2025-04-19 22:41:26 +08:00
caoqianming adcde59bd1 fix: 其他入库时batchst.g_create传参错误 2025-04-19 21:23:21 +08:00
caoqianming 101a51050d Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-04-19 21:15:35 +08:00
caoqianming 9e4e5a28c6 fix:: 交接记录提交时校验count>0 2025-04-19 21:11:45 +08:00
caoqianming 00e352d8ef feat: 改版交接需要触发统计数量 2025-04-18 14:52:03 +08:00
caoqianming 1e379d412c feat: 物料统计数量接口 2025-04-18 14:18:41 +08:00
caoqianming 2b9e3322ac feat: update_material_count时更新组合件数量 2025-04-18 14:13:34 +08:00
caoqianming 6678e39518 feat: 采购和其他入库可入已有批次 2025-04-18 11:29:32 +08:00
caoqianming b44fec0695 feat: 合并的交接需要校验物料是否一致 2025-04-17 14:05:56 +08:00
caoqianming 6d904341a0 feat: 交接需要校验物料是否一致 2025-04-17 13:30:30 +08:00
caoqianming 369c452dd5 fix: 正常交接后的物料状态不变 2025-04-17 11:17:05 +08:00
caoqianming fafe39c1bd fix: 存在fmlog时将route带给mlog并进行物料校验 2025-04-17 11:16:40 +08:00
caoqianming 044b489388 fix: do_in保证production_dept赋值 2025-04-17 11:16:08 +08:00
caoqianming 1aad0bc481 fix: 订单检索条件错误 2025-04-17 11:13:59 +08:00
caoqianming 5f2385ccd1 fix: fmlog填写mtask或route即可 2025-04-17 09:06:34 +08:00
caoqianming 896be043f5 feat: 返工可选择不合格品/根据工艺路线决定返工后是合格不合格还是返修完成 2025-04-16 19:14:18 +08:00
caoqianming 9179803fda feat: 检验项和缺陷项删除的时候同步删除qct 2025-04-16 19:12:49 +08:00
caoqianming b5c21b7472 feat: fmlog添加工艺步骤字段及相应返回数据 2025-04-16 17:59:20 +08:00
caoqianming 4bc126ca97 release: 2.6.2025041613 2025-04-16 13:49:06 +08:00
caoqianming 300023ff81 fix: 交接分批bug/加工前不良的bug 2025-04-16 13:43:38 +08:00
caoqianming e0217e86c4 feat: 添加count_json_from字段及相应逻辑 2025-04-16 11:31:23 +08:00
caoqianming 4c7f2b24f8 feat: 车间库存按任务筛选时count>0 2025-04-16 08:57:12 +08:00
caoqianming 7a3c5be4eb feat: 工艺步骤增加排序字段 2025-04-15 18:14:00 +08:00
caoqianming d3263236f9 feat: 物料筛选low_inm进行优化 2025-04-15 18:13:41 +08:00
caoqianming 50b264929b fix: mio_saleout时正确计算delivered_count 2025-04-15 17:44:43 +08:00
caoqianming 4874f6356e feat: 变更order状态 2025-04-15 15:09:03 +08:00
caoqianming d864405c0d feat: 扣减库存不足增加提示 2025-04-15 15:08:42 +08:00
caoqianming 8c27b1a03c feat: update_inm关于销售发货/其他出库的bug2 2025-04-15 14:31:58 +08:00
caoqianming afc884eb7f feat: update_inm关于销售发货/其他出库的bug 2025-04-15 11:06:16 +08:00
caoqianming c670bd69e1 feat: fmlog添加is_fix字段校验 2025-04-15 10:30:02 +08:00
caoqianming cd294bc53c fix: 返修交接后物料状态都变为返修品 2025-04-14 23:03:34 +08:00
caoqianming 88bf75baaf feat: fmlog添加is_fix字段 2025-04-14 22:49:31 +08:00
caoqianming 1cbbade280 feat: 单个日志填写时带入wm_in的batch 2025-04-14 22:26:42 +08:00
caoqianming c791bde5be fix: handover拆批请选择车间库存2 2025-04-14 15:29:36 +08:00
caoqianming 22b6b1305f fix: handover拆批请选择车间库存 2025-04-14 15:19:45 +08:00
caoqianming 882b0f2cbd feat: 添加ichat以支持AI对话 2025-04-13 10:12:38 +08:00
caoqianming f228884b27 release: 2.6.2025041113 2025-04-11 13:33:48 +08:00
caoqianming fda9ecae94 fix: get_alldata_with_batch中小数计算异常捕获 2025-04-11 10:16:51 +08:00
caoqianming 1ff822161a feat: ptest性能检验样品编号字段更改为text 2025-04-11 08:42:10 +08:00
caoqianming 4b87f6fe8e Merge branch 'gzerp' 2025-04-09 15:30:58 +08:00
caoqianming 648aaa465c fix: 光子综合查询对小数和None的处理 2025-04-09 15:30:17 +08:00
caoqianming cdc30f5163 release: 2.6.2025040913 2025-04-09 13:20:45 +08:00
caoqianming 060c07abd7 fix: 来料加工和完成筛选完善 2025-04-09 13:19:12 +08:00
caoqianming c0eacd7fa8 fix: decimal存入json字段时使用myjsondecoder 2025-04-09 13:18:42 +08:00
caoqianming 8068cb1ca3 fix: cal_x_task_count分配任务数的bug 2025-04-09 13:18:00 +08:00
caoqianming 15ccfbe6f9 feat: 添加MyJSONEncoder以支持decimal 2025-04-09 10:50:18 +08:00
212 changed files with 10504 additions and 2590 deletions

2
.gitignore vendored
View File

@ -20,6 +20,8 @@ db.sqlite3
server/conf*.py
server/conf.ini
server/conf*.json
config/conf*.py
config/conf*.json
sh/*
temp/*
nohup.out

View File

@ -7,7 +7,7 @@ from apps.utils.models import CommonADModel, CommonBModel
class Area(CommonBModel):
"""
地图区域
TN: 地图区域
"""
AREA_1 = 10
AREA_2 = 20
@ -47,7 +47,7 @@ class Area(CommonBModel):
class Access(CommonADModel):
"""
准入禁入权限(动态变化)
TN: 准入禁入权限(动态变化)
"""
ACCESS_IN_YES = 10
ACCESS_IN_NO = 20

View File

@ -20,6 +20,10 @@ class WxCodeSerializer(serializers.Serializer):
code = serializers.CharField(label="code")
class UserIdSerializer(serializers.Serializer):
user_id = serializers.CharField(label="用户id")
class PwResetSerializer(serializers.Serializer):
phone = serializers.CharField(label="手机号")
code = serializers.CharField(label="验证码")

View File

@ -3,7 +3,8 @@ from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView,
SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView, FaceLoginView)
SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin,
TokenLoginView, FaceLoginView, UserIdLogin)
API_BASE_URL = 'api/auth/'
urlpatterns = [
@ -18,5 +19,6 @@ urlpatterns = [
path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'),
path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'),
path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'),
path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login')
path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login'),
path(API_BASE_URL + 'login_userid/', UserIdLogin.as_view(), name='userid_login'),
]

View File

@ -10,7 +10,7 @@ from apps.auth1.errors import USERNAME_OR_PASSWORD_WRONG
from rest_framework_simplejwt.tokens import RefreshToken
from django.core.cache import cache
from apps.auth1.services import check_phone_code
from apps.utils.sms import send_sms
from apps.utils.tools import rannum
from apps.utils.wxmp import wxmpClient
from apps.utils.wx import wxClient
@ -23,7 +23,8 @@ from apps.auth1.serializers import FaceLoginSerializer
from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer,
PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer)
PwResetSerializer, SecretLoginSerializer,
SendCodeSerializer, WxCodeSerializer, UserIdSerializer)
from apps.system.models import User
from rest_framework_simplejwt.views import TokenObtainPairView
from apps.auth1.authentication import get_user_by_username_or
@ -182,6 +183,7 @@ class SendCode(CreateAPIView):
短信验证码发送
"""
from apps.utils.sms import send_sms
phone = request.data['phone']
code = rannum(6)
is_ok, _ = send_sms(phone, 505, {'code': code})
@ -233,6 +235,29 @@ class SecretLogin(CreateAPIView):
return Response(ret)
raise ParseError('登录失败')
class UserIdLogin(CreateAPIView):
"""直接UserId登录(危险操作)
直接UserId登录
"""
authentication_classes = []
permission_classes = []
serializer_class = UserIdSerializer
def post(self, request):
sr = UserIdSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
userid = vdata['user_id']
try:
user = User.objects.get(id=userid)
except Exception as e:
raise ParseError(f'用户不存在-{e}')
if user:
ret = get_tokens_for_user(user)
return Response(ret)
raise ParseError('登录失败')
class PwResetView(CreateAPIView):
"""重置密码

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-07-25 03:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bi', '0005_datasetrecord'),
]
operations = [
migrations.AddField(
model_name='dataset',
name='enabled',
field=models.BooleanField(default=True, verbose_name='启用'),
),
]

View File

@ -12,6 +12,7 @@ class Dataset(CommonBDModel):
test_param = models.JSONField('测试查询参数', default=dict, blank=True)
default_param = models.JSONField('默认查询参数', default=dict, blank=True)
cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True)
enabled = models.BooleanField('启用', default=True)
# class Report(CommonBDModel):

View File

@ -5,16 +5,19 @@ from apps.bi.models import Dataset
import concurrent
from apps.utils.sql import execute_raw_sql, format_sqldata
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE"]
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE", "INSERT", "CREATE", "ALTER", "GRANT", "REVOKE", "EXEC", "EXECUTE"]
def check_sql_safe(sql: str):
"""检查sql安全性
"""
sql_upper = sql.upper()
# 将SQL按空格和分号分割成单词
words = [word for word in sql_upper.replace(';', ' ').split() if word]
for kw in forbidden_keywords:
if kw in sql_upper:
raise ParseError('sql查询有风险')
# 检查关键字是否作为独立单词出现
if kw in words:
raise ParseError(f'sql查询有风险-{kw}')
return sql
def format_json_with_placeholders(json_str, **kwargs):

View File

@ -4,8 +4,8 @@ from django.utils import timezone
from apps.bi.models import Dataset, DatasetRecord
from apps.bi.services import exec_dataset
import json
from apps.utils.tools import MyJSONEncoder
@shared_task()
def exec_dataset_and_store(code: str, query: str = ''):
dt = Dataset.objects.get(code=code)
@ -16,4 +16,5 @@ def exec_dataset_and_store(code: str, query: str = ''):
if query:
squery = json.loads(query)
dtr.full_sql, dtr.result = exec_dataset(dt, squery)
dtr.result = json.loads(json.dumps(dtr.result, cls=MyJSONEncoder))
dtr.save()

View File

@ -64,6 +64,8 @@ class DatasetViewSet(CustomModelViewSet):
执行sql查询支持code
"""
dt: Dataset = self.get_object()
if not dt.enabled:
raise ParseError(f'{dt.name}-该查询未启用')
rdata = DatasetSerializer(instance=dt).data
xquery = request.data.get('query', {})
is_test = request.data.get('is_test', False)

11
apps/cm/filters.py Normal file
View File

@ -0,0 +1,11 @@
from django_filters import rest_framework as filters
from django.utils import timezone
from .models import LabelTemplate
from apps.utils.filters import MyJsonListFilter
class LabelTemplateFilter(filters.FilterSet):
process = MyJsonListFilter(label='按工序查询', field_name="process_json")
class Meta:
model = LabelTemplate
fields = ['process', "name"]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.12 on 2025-04-30 05:17
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('cm', '0003_alter_lablemat_state'),
]
operations = [
migrations.CreateModel(
name='LabelTemplate',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.TextField(verbose_name='名称')),
('commands', models.JSONField(blank=True, default=list, verbose_name='指令模板')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-05-06 02:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cm', '0004_labeltemplate'),
]
operations = [
migrations.AddField(
model_name='labeltemplate',
name='process_json',
field=models.JSONField(blank=True, default=list, verbose_name='工序'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-05-23 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cm', '0005_labeltemplate_process_json'),
]
operations = [
migrations.AlterField(
model_name='lablemat',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
]

View File

@ -3,13 +3,40 @@ from apps.utils.models import BaseModel
from apps.mtm.models import Material
from apps.pum.models import Supplier
from apps.wpm.models import WmStateOption
from rest_framework.exceptions import NotFound, ParseError
# Create your models here.
class LableMat(BaseModel):
"""TN: 标签物料"""
state = models.PositiveSmallIntegerField('状态', default=10, choices=WmStateOption.choices)
material = models.ForeignKey(Material, on_delete=models.CASCADE)
batch = models.CharField('批次号', max_length=100)
batch = models.TextField('批次号', db_index=True)
supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True)
notok_sign = models.CharField('不合格标记', max_length=10, null=True, blank=True)
defect = models.ForeignKey("qm.defect", verbose_name='缺陷', on_delete=models.SET_NULL, null=True, blank=True)
material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='lm_mo')
material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='lm_mo')
class LabelTemplate(BaseModel):
"""TN: 标签模板"""
name = models.TextField("名称")
commands = models.JSONField("指令模板", default=list, blank=True)
process_json = models.JSONField("工序", default=list, blank=True)
@classmethod
def gen_commands(cls, label_template, label_template_name, tdata):
if label_template:
lt = LabelTemplate.objects.get(id=label_template)
else:
lt = LabelTemplate.objects.filter(name=label_template_name).first()
if not lt:
raise NotFound("标签模板不存在")
commands:list = lt.commands
try:
n_commands = []
for item in commands:
item = item.format(**tdata)
n_commands.append(item)
except Exception as e:
raise ParseError(f"标签解析错误-{str(e)}")
return n_commands

View File

@ -1,11 +1,20 @@
from rest_framework import serializers
from .models import LableMat
from .models import LableMat, LabelTemplate
from apps.qm.models import NotOkOption
from apps.wpm.models import WmStateOption
from apps.utils.serializers import CustomModelSerializer
class TidSerializer(serializers.Serializer):
tid = serializers.CharField(label='表ID')
label_template = serializers.CharField(label='标签模板ID', allow_null=True, required=False)
label_template_name = serializers.CharField(label='标签模板名称', allow_null=True, required=False)
extra_data = serializers.JSONField(label='额外数据', allow_null=True, required=False)
class Tid2Serializer(serializers.Serializer):
label_template = serializers.CharField(label='标签模板ID', allow_null=True, required=False)
label_template_name = serializers.CharField(label='标签模板名称', allow_null=True, required=False)
data = serializers.JSONField(label='数据', allow_null=True, required=False)
class LabelMatSerializer(serializers.ModelSerializer):
@ -23,4 +32,10 @@ class LabelMatSerializer(serializers.ModelSerializer):
return getattr(NotOkOption, obj.notok_sign, NotOkOption.qt).label if obj.notok_sign else None
def get_state_name(self, obj):
return getattr(WmStateOption, str(obj.state), WmStateOption.OK).label if obj.state else None
return getattr(WmStateOption, str(obj.state), WmStateOption.OK).label if obj.state else None
class LabelTemplateSerializer(CustomModelSerializer):
class Meta:
model = LabelTemplate
fields = '__all__'

View File

@ -1,12 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.cm.views import LableMatViewSet
from apps.cm.views import LableMatViewSet, LabelTemplateViewSet
API_BASE_URL = 'api/cm/'
HTML_BASE_URL = 'dhtml/cm/'
router = DefaultRouter()
router.register('labelmat', LableMatViewSet, basename='labelmat')
router.register('labeltemplate', LabelTemplateViewSet, basename='labeltemplate')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,11 +1,12 @@
from apps.cm.models import LableMat
from apps.cm.models import LableMat, LabelTemplate
from rest_framework.decorators import action
from apps.cm.serializers import TidSerializer, LabelMatSerializer
from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer
from apps.inm.models import MaterialBatch, MIOItem
from apps.wpm.models import WMaterial
from rest_framework.exceptions import ParseError, NotFound
from rest_framework.response import Response
from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin
from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin, CustomModelViewSet
from apps.cm.filters import LabelTemplateFilter
# Create your views here.
SPLIT_FIELD = "#"
@ -24,6 +25,9 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
从仓库明细获取物料标签
"""
tid = request.data.get("tid")
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
extra_data = request.data.get("extra_data", {})
try:
mb = MaterialBatch.objects.get(id=tid)
except MaterialBatch.DoesNotExist:
@ -31,6 +35,10 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
obj, _ = LableMat.objects.get_or_create(state=mb.state, material=mb.material, batch=mb.batch, defect=mb.defect, defaults={"supplier": mb.supplier})
rdata = LabelMatSerializer(obj).data
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
if label_template or label_template_name:
tdata = {**rdata, **extra_data}
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
rdata["commands"] = commands
return Response(rdata)
@action(methods=["post"], detail=False, serializer_class=TidSerializer)
@ -41,6 +49,9 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
从车间库存明细获取物料标签
"""
tid = request.data.get("tid")
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
extra_data = request.data.get("extra_data", {})
try:
wm = WMaterial.objects.get(id=tid)
except WMaterial.DoesNotExist:
@ -50,6 +61,10 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
material_origin=wm.material_origin)
rdata = LabelMatSerializer(obj).data
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
if label_template or label_template_name:
tdata = {**rdata, **extra_data}
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
rdata["commands"] = commands
return Response(rdata)
@action(methods=["post"], detail=False, serializer_class=TidSerializer)
@ -60,6 +75,9 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
从出入库明细获取物料标签
"""
tid = request.data.get("tid")
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
extra_data = request.data.get("extra_data", {})
try:
mioitem: MIOItem = MIOItem.objects.get(id=tid)
except MIOItem.DoesNotExist:
@ -67,4 +85,32 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
obj, _ = LableMat.objects.get_or_create(state=10, material=mioitem.material, batch=mioitem.batch, defaults={"supplier": mioitem.mio.supplier})
rdata = LabelMatSerializer(obj).data
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
if label_template or label_template_name:
tdata = {**rdata, **extra_data}
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
rdata["commands"] = commands
return Response(rdata)
class LabelTemplateViewSet(CustomModelViewSet):
"""
list: 标签模板
标签模板
"""
queryset = LabelTemplate.objects.all()
serializer_class = LabelTemplateSerializer
filterset_class = LabelTemplateFilter
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"})
def commands(self, request, *args, **kwargs):
"""
获取标签指令
获取标签指令
"""
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
data = request.data.get("data", {})
return Response({"commands": LabelTemplate.gen_commands(label_template, label_template_name, data)})

View File

@ -3,6 +3,7 @@ from apps.utils.models import CommonADModel
# Create your models here.
class Article(CommonADModel):
"""TN: 文章"""
title = models.CharField(max_length=200, verbose_name="标题")
poster = models.TextField(verbose_name="封面地址", null=True, blank=True)
video = models.TextField(verbose_name="视频地址", null=True, blank=True)

View File

@ -5,6 +5,7 @@ from apps.utils.models import BaseModel
class Project(BaseModel):
"""TN: 项目"""
name = models.CharField('项目名称', max_length=50)
code = models.CharField('标识', max_length=20, unique=True)
config_json = models.JSONField('配置信息', default=dict)

View File

@ -35,4 +35,9 @@ class SpeakerSerializer(serializers.Serializer):
class AreaManSerializer(serializers.Serializer):
area = serializers.CharField()
area = serializers.CharField()
class ServerTimeSerializer(serializers.Serializer):
server_time = serializers.DateTimeField()
timezone = serializers.CharField(read_only=True)

View File

@ -2,6 +2,8 @@ from __future__ import absolute_import, unicode_literals
from celery import shared_task
import subprocess
from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD
import logging
myLogger = logging.getLogger('log')
@shared_task
@ -23,6 +25,7 @@ def backup_database():
@shared_task
def reload_server_git():
command = "bash {}/git_server.sh".format(SH_PATH)
myLogger.info(f"reload_server_git: {command}")
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
if completed.returncode != 0:
return completed.stderr

View File

@ -1,5 +1,6 @@
from django.urls import path, include
from apps.develop.views import BackupDatabase, BackupMedia, ReloadClientGit, ReloadServerGit, ReloadServerOnly, TestViewSet, CorrectViewSet, testScanHtml
from apps.develop.views import (BackupDatabase, BackupMedia, ReloadClientGit,
ReloadServerGit, ReloadServerOnly, TestViewSet, CorrectViewSet, testScanHtml, ServerTime)
from rest_framework.routers import DefaultRouter
API_BASE_URL = 'api/develop/'
@ -14,6 +15,7 @@ urlpatterns = [
path(API_BASE_URL + 'reload_server_only/', ReloadServerOnly.as_view()),
path(API_BASE_URL + 'backup_database/', BackupDatabase.as_view()),
path(API_BASE_URL + 'backup_media/', BackupMedia.as_view()),
path(API_BASE_URL + 'server_time/', ServerTime.as_view()),
path(API_BASE_URL, include(router.urls)),
path(HTML_BASE_URL + "testscan/", testScanHtml)
]

View File

@ -2,14 +2,14 @@
from rest_framework.views import APIView
from rest_framework.exceptions import ParseError
from rest_framework.permissions import IsAdminUser
from rest_framework.permissions import IsAdminUser, AllowAny
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.decorators import action
from apps.am.models import Area
from apps.am.tasks import cache_areas_info
from apps.develop.serializers import AreaManSerializer, CleanDataSerializer, GenerateVoiceSerializer, SendSmsSerializer, SpeakerSerializer, \
TestTaskSerializer, TestAlgoSerializer
TestTaskSerializer, TestAlgoSerializer, ServerTimeSerializer
from apps.develop.tasks import backup_database, backup_media, reload_web_git, reload_server_git, reload_server_only
from rest_framework.exceptions import APIException
from apps.ecm.service import check_not_in_place, create_remind, handle_xx_event, loc_change, notify_event, rail_in, snap_and_analyse
@ -19,7 +19,6 @@ from apps.opm.models import Opl
from apps.third.dahua import dhClient
from apps.third.speaker import spClient
from apps.third.models import TDevice
from apps.utils.permission import RbacPermission
from apps.utils.sms import send_sms
from apps.utils.speech import generate_voice
from apps.utils.tools import get_info_from_id
@ -29,15 +28,50 @@ from rest_framework.authentication import BasicAuthentication, SessionAuthentica
from apps.utils.viewsets import CustomGenericViewSet
from apps.utils.wx import wxClient
from apps.wf.models import State, Transition, Workflow
from apps.wf.models import Transition
from django.db import transaction
from apps.utils.snowflake import idWorker
from django.core.cache import cache
import json
from apps.utils.decorators import auto_log, idempotent
from apps.utils.decorators import auto_log
from django.http import HttpResponse
from server.settings import SD_PWD, TIME_ZONE
import subprocess
from drf_yasg.utils import swagger_auto_schema
from datetime import datetime
# Create your views here.
class ServerTime(APIView):
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return [IsAdminUser()]
@swagger_auto_schema(responses={200: ServerTimeSerializer})
def get(self, request):
"""
获取服务器时间
获取服务器时间
"""
return Response({"server_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timezone": TIME_ZONE})
@swagger_auto_schema(request_body=ServerTimeSerializer)
def post(self, request):
"""
修改服务器时间
修改服务器时间
"""
command = f'date -s "{request.data["server_time"]}"'
completed = subprocess.run(
["sudo", "-S", "sh", "-c", command], # 添加 -S 参数
input=SD_PWD + "\n", # 注意要在密码后加换行符
capture_output=True,
text=True
)
if completed.returncode != 0:
raise ParseError(completed.stderr)
return Response()
class ReloadServerGit(APIView):
permission_classes = [IsAdminUser]
@ -616,14 +650,14 @@ class TestViewSet(CustomGenericViewSet):
'apps.rpm.tasks.close_rpj_by_leave_time']).delete()
if 'mes' in datas:
from apps.inm.models import MaterialBatch, MIO
from apps.mtm.models import Material
from apps.mtm.models import Material, RoutePack
from apps.wpmw.models import Wpr
from apps.wpm.models import WMaterial, Mlog
from apps.wpm.models import WMaterial, Mlog, Handover, BatchSt
from apps.pum.models import PuOrder
from apps.sam.models import Order
from apps.pm.models import Utask, Mtask
from apps.wpm.models import Handover
from apps.qm.models import Ftest, FtestWork
from apps.wf.models import Ticket
MaterialBatch.objects.all().delete()
MIO.objects.all().delete()
Wpr.objects.all().delete()
@ -636,7 +670,9 @@ class TestViewSet(CustomGenericViewSet):
Handover.objects.all().delete()
Ftest.objects.all().delete()
FtestWork.objects.all().delete()
Material.objects.all().update(count=0, count_mb=0, count_wm=0)
Material.objects.get_queryset(all=True).update(count=0, count_mb=0, count_wm=0)
BatchSt.objects.all().delete()
Ticket.objects.get_queryset(all=True).delete()
return Response()
@action(methods=['post'], detail=False, serializer_class=Serializer, permission_classes=[])

View File

@ -5,7 +5,7 @@ from django_celery_beat.models import PeriodicTask
# Create your models here.
class RiskPoint(CommonAModel):
"""
风险点表
TN: 风险点表
"""
R_LEVEL = (
(10, '低风险'),
@ -34,7 +34,7 @@ class RiskPoint(CommonAModel):
class Risk(CommonAModel):
"""
风险表
TN: 风险表
"""
name = models.TextField('项目/步骤')
level = models.PositiveSmallIntegerField('风险等级', default=10, choices=RiskPoint.R_LEVEL)
@ -56,7 +56,7 @@ class Risk(CommonAModel):
class CheckTaskSet(CommonADModel):
"""
检查任务派发设置
TN:检查任务派发设置
"""
riskpoint = models.ForeignKey(RiskPoint, verbose_name='关联风险点', related_name='ctask_riskpoint', on_delete=models.SET_NULL, null=True, blank=True)
note = models.TextField('派发备注', null=True, blank=True)
@ -68,7 +68,7 @@ class CheckTaskSet(CommonADModel):
class CheckWork(CommonAModel):
"""
检查工作
TN: 检查工作
"""
CW_TYPE = (
(10, '手动'),
@ -90,7 +90,7 @@ class CheckWork(CommonAModel):
class Hazard(CommonADModel):
"""
事故隐患表
TN: 事故隐患表
"""
H_HARM = (
(10, '无伤害'),
@ -142,7 +142,7 @@ class Hazard(CommonADModel):
class CheckItem(BaseModel):
"""
检查工作-隐患关联表
TN:检查工作-隐患关联表
"""
CITEM_RESULT = (
(10, '未检查'),

View File

@ -16,7 +16,7 @@ LEVEL_CHOICES = (
class EventCate(CommonAModel):
"""
事件种类
TN: 事件种类
"""
EVENT_TRIGGER_CHOICES = (
(10, '监控'),
@ -47,7 +47,7 @@ class EventCate(CommonAModel):
class AlgoChannel(BaseModel):
"""TN: 算法通道"""
algo = models.ForeignKey(EventCate, verbose_name='关联算法', on_delete=models.CASCADE, related_name='ac_algo')
vchannel = models.ForeignKey(TDevice, verbose_name='视频通道', on_delete=models.CASCADE, related_name='ac_vchannel')
always_on = models.BooleanField("实时开启", default=False)
@ -58,7 +58,7 @@ class AlgoChannel(BaseModel):
class NotifySetting(CommonADModel):
"""
提醒配置
TN: 提醒配置
"""
PUSH_FILTER1_CHOICES = (
(10, '当事人部门'),
@ -84,7 +84,7 @@ class NotifySetting(CommonADModel):
class Event(CommonBDModel):
"""
事件
TN: 事件
"""
EVENT_MARK_CHOICES = (
(10, '正常'),
@ -118,6 +118,7 @@ class Event(CommonBDModel):
class Eventdo(BaseModel):
"""TN: 事件处理记录"""
cate = models.ForeignKey(EventCate, verbose_name='关联事件种类',
on_delete=models.CASCADE, related_name='do_cate')
event = models.ForeignKey(Event, verbose_name='关联事件',
@ -130,7 +131,7 @@ class Eventdo(BaseModel):
class Remind(BaseModel):
"""
事件提醒表
TN: 事件提醒表
"""
event = models.ForeignKey(Event, verbose_name='关联事件',
on_delete=models.CASCADE)

View File

@ -6,6 +6,7 @@ from apps.system.models import User, Dept, File
class Train(CommonADModel):
"""TN: 线下培训"""
T_L_POST = 10
T_L_TEAM = 20
T_L_DEPT = 30
@ -27,6 +28,7 @@ class Train(CommonADModel):
class Questioncat(CommonAModel):
"""TN: 题库分类"""
name = models.CharField(max_length=200, verbose_name="名称")
parent = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="父级", related_name="cate_parent")
sort = models.PositiveIntegerField(default=0, verbose_name="排序")
@ -40,6 +42,7 @@ class Questioncat(CommonAModel):
class Question(CommonAModel):
"""TN: 题目"""
Q_DAN = 10
Q_DUO = 20
Q_PAN = 30
@ -65,6 +68,7 @@ class Question(CommonAModel):
class Paper(CommonAModel):
"""TN: 押题卷"""
PAPER_FIX = 10 # 固定试卷
PAPER_RANDOM = 20 # 自动抽题
@ -94,6 +98,7 @@ class Paper(CommonAModel):
class PaperQuestion(BaseModel):
"""TN: 试卷-试题关系表"""
paper = models.ForeignKey(Paper, on_delete=models.CASCADE, verbose_name="试卷")
question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name="试题")
total_score = models.FloatField(default=0, verbose_name="单题满分")
@ -101,6 +106,7 @@ class PaperQuestion(BaseModel):
class Exam(CommonADModel):
"""TN: 在线考试"""
name = models.CharField("名称", max_length=100)
open_time = models.DateTimeField("开启时间")
close_time = models.DateTimeField("关闭时间", null=True, blank=True)
@ -120,7 +126,7 @@ class Exam(CommonADModel):
class ExamRecord(CommonADModel):
"""
考试记录表
TN: 考试记录表
"""
exam = models.ForeignKey(Exam, on_delete=models.CASCADE, verbose_name="关联考试", related_name="record_exam")
@ -139,6 +145,7 @@ class ExamRecord(CommonADModel):
class AnswerDetail(BaseModel):
"""TN: 答题记录详情表"""
examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE, related_name="detail_er")
total_score = models.FloatField(default=0, verbose_name="该题满分")
question = models.ForeignKey(Question, on_delete=models.CASCADE)

View File

@ -1,26 +1,252 @@
import socket
from rest_framework.exceptions import ParseError
import json
import time
from django.core.cache import cache
from apps.utils.thread import MyThread
import uuid
import logging
import threading
import requests
myLogger = logging.getLogger('log')
def get_checksum(body_msg):
return sum(body_msg) & 0xFF
def handle_bytes(arr):
if len(arr) < 8:
return f"返回数据长度错误-{arr}"
if arr[0] != 0xEB or arr[1] != 0x90:
return "数据头不正确"
# 读取长度信息
length_arr = arr[2:4][::-1] # 反转字节
length = int.from_bytes(length_arr, byteorder='little', signed=True) # 小端格式
# 提取内容
body = arr[4:4 + length - 3]
# 校验和检查
check_sum = get_checksum(body)
if check_sum != arr[length + 1]:
return "校验错误"
# 尾部标识检查
if arr[length + 2] != 0xFF or arr[length + 3] != 0xFE:
return "尾错误"
content = body.decode('utf-8')
res = json.loads(content)
return res[0]
def get_tyy_data_t(host, port, tid):
cd_thread_key_id = f"cd_thread_{host}_{port}_id"
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
sc = None
def connect_and_send(retry=1):
nonlocal sc
try:
if sc is None:
sc = socket.socket()
sc.connect((host, int(port)))
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
connect_and_send(retry-1)
else:
if sc:
try:
sc.close()
except Exception:
pass
sc = None
except OSError as e:
sc = None
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"})
except ConnectionResetError:
sc = None
cache.set(cd_thread_key_val, {"err_msg": "采集器重置了连接"})
except socket.timeout:
sc = None
cache.set(cd_thread_key_val, {"err_msg": "采集器连接超时"})
except Exception as e:
sc = None
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"})
while cache.get(cd_thread_key_id) == tid:
if cache.get(cd_thread_key_val) == "get":
cache.set(cd_thread_key_val, "working")
connect_and_send()
if sc is None:
continue
resp = sc.recv(1024)
res = handle_bytes(resp)
if isinstance(res, str):
cache.set(cd_thread_key_val, {"err_msg": f'采集器返回数据错误-{res}'})
elif not res:
cache.set(cd_thread_key_val, {"err_msg": f"采集器返回数据为空-{str(res)}"})
else:
myLogger.info(f"采集器返回数据-{res}")
cache.set(cd_thread_key_val, res)
time.sleep(0.3)
if sc:
try:
sc.close()
except Exception:
pass
def get_tyy_data_2(*args, retry=1):
host, port = args[0], int(args[1])
cd_thread_key_id = f"cd_thread_{host}_{port}_id"
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
cd_thread_val_id = cache.get(cd_thread_key_id, default=None)
if cd_thread_val_id is None:
tid = uuid.uuid4()
cache.set(cd_thread_key_id, tid, timeout=10800)
cd_thread = MyThread(target=get_tyy_data_t, args=(host, port, tid), daemon=True)
cd_thread.start()
cache.set(cd_thread_key_val, "get")
num = 0
get_val = False
while True:
num += 1
if num > 8:
break
val = cache.get(cd_thread_key_val)
if isinstance(val, dict):
get_val = True
if "err_msg" in val:
raise ParseError(val["err_msg"])
return val
time.sleep(0.3)
if not get_val and retry > 0:
cache.set(cd_thread_key_id, None)
get_tyy_data_2(*args, retry=retry-1)
sc_all = {}
sc_lock = threading.Lock()
def get_tyy_data_1(*args, retry=1):
host, port = args[0], int(args[1])
global sc_all
sc = None
def connect_and_send(retry=1):
nonlocal sc
sc = sc_all.get(f"{host}_{port}", None)
try:
if sc is None:
sc = socket.socket()
sc.settimeout(5) # 设置超时
sc.connect((host, port))
sc_all[f"{host}_{port}"] = sc
else:
# 清空接收缓冲区
sc.settimeout(0.1) # 设置短暂超时
for _ in range(5):
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5) # 恢复原超时设置
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
return connect_and_send(retry-1)
else:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError("采集器连接失败-管道重置")
except OSError as e:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接失败-{str(e)}")
except TimeoutError as e:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接超时-{str(e)}")
with sc_lock:
connect_and_send()
resp = sc.recv(1024)
res = handle_bytes(resp)
# myLogger.error(res)
if isinstance(res, str):
raise ParseError(f'采集器返回数据错误-{res}')
else:
return res
def get_tyy_data_3(*args, retry=2):
host, port = args[0], int(args[1])
for attempt in range(retry):
try:
# 每次请求都新建连接(确保无共享状态)
with socket.create_connection((host, port), timeout=10) as sc:
sc.sendall(b"R")
# 接收完整响应(避免数据不完整)
# resp = b""
# while True:
# chunk = sc.recv(4096)
# if not chunk:
# break
# resp += chunk
resp = sc.recv(4096)
if not resp:
raise ParseError("设备未启动")
res = handle_bytes(resp)
if isinstance(res, str):
raise ParseError(f"采集器返回数据错误: {res}")
return res
except (socket.timeout, ConnectionError) as e:
if attempt == retry - 1: # 最后一次尝试失败才报错
raise ParseError(f"采集器连接失败: {str(e)}")
time.sleep(0.5) # 失败后等待 1s 再重试
except ParseError:
raise
except Exception as e:
raise ParseError(f"未知错误: {str(e)}")
def get_tyy_data(*args):
sc = socket.socket()
try:
sc.connect((args[0], int(args[1])))
except Exception:
raise ParseError("无法连接到采集器")
sc.send(b"R")
resp = sc.recv(1024)
if len(resp) < 8:
raise ParseError("设备未启动")
try:
json_data = resp[5:-4]
json_str = json_data.decode('utf-8')
res = json.loads(json_str)
except Exception:
raise
finally:
sc.close()
return res
if __name__ == '__main__':
print(get_tyy_data())
host, port = args[0], int(args[1])
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")
res = r.json()
if "err_msg" in res:
raise ParseError(res["err_msg"])
return res

View File

@ -9,6 +9,7 @@ etype_choices = ((10, "生产设备"), (20, "计量设备"), (30, "治理设备"
class Ecate(CommonADModel):
"""TN:设备类别"""
name = models.CharField("名称", max_length=50, unique=True)
code = models.CharField("编码", max_length=50, unique=True, null=True, blank=True)
type = models.PositiveSmallIntegerField("类型", choices=etype_choices, help_text=str(etype_choices))
@ -22,8 +23,7 @@ class Ecate(CommonADModel):
class Equipment(CommonBModel):
"""
设备台账信息
其中belong_dept是责任部门
TN:设备台账信息,其中belong_dept是责任部门
"""
RUNING = 10
STANDBY = 20
@ -126,7 +126,7 @@ class Equipment(CommonBModel):
class EcheckRecord(CommonADModel):
"""
校准检定记录
TN:校准检定记录
"""
CHECK_CHOICES = ((10, "正常"), (20, "异常"))
@ -139,7 +139,7 @@ class EcheckRecord(CommonADModel):
class EInspect(CommonADModel):
"""
巡检记录
TN:巡检记录
"""
INSPECT_RESULTS = (

View File

@ -180,7 +180,9 @@ class CdView(MyLoggingMixin, APIView):
执行采集数据方法
"""
method = request.data.get("method")
method = request.data.get("method", None)
if not method:
raise ParseError("请传入method参数")
m = method.split("(")[0]
args = method.split("(")[1].split(")")[0].split(",")
module, func = m.rsplit(".", 1)

View File

@ -5,6 +5,7 @@ from apps.mtm.models import Material, Mgroup, Team
from django_celery_beat.models import PeriodicTask
class Xscript(BaseModel):
"""TN:脚本"""
name = models.CharField("脚本名称", max_length=50)
code = models.TextField("脚本内容", null=True, blank=True)
base_data = models.JSONField("基础数据", default=dict, null=True, blank=True)
@ -13,7 +14,7 @@ class Xscript(BaseModel):
periodictask = models.ForeignKey(PeriodicTask, verbose_name='关联定时任务', on_delete=models.CASCADE, related_name='xscript_periodictask', null=True, blank=True)
class Mpoint(CommonBModel):
"""测点"""
"""TN:测点"""
MT_AUTO = 10
MT_COMPUTE = 20
MT_MANUAL = 30
@ -61,7 +62,7 @@ class Mpoint(CommonBModel):
cal_coefficient = models.FloatField("计算系数", null=True, blank=True)
mpoint_from = models.ForeignKey("self", verbose_name="来源自采测点", related_name="mp_mpoint_from", on_delete=models.SET_NULL, null=True, blank=True)
cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "运行时统计")], null=True, blank=True)
cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "不涉及"), (20, "运行时统计")], null=True, blank=True)
@classmethod
def cache_key(cls, code: str):
@ -69,7 +70,7 @@ class Mpoint(CommonBModel):
class MpLogx(models.Model):
"""
测点记录超表
TN:测点记录超表
"""
timex = models.DateTimeField("采集时间", primary_key=True)
@ -87,7 +88,7 @@ class MpLogx(models.Model):
class MpLog(BaseModel):
"""旧表(已废弃)"""
"""TN:旧表(已废弃)"""
mpoint = models.ForeignKey(Mpoint, verbose_name="关联测点", on_delete=models.SET_NULL, null=True, blank=True)
tag_id = models.BigIntegerField("记录ID", db_index=True)
@ -97,7 +98,7 @@ class MpLog(BaseModel):
class MpointStat(CommonADModel):
"""测点统计表"""
"""TN:测点统计表"""
type = models.CharField("统计维度", max_length=50, default="hour", help_text="year/month/day/year_s/month_s/month_st/day_s/sflog/hour_s/hour")
year = models.PositiveSmallIntegerField("", null=True, blank=True)
@ -123,7 +124,7 @@ class MpointStat(CommonADModel):
class EnStat(BaseModel):
"""
能源数据统计表
TN:能源数据统计表
"""
type = models.CharField("统计维度", max_length=50, default="hour", help_text="year_s/month_s/month_st/day_s/sflog/hour_s")
@ -174,7 +175,7 @@ class EnStat(BaseModel):
class EnStat2(BaseModel):
"""
能源数据统计表2
TN:能源数据统计表2
"""
type = models.CharField("统计维度", max_length=50, default="month_s", help_text="month_s/day_s")

View File

@ -207,6 +207,8 @@ class MpointCache:
set_eq_rs(ep_belong_id, timex, Equipment.OFFLINE)
def set(self, last_timex: datetime, last_val):
# last_timex保存到秒
last_timex = last_timex.replace(microsecond=0)
current_cache_val = self.data
cache_key = self.cache_key
last_data = current_cache_val["last_data"]

View File

@ -86,10 +86,10 @@ def db_ins_mplogx():
if bill_date is None:
raise Exception("bill_date is None")
query = """
SELECT id, de_real_quantity, CONCAT('x', inv_name) AS inv_name, bill_date
SELECT id, de_real_quantity, inv_code, bill_date
FROM sa_weigh_view
WHERE bill_date >= %s and de_real_quantity > 0
AND inv_name IN %s
AND inv_code IN %s
ORDER BY bill_date
"""
cursor.execute(query, (bill_date, tuple(batchs)))
@ -167,11 +167,11 @@ def get_first_stlog_time_from_duration(mgroup:Mgroup, dt_start:datetime, dt_end:
if st:
return st, "ending"
st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__lte=600).order_by("start_time").last()
st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__gte=600).order_by("start_time").last()
if st:
return st, "start"
st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__lte=600).order_by("end_time").first()
st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__gte=600).order_by("end_time").first()
if st:
return st, "end"
@ -213,7 +213,7 @@ def cal_mpointstat_hour(mpointId: str, year: int, month: int, day: int, hour: in
val = abs(first_val - last_val)
else:
xtype = "normal"
if mpointfrom and mpoint.cal_related_mgroup_running == 10:
if mpointfrom and mpoint.cal_related_mgroup_running == 20:
stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n)

View File

@ -1,18 +1,19 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.enm.views import (MpointViewSet, MpLogxViewSet, MpointStatViewSet,
EnStatViewSet, EnStat2ViewSet, XscriptViewSet)
from apps.enm.views import (MpointViewSet, MpointStatViewSet,
EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView)
API_BASE_URL = 'api/enm/'
HTML_BASE_URL = 'dhtml/enm/'
router = DefaultRouter()
router.register('mpoint', MpointViewSet, basename='mpoint')
router.register('mplogx', MpLogxViewSet, basename='mplogx')
# router.register('mplogx', MpLogxViewSet, basename='mplogx')
router.register('mpointstat', MpointStatViewSet, basename='mpointstat')
router.register('enstat', EnStatViewSet, basename='enstat')
router.register('enstat2', EnStat2ViewSet, basename='enstat2')
router.register('xscript', XscriptViewSet, basename='xscript')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
path(f'{API_BASE_URL}mplogx/', MpLogxAPIView.as_view(), name='mplogx_list'),
]

View File

@ -12,6 +12,7 @@ from apps.enm.tasks import cal_mpointstat_manual
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.decorators import action
from rest_framework.views import APIView
from apps.enm.tasks import cal_mpointstats_duration
from apps.enm.services import king_sync, MpointCache
from django.db import transaction
@ -21,7 +22,13 @@ from apps.enm.services import get_analyse_data_mgroups_duration
from django.db.models import Sum
import logging
from django.core.cache import cache
from apps.utils.sql import query_one_dict, query_all_dict
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from django.utils import timezone
myLogger = logging.getLogger('log')
class MpointViewSet(CustomModelViewSet):
"""
list:测点
@ -84,6 +91,34 @@ class MpointViewSet(CustomModelViewSet):
king_sync(getattr(settings, "KING_PROJECTNAME", ""))
return Response()
@action(methods=["post"], detail=False, perms_map={"post": "mpoint.create"}, serializer_class=Serializer)
def show_picture(self, request, *args, **kwargs):
import requests
import os
headers = {
"Content-Type": "application/json;charset=utf-8",
}
url = "http://localhost:8093/boxplot"
payload = {
"startTime1": request.data.get("startTime1"),
"endTime1": request.data.get("endTime1"),
"startTime2": request.data.get("startTime2"),
"endTime2": request.data.get("endTime2")
}
try:
response = requests.request("POST", url, json=payload, headers=headers)
except Exception as e:
myLogger.error(e)
pic_dir = os.path.join(settings.MEDIA_ROOT, "box_pic")
os.makedirs(pic_dir, exist_ok=True)
file_name= datetime.now().strftime('%Y%m%d_%H%M%S')+'.png'
pic_path = os.path.join(pic_dir, file_name)
with open(pic_path, 'wb') as f:
f.write(response.content)
rel_path = os.path.join('media/box_pic', file_name)
rel_path = rel_path.replace('\\', '/')
return Response({"rel_path": rel_path})
class XscriptViewSet(CustomModelViewSet):
"""
@ -138,6 +173,97 @@ class XscriptViewSet(CustomModelViewSet):
# select_related_fields = ['mpoint']
# filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept']
class MpLogxAPIView(APIView):
"""
list:测点采集数据
测点采集数据
"""
perms_map = {"get": "*"}
@swagger_auto_schema(manual_parameters=[
openapi.Parameter('mpoint', openapi.IN_QUERY, description='测点ID', type=openapi.TYPE_STRING),
openapi.Parameter('timex__gte', openapi.IN_QUERY, description='开始时间', type=openapi.TYPE_STRING),
openapi.Parameter('timex__lte', openapi.IN_QUERY, description='结束时间', type=openapi.TYPE_STRING),
openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER),
openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量', type=openapi.TYPE_INTEGER),
openapi.Parameter('ordering', openapi.IN_QUERY, description='排序字段,如 -timex', type=openapi.TYPE_STRING),
openapi.Parameter('fields', openapi.IN_QUERY, description='返回字段,如 timex,val_float,val_int', type=openapi.TYPE_STRING),
])
def get(self, request, *args, **kwargs):
mpoint = request.query_params.get("mpoint", None)
timex__gte_str = request.query_params.get("timex__gte", None)
timex__lte_str = request.query_params.get("timex__lte", None)
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 20))
fields = request.query_params.get("fields", None)
if page < 0 and page_size < 0:
raise ParseError("page, page_size must be positive")
ordering = request.query_params.get("ordering", "-timex") # 默认倒序
if mpoint is None or timex__gte_str is None:
raise ParseError("mpoint, timex__gte are required")
# 处理时间
timex__gte = timezone.make_aware(datetime.strptime(timex__gte_str, "%Y-%m-%d %H:%M:%S"))
timex__lte = timezone.make_aware(datetime.strptime(timex__lte_str, "%Y-%m-%d %H:%M:%S")) if timex__lte_str else timezone.now()
# 统计总数
count_sql = """SELECT COUNT(*) AS total_count FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s"""
count_data = query_one_dict(count_sql, [mpoint, timex__gte, timex__lte], with_time_format=True)
# 排序白名单
allowed_fields = {"timex", "val_mrs", "val_int", "val_float"} # 根据表字段修改
order_fields = []
for field in ordering.split(","):
field = field.strip()
if not field:
continue
desc = field.startswith("-")
field_name = field[1:] if desc else field
if field_name in allowed_fields:
order_fields.append(f"{field_name} {'DESC' if desc else 'ASC'}")
# 如果没有合法字段,使用默认排序
if not order_fields:
order_fields = ["timex DESC"]
order_clause = "ORDER BY " + ", ".join(order_fields)
# 构造 SQL
if page == 0:
if fields:
# 过滤白名单,避免非法列
fields = [f for f in fields.split(",") if f in allowed_fields]
if not fields:
fields = ["timex", "val_float", "val_int"] # 默认列
select_clause = ", ".join(fields)
else:
select_clause = "timex, val_float, val_int" # 默认列
page_sql = f"""SELECT {select_clause} FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
{order_clause}"""
page_params = [mpoint, timex__gte, timex__lte]
else:
page_sql = f"""SELECT * FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
{order_clause} LIMIT %s OFFSET %s"""
page_params = [mpoint, timex__gte, timex__lte, page_size, (page-1)*page_size]
page_data = query_all_dict(page_sql, page_params, with_time_format=True)
if page == 0:
return Response(page_data)
return Response({
"count": count_data["total_count"],
"page": page,
"page_size": page_size,
"results": page_data
})
class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
"""
@ -151,11 +277,20 @@ class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
serializer_class = MpLogxSerializer
filterset_fields = {
"timex": ["exact", "gte", "lte", "year", "month", "day"],
"mpoint": ["exact"],
"mpoint": ["exact", "in"],
"mpoint__ep_monitored": ["exact"]
}
ordering_fields = ["timex"]
ordering = ["-timex"]
@action(methods=["get"], detail=False, perms_map={"get": "*"})
def to_wide(self, request, *args, **kwargs):
"""转换为宽表
转换为宽表
"""
queryset = self.filter_queryset(self.get_queryset())
class MpointStatViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""

View File

@ -6,6 +6,7 @@ from apps.em.models import Equipment
class Drain(CommonBModel):
"""TN:排口表"""
DR_TYPE_CHOICES = (
(10, "排放口"),
(20, "污染源"),
@ -37,7 +38,7 @@ class Drain(CommonBModel):
class DrainEquip(BaseModel):
"""
排口/设备关系表
TN:排口/设备关系表
"""
drain = models.ForeignKey(Drain, verbose_name="排口", related_name="drainequip_drain", on_delete=models.CASCADE)
@ -50,7 +51,7 @@ class DrainEquip(BaseModel):
class EnvData(models.Model):
"""
环保监测数据
TN:环保监测数据
"""
enp_fields = [
@ -105,6 +106,7 @@ class EnvData(models.Model):
class VehicleAccess(BaseModel):
"""TN:车辆出入厂记录"""
type = models.PositiveSmallIntegerField("出入类型", default=1, help_text="1: 进厂, 2: 出厂")
vehicle_number = models.CharField("车牌号", max_length=10)
access_time = models.DateTimeField("出入时间", null=True, blank=True)
@ -114,6 +116,7 @@ class VehicleAccess(BaseModel):
class CarWash(BaseModel):
"""TN:洗车记录"""
station = models.ForeignKey(Equipment, verbose_name="洗车台", on_delete=models.CASCADE)
vehicle_number = models.CharField("车牌号", max_length=10, default="")
start_time = models.DateTimeField("洗车时间", null=True, blank=True)

View File

@ -6,6 +6,7 @@ from apps.wpm.models import SfLog
# Create your models here.
class Fee(BaseModel):
"""TN:费用"""
name = models.CharField('名称', max_length=20)
cate = models.CharField('父名称', max_length=20)
element = models.CharField('要素', max_length=20, help_text='直接材料/直接人工/制造费用')
@ -13,6 +14,7 @@ class Fee(BaseModel):
class FeeSet(CommonADModel):
"""TN:费用集合"""
year = models.PositiveSmallIntegerField('年份')
month = models.PositiveSmallIntegerField('月份')
mgroup = models.ForeignKey(Mgroup, verbose_name='关联工段', on_delete=models.CASCADE)
@ -21,6 +23,7 @@ class FeeSet(CommonADModel):
class PriceSet(CommonADModel):
"""TN:价格集合"""
year = models.PositiveSmallIntegerField('年份')
month = models.PositiveSmallIntegerField('月份')
material = models.ForeignKey('mtm.material', on_delete=models.CASCADE, verbose_name='关联物料')

View File

@ -8,7 +8,7 @@ from datetime import timedelta
class Employee(CommonBModel):
"""
员工信息
TN:员工信息
"""
JOB_ON = 10
JOB_OFF = 20
@ -77,7 +77,7 @@ class Employee(CommonBModel):
class NotWorkRemark(CommonADModel):
"""
离岗说明
TN:离岗说明
"""
not_work_date = models.DateField('未打卡日期')
user = models.ForeignKey(User, verbose_name='用户', on_delete=models.CASCADE)
@ -86,7 +86,7 @@ class NotWorkRemark(CommonADModel):
class Attendance(CommonADModel):
"""
到岗记录
TN:到岗记录
"""
ATT_STATE_CHOICES = [
('pending', '待定'),
@ -119,7 +119,7 @@ class Attendance(CommonADModel):
class ClockRecord(BaseModel):
"""
打卡记录
TN:打卡记录
"""
ClOCK_ON = 10
CLOCK_OFF = 20
@ -157,7 +157,7 @@ class ClockRecord(BaseModel):
class Certificate(CommonAModel):
"""
证书
TN:证书
"""
CERT_OK = 10
CERT_EXPIRING = 20

0
apps/ichat/__init__.py Normal file
View File

3
apps/ichat/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/ichat/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.ichat'

View File

@ -0,0 +1,48 @@
# Generated by Django 3.2.12 on 2025-05-21 05:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Conversation',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('title', models.CharField(default='新对话', max_length=200, verbose_name='对话标题')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversation_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversation_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('content', models.TextField(verbose_name='消息内容')),
('role', models.CharField(default='user', help_text='system/user', max_length=10, verbose_name='角色')),
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='ichat.conversation', verbose_name='对话')),
],
options={
'abstract': False,
},
),
]

View File

17
apps/ichat/models.py Normal file
View File

@ -0,0 +1,17 @@
from django.db import models
from apps.system.models import CommonADModel, BaseModel
# Create your models here.
class Conversation(CommonADModel):
"""
TN: 对话
"""
title = models.CharField(max_length=200, default='新对话',verbose_name='对话标题')
class Message(BaseModel):
"""
TN: 消息
"""
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages', verbose_name='对话')
content = models.TextField(verbose_name='消息内容')
role = models.CharField("角色", max_length=10, default='user', help_text="system/user")

View File

@ -0,0 +1,14 @@
# 角色
你是一位数据分析专家和前端程序员,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述, 并形成报告。
# 技能
1. 仔细分析用户提供的JSON格式数据分析用户需求。
2. 依据得到的需求, 分别获取JSON数据中的关键信息。
3. 根据2中的关键信息最优化选择表格/饼图/柱状图/折线图等格式绘制报告。
# 回答要求
1. 仅生成完整的HTML代码所有功能都需要实现支持响应式不要输出任何解释或说明。
2. 代码中如需要Echarts等js库请直接使用中国大陆的CDN链接例如bootcdn的链接。
3. 标题为 数据分析报告。
3. 在开始部分请以表格形式简略展示获取的JSON数据。
4. 之后选择最合适的图表方式生成相应的图。
5. 在最后提供可下载该报告的完整PDF的按钮和功能。
6. 在最后提供可下载含有JSON数据的EXCEL文件的按钮和功能。

View File

@ -0,0 +1,53 @@
# 角色
你是一位资深的Postgresql数据库SQL专家具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述并生成准确可执行的SQL语句。
# 技能
1. 仔细分析用户提供的文本描述,明确用户需求。
2. 根据对用户需求的理解生成符合Postgresql数据库语法的准确可执行的SQL语句。
# 回答要求
1. 如果用户的询问未以 查询 开头,请直接回复 "请以 查询 开头,重新描述你的需求"。
2. 生成的SQL语句必须符合Postgresql数据库的语法规范。
3. 不要使用 Markerdown 和 SQL 语法格式输出,禁止添加语法标准、备注、说明等信息。
4. 直接输出符合Postgresql标准的SQL语句用txt纯文本格式展示即可。
5. 如果无法生成符合要求的SQL语句请直接回复 "无法生成"。
# 示例
1. 问:查询 外协白片抛 工段在2025年6月1日到2025年6月15日之间的生产合格数以及合格率等
select
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mgroup.name = '外协白片抛'
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-15'
2. 问:查询 黑化 工段在2025年6月的生产合格数以及合格率等
答: select
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mgroup.name = '黑化'
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-30'
3. 问:查询 各工段 在2025年6月的生产合格数以及合格率等
答: select
mgroup.name as 工段,
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-30'
group by mgroup.id
order by mgroup.sort

22
apps/ichat/script.py Normal file
View File

@ -0,0 +1,22 @@
import json
from .models import Message
from django.http import StreamingHttpResponse
def stream_generator(stream_response: bytes, conversation_id: str):
full_content = ''
for chunk in stream_response.iter_content(chunk_size=1024):
if chunk:
full_content += chunk.decode('utf-8')
try:
data = json.loads(full_content)
content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
Message.objects.create(
conversation_id=conversation_id,
content=content
)
yield f" data:{content}\n\n"
full_content = ''
except json.JSONDecodeError:
continue
return StreamingHttpResponse(stream_generator(stream_response, conversation_id), content_type='text/event-stream')

18
apps/ichat/serializers.py Normal file
View File

@ -0,0 +1,18 @@
from rest_framework import serializers
from .models import Conversation, Message
from apps.utils.constants import EXCLUDE_FIELDS
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ['id', 'conversation', 'content', 'role']
read_only_fields = EXCLUDE_FIELDS
class ConversationSerializer(serializers.ModelSerializer):
messages = MessageSerializer(many=True, read_only=True)
class Meta:
model = Conversation
fields = ['id', 'title', 'messages']
read_only_fields = EXCLUDE_FIELDS

3
apps/ichat/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
apps/ichat/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.ichat.views import QueryLLMviewSet, ConversationViewSet
from apps.ichat.views2 import WorkChain
API_BASE_URL = 'api/ichat/'
router = DefaultRouter()
router.register('conversation', ConversationViewSet, basename='conversation')
router.register('message', QueryLLMviewSet, basename='message')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
path(API_BASE_URL + 'workchain/ask/', WorkChain.as_view(), name='workchain')
]

88
apps/ichat/utils.py Normal file
View File

@ -0,0 +1,88 @@
import re
import psycopg2
import threading
from django.db import transaction
from .models import Message
# 数据库连接
def connect_db():
from server.conf import DATABASES
db_conf = DATABASES['default']
conn = psycopg2.connect(
host=db_conf['HOST'],
port=db_conf['PORT'],
user=db_conf['USER'],
password=db_conf['PASSWORD'],
database=db_conf['NAME']
)
return conn
def extract_sql_code(text):
# 优先尝试 ```sql 包裹的语句
match = re.search(r"```sql\s*(.+?)```", text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
# fallback: 寻找首个 select 语句
match = re.search(r"(SELECT\s.+?;)", text, re.IGNORECASE | re.DOTALL)
if match:
return match.group(1).strip()
return None
def get_schema_text(conn, table_names:list):
cur = conn.cursor()
query = """
SELECT
table_name, column_name, data_type
FROM
information_schema.columns
WHERE
table_schema = 'public'
and table_name in %s;
"""
cur.execute(query, (tuple(table_names), ))
schema = {}
for table_name, column_name, data_type in cur.fetchall():
if table_name not in schema:
schema[table_name] = []
schema[table_name].append(f"{column_name} ({data_type})")
cur.close()
schema_text = ""
for table_name, columns in schema.items():
schema_text += f"{table_name} 包含列:{', '.join(columns)}\n"
return schema_text
def is_safe_sql(sql:str) -> bool:
sql = sql.strip().lower()
return sql.startswith("select") or sql.startswith("show") and not re.search(r"delete|update|insert|drop|create|alter", sql)
def execute_sql(conn, sql_query):
cur = conn.cursor()
cur.execute(sql_query)
try:
rows = cur.fetchall()
columns = [desc[0] for desc in cur.description]
result = [dict(zip(columns, row)) for row in rows]
except psycopg2.ProgrammingError:
result = cur.statusmessage
cur.close()
return result
def strip_sql_markdown(content: str) -> str:
# 去掉包裹在 ```sql 或 ``` 中的内容
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
else:
return None
# ORM 写入包装函数
def save_message_thread_safe(**kwargs):
def _save():
with transaction.atomic():
Message.objects.create(**kwargs)
threading.Thread(target=_save).start()

87
apps/ichat/view_bak.py Normal file
View File

@ -0,0 +1,87 @@
import requests
from langchain_core.language_models import LLM
from langchain_core.outputs import LLMResult, Generation
from langchain_experimental.sql import SQLDatabaseChain
from langchain_community.utilities import SQLDatabase
from server.conf import DATABASES
from apps.ichat.serializers import CustomLLMrequestSerializer
from rest_framework.views import APIView
from urllib.parse import quote_plus
from rest_framework.response import Response
db_conf = DATABASES['default']
# 密码需要 URL 编码(因为有特殊字符如 @
password_encodeed = quote_plus(db_conf['PASSWORD'])
db = SQLDatabase.from_uri(f"postgresql+psycopg2://{db_conf['USER']}:{password_encodeed}@{db_conf['HOST']}/{db_conf['NAME']}", include_tables=["enm_mpoint", "enm_mpointstat"])
# model_url = "http://14.22.88.72:11025/v1/chat/completions"
model_url = "http://139.159.180.64:11434/v1/chat/completions"
class CustomLLM(LLM):
model_url: str
mode: str = 'chat'
def _call(self, prompt: str, stop: list = None) -> str:
data = {
"model":"glm4",
"messages": self.build_message(prompt),
"stream": False,
}
response = requests.post(self.model_url, json=data, timeout=600)
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"]
print('content---', content)
clean_sql = self.strip_sql_markdown(content) if self.mode == 'sql' else content.strip()
return clean_sql
def _generate(self, prompts: list, stop: list = None) -> LLMResult:
generations = []
for prompt in prompts:
text = self._call(prompt, stop)
generations.append([Generation(text=text)])
return LLMResult(generations=generations)
def strip_sql_markdown(self, content: str) -> str:
import re
# 去掉包裹在 ```sql 或 ``` 中的内容
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
else:
return content.strip()
def build_message(self, prompt: str) -> list:
if self.mode == 'sql':
system_prompt = (
"你是一个 SQL 助手,严格遵循以下规则:\n"
"1. 只返回 PostgreSQL 语法 SQL 语句。\n"
"2. 严格禁止添加任何解释、注释、Markdown 代码块标记(包括 ```sql 和 ```)。\n"
"3. 输出必须是纯 SQL且可直接执行无需任何额外处理。\n"
"4. 在 SQL 中如有多个表,请始终使用表名前缀引用字段,避免字段歧义。"
)
else:
system_prompt = "你是一个聊天助手,请根据用户的问题,提供简洁明了的答案。"
return [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
]
@property
def _llm_type(self) -> str:
return "custom_llm"
class QueryLLMview(APIView):
def post(self, request):
serializer = CustomLLMrequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
prompt = serializer.validated_data['prompt']
mode = serializer.validated_data.get('mode', 'chat')
llm = CustomLLM(model_url=model_url, mode=mode)
print('prompt---', prompt, mode)
if mode == 'sql':
chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)
result = chain.invoke(prompt)
else:
result = llm._call(prompt)
return Response({"result": result})

155
apps/ichat/views.py Normal file
View File

@ -0,0 +1,155 @@
import requests
import json
from rest_framework.views import APIView
from apps.ichat.serializers import MessageSerializer, ConversationSerializer
from rest_framework.response import Response
from apps.ichat.models import Conversation, Message
from apps.ichat.utils import connect_db, extract_sql_code, execute_sql, get_schema_text, is_safe_sql, save_message_thread_safe
from django.http import StreamingHttpResponse, JsonResponse
from rest_framework.decorators import action
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
# API_KEY = "sk-5644e2d6077b46b9a04a8a2b12d6b693"
# API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
# MODEL = "qwen-plus"
# #本地部署的模式
API_KEY = "JJVAide0hw3eaugGmxecyYYFw45FX2LfhnYJtC+W2rw"
API_BASE = "http://106.0.4.200:9000/v1"
MODEL = "qwen14b"
# google gemini
# API_KEY = "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"
# API_BASE = "https://openrouter.ai/api/v1"
# MODEL="google/gemini-2.0-flash-exp:free"
# deepseek v3
# API_KEY = "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"
# API_BASE = "https://openrouter.ai/api/v1"
# MODEL="deepseek/deepseek-chat-v3-0324:free"
TABLES = ["enm_mpoint", "enm_mpointstat", "enm_mplogx"] # 如果整个数据库全都给模型,准确率下降,所以只给模型部分表
class QueryLLMviewSet(CustomModelViewSet):
queryset = Message.objects.all()
serializer_class = MessageSerializer
ordering = ['create_time']
perms_map = {'get':'*', 'post':'*', 'put':'*'}
@action(methods=['post'], detail=False, perms_map={'post':'*'} ,serializer_class=MessageSerializer)
def completion(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
prompt = serializer.validated_data['content']
conversation = serializer.validated_data['conversation']
if not prompt or not conversation:
return JsonResponse({"error": "缺少 prompt 或 conversation"}, status=400)
save_message_thread_safe(content=prompt, conversation=conversation, role="user")
url = f"{API_BASE}/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
}
user_prompt = f"""
我提问的问题是:{prompt}请判断我的问题是否与数据库查询或操作相关如果是回答"database"如果不是回答"general"
注意
只需回答"database""general"即可不要有其他内容
"""
_payload = {
"model": MODEL,
"messages": [{"role": "user", "content": user_prompt}, {"role":"system" , "content": "只返回一个结果'database''general'"}],
"temperature": 0,
"max_tokens": 10
}
try:
class_response = requests.post(url, headers=headers, json=_payload)
class_response.raise_for_status()
class_result = class_response.json()
question_type = class_result.get('choices', [{}])[0].get('message', {}).get('content', '').strip().lower()
print("question_type", question_type)
if question_type == "database":
conn = connect_db()
schema_text = get_schema_text(conn, TABLES)
print("schema_text----------------------", schema_text)
user_prompt = f"""你是一个专业的数据库工程师,根据以下数据库结构:
{schema_text}
请根据我的需求生成一条标准的PostgreSQL SQL语句直接返回SQL不要额外解释
需求是{prompt}
"""
else:
user_prompt = f"""
回答以下问题不需要涉及数据库查询
问题: {prompt}
请直接回答问题不要提及数据库或SQL
"""
# TODO 是否应该拿到conservastion的id然后根据id去数据库查询所以的messages, 然后赋值给messages
history = Message.objects.filter(conversation=conversation).order_by('create_time')
# chat_history = [{"role": msg.role, "content": msg.content} for msg in history]
# chat_history.append({"role": "user", "content": prompt})
chat_history = [{"role":"user", "content":prompt}]
print("chat_history", chat_history)
payload = {
"model": MODEL,
"messages": chat_history,
"temperature": 0,
"stream": True
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.RequestException as e:
return JsonResponse({"error":f"LLM API调用失败: {e}"}, status=500)
def stream_generator():
accumulated_content = ""
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data:'):
if decoded_line.strip() == "data: [DONE]":
break # OpenAI-style标志结束
try:
data = json.loads(decoded_line[6:])
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
accumulated_content += content
yield f"data: {content}\n\n"
except Exception as e:
yield f"data: [解析失败]: {str(e)}\n\n"
print("accumulated_content", accumulated_content)
save_message_thread_safe(content=accumulated_content, conversation=conversation, role="system")
if question_type == "database":
sql = extract_sql_code(accumulated_content)
if sql:
try:
conn = connect_db()
if is_safe_sql(sql):
result = execute_sql(conn, sql)
save_message_thread_safe(content=f"SQL结果: {result}", conversation=conversation, role="system")
yield f"data: SQL执行结果: {result}\n\n"
else:
yield f"data: 拒绝执行非查询类 SQL{sql}\n\n"
except Exception as e:
yield f"data: SQL执行失败: {str(e)}\n\n"
finally:
if conn:
conn.close()
else:
yield "data: \\n[文本结束]\n\n"
return StreamingHttpResponse(stream_generator(), content_type='text/event-stream')
# 先新建对话 生成对话session_id
class ConversationViewSet(CustomModelViewSet):
queryset = Conversation.objects.all()
serializer_class = ConversationSerializer
ordering = ['create_time']
perms_map = {'get':'*', 'post':'*', 'put':'*'}

129
apps/ichat/views2.py Normal file
View File

@ -0,0 +1,129 @@
import requests
import os
from apps.utils.sql import execute_raw_sql
import json
from apps.utils.tools import MyJSONEncoder
from .utils import is_safe_sql
from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from django.conf import settings
from apps.utils.mixins import MyLoggingMixin
from django.core.cache import cache
import uuid
from apps.utils.thread import MyThread
LLM_URL = getattr(settings, "LLM_URL", "")
API_KEY = getattr(settings, "LLM_API_KEY", "")
MODEL = "qwen14b"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
def load_promot(name):
with open(os.path.join(CUR_DIR, f'promot/{name}.md'), 'r') as f:
return f.read()
def ask(input:str, p_name:str, stream=False):
his = [{"role":"system", "content": load_promot(p_name)}]
his.append({"role":"user", "content": input})
payload = {
"model": MODEL,
"messages": his,
"temperature": 0,
"stream": stream
}
response = requests.post(LLM_URL, headers=HEADERS, json=payload, stream=stream)
if not stream:
return response.json()["choices"][0]["message"]["content"]
else:
# 处理流式响应
full_content = ""
for chunk in response.iter_lines():
if chunk:
# 通常流式响应是SSE格式data: {...}
decoded_chunk = chunk.decode('utf-8')
if decoded_chunk.startswith("data:"):
json_str = decoded_chunk[5:].strip()
if json_str == "[DONE]":
break
try:
chunk_data = json.loads(json_str)
if "choices" in chunk_data and chunk_data["choices"]:
delta = chunk_data["choices"][0].get("delta", {})
if "content" in delta:
print(delta["content"])
full_content += delta["content"]
except json.JSONDecodeError:
continue
return full_content
def work_chain(input:str, t_key:str):
pdict = {"state": "progress", "steps": [{"state":"ok", "msg":"正在生成查询语句"}]}
cache.set(t_key, pdict)
res_text = ask(input, 'w_sql')
if res_text == '请以 查询 开头,重新描述你的需求':
pdict["state"] = "error"
pdict["steps"].append({"state":"error", "msg":res_text})
cache.set(t_key, pdict)
return
else:
pdict["steps"].append({"state":"ok", "msg":"查询语句生成成功", "content":res_text})
cache.set(t_key, pdict)
if not is_safe_sql(res_text):
pdict["state"] = "error"
pdict["steps"].append({"state":"error", "msg":"当前查询存在风险,请重新描述你的需求"})
cache.set(t_key, pdict)
return
pdict["steps"].append({"state":"ok", "msg":"正在执行查询语句"})
cache.set(t_key, pdict)
res = execute_raw_sql(res_text)
pdict["steps"].append({"state":"ok", "msg":"查询语句执行成功", "content":res})
cache.set(t_key, pdict)
pdict["steps"].append({"state":"ok", "msg":"正在生成报告"})
cache.set(t_key, pdict)
res2 = ask(json.dumps(res, cls=MyJSONEncoder, ensure_ascii=False), 'w_ana')
content = res2.lstrip('```html ').rstrip('```')
pdict["state"] = "done"
pdict["content"] = content
pdict["steps"].append({"state":"ok", "msg":"报告生成成功", "content": content})
cache.set(t_key, pdict)
return
class InputSerializer(serializers.Serializer):
input = serializers.CharField(label="查询需求")
class WorkChain(MyLoggingMixin, APIView):
@swagger_auto_schema(
operation_summary="提交查询需求",
request_body=InputSerializer)
def post(self, request):
llm_enabled = getattr(settings, "LLM_ENABLED", False)
if not llm_enabled:
raise ParseError('LLM功能未启用')
input = request.data.get('input')
t_key = f'ichat_{uuid.uuid4()}'
MyThread(target=work_chain, args=(input, t_key)).start()
return Response({'ichat_tid': t_key})
@swagger_auto_schema(
operation_summary="获取查询进度")
def get(self, request):
llm_enabled = getattr(settings, "LLM_ENABLED", False)
if not llm_enabled:
raise ParseError('LLM功能未启用')
ichat_tid = request.GET.get('ichat_tid')
if ichat_tid:
return Response(cache.get(ichat_tid))
if __name__ == "__main__":
print(work_chain("查询 一次超洗 工段在2025年6月的生产合格数等并形成报告"))
from apps.ichat.views2 import work_chain
print(work_chain('查询外观检验工段在2025年6月的生产合格数等并形成报告'))

View File

@ -39,8 +39,7 @@ def correct_mb_count_notok():
count_notok = mi.count_n_zw + mi.count_n_tw + mi.count_n_qp + mi.count_n_wq + mi.count_n_dl + mi.count_n_pb + mi.count_n_dxt + mi.count_n_js + mi.count_n_qx + mi.count_n_zz + mi.count_n_ysq + mi.count_n_hs + mi.count_n_b + mi.count_n_qt
# 先处理库存
try:
with transaction.atomic():
MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok)
InmService.update_mb_after_test(mi)
MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok)
InmService.update_mb_after_test(mi)
except ParseError as e:
MIOItem.objects.filter(id=mi.id).update(test_date=None)

View File

@ -13,7 +13,8 @@ class MaterialBatchFilter(filters.FilterSet):
"material__process": ["exact", "in"],
"count": ["exact", "gte", "lte"],
"state": ["exact", "in"],
"defect": ["exact"]
"defect": ["exact"],
"batch": ["exact"]
}
@ -34,8 +35,11 @@ class MioFilter(filters.FilterSet):
"order": ["exact"],
"item_mio__test_date": ["isnull"],
"item_mio__test_user": ["isnull"],
"item_mio__w_mioitem__number": ["exact"],
"mgroup": ["exact"],
"item_mio__batch": ["exact"]
"item_mio__batch": ["exact"],
"inout_date": ["gte", "lte", "exact"],
"belong_dept": ["exact"]
}
def filter_materials__type(self, queryset, name, value):

View File

@ -0,0 +1,33 @@
# Generated by Django 3.2.12 on 2025-05-23 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0029_alter_mioitem_batch'),
]
operations = [
migrations.AlterField(
model_name='materialbatch',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
migrations.AlterField(
model_name='materialbatcha',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
migrations.AlterField(
model_name='mioitem',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
migrations.AlterField(
model_name='mioitema',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-06-19 02:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0030_auto_20250523_0922'),
]
operations = [
migrations.AddField(
model_name='mioitem',
name='unit_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=14, null=True, verbose_name='单价'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2025-07-23 08:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0031_mioitem_unit_price'),
]
operations = [
migrations.AddField(
model_name='mioitem',
name='note',
field=models.TextField(blank=True, null=True, verbose_name='备注'),
),
migrations.AlterField(
model_name='mio',
name='type',
field=models.CharField(choices=[('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('other_in', '其他入库'), ('other_out', '其他出库')], default='do_out', help_text="(('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('other_in', '其他入库'), ('other_out', '其他出库'))", max_length=10, verbose_name='出入库类型'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-07-28 05:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0032_auto_20250723_1639'),
]
operations = [
migrations.AlterField(
model_name='mio',
name='type',
field=models.CharField(choices=[('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('borrow_out', '领用出库'), ('return_in', '退还入库'), ('other_in', '其他入库'), ('other_out', '其他出库')], default='do_out', help_text="(('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('borrow_out', '领用出库'), ('return_in', '退还入库'), ('other_in', '其他入库'), ('other_out', '其他出库'))", max_length=10, verbose_name='出入库类型'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-07-28 08:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0033_alter_mio_type'),
]
operations = [
migrations.AddField(
model_name='mioitemw',
name='number_out',
field=models.TextField(blank=True, null=True, verbose_name='对外编号'),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 3.2.12 on 2025-07-31 06:04
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('inm', '0034_mioitemw_number_out'),
]
operations = [
migrations.CreateModel(
name='Pack',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('index', models.PositiveSmallIntegerField(default=1, verbose_name='序号')),
('mio', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pack_mio', to='inm.mio', verbose_name='关联出入库记录')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='mioitem',
name='pack',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mioitem_pack', to='inm.pack', verbose_name='关联装箱单'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-08-01 06:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0035_auto_20250731_1404'),
]
operations = [
migrations.AddField(
model_name='mioitem',
name='pack_index',
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='装箱序号'),
),
]

View File

@ -5,13 +5,13 @@ from apps.sam.models import Customer, Order
from apps.mtm.models import Material, Mgroup
from apps.system.models import User
from datetime import datetime
from django.db.models import Max
from django.db.models import Max, Sum
# Create your models here.
class WareHouse(CommonBModel):
"""
仓库信息
TN:仓库信息
"""
number = models.CharField('仓库编号', max_length=20)
name = models.CharField('仓库名称', max_length=20)
@ -20,9 +20,9 @@ class WareHouse(CommonBModel):
class MaterialBatch(BaseModel):
"""
物料批次
TN:物料批次
"""
batch = models.TextField('批次号')
batch = models.TextField('批次号', db_index=True)
state = models.PositiveSmallIntegerField('状态', default=10, choices=((10, '合格'), (20, '不合格'), (30, '返修'), (40, '检验'), (50, '报废')))
material = models.ForeignKey(
Material, on_delete=models.CASCADE, verbose_name='物料')
@ -39,11 +39,15 @@ class MaterialBatch(BaseModel):
defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True)
@property
def count_mioing(self):
return MIOItem.objects.filter(mb=self, mio__submit_time__isnull=True).aggregate(count=Sum('count'))['count'] or 0
class MaterialBatchA(BaseModel):
"""
组合件物料批次
TN:组合件物料批次
"""
batch = models.CharField('批次号', max_length=100)
batch = models.TextField('批次号', db_index=True)
material = models.ForeignKey(
Material, on_delete=models.CASCADE, verbose_name='物料')
rate = models.PositiveIntegerField('比例', default=1)
@ -52,30 +56,39 @@ class MaterialBatchA(BaseModel):
MIO_TYPE_PREFIX = {
'do_out': 'SCLL', # 生产领料 (Shēngchǎn Lǐngliào)
'sale_out': 'XSFH', # 销售发货 (Xiāoshòu Fāhuò)
'pur_in': 'CGRK', # 采购入库 (Cǎigòu Rùkù)
'do_in': 'SCRK', # 生产入库 (Shēngchǎn Rùkù)
'other_in': 'QTRK', # 其他入库 (Qítā Rùkù)
'other_out': 'QTCK' # 其他出库 (Qítā Chūkù)
'do_in': 'SCRK', # 生产入库
'do_out': 'SCLL', # 生产领料
'sale_out': 'XSFH', # 销售发货
'pur_in': 'CGRK', # 采购入库
'pur_out': 'CGTH', # 采购退货
'borrow_out': 'LYCK', # 领用出库
'return_in': 'THRK', # 退还入库
'other_in': 'QTRK', # 其他入库
'other_out': 'QTCK' # 其他出库
}
class MIO(CommonBDModel):
"""
出入库记录
TN:出入库记录
"""
MIO_TYPE_DO_OUT = 'do_out'
MIO_TYPE_SALE_OUT = 'sale_out'
MIO_TYPE_PUR_IN = 'pur_in'
MIO_TYPE_PUR_OUT = 'pur_out'
MIO_TYPE_DO_IN = 'do_in'
MIO_TYPE_OTHER_IN = 'other_in'
MIO_TYPE_OTHER_OUT = 'other_out'
MIO_TYPE_BORROW_OUT = 'borrow_out'
MIO_TYPE_RETURN_IN = 'return_in'
MIO_TYPES = (
(MIO_TYPE_DO_OUT, '生产领料'),
(MIO_TYPE_SALE_OUT, '销售发货'),
(MIO_TYPE_PUR_IN, '采购入库'),
(MIO_TYPE_PUR_OUT, '采购退货'),
(MIO_TYPE_DO_IN, '生产入库'),
(MIO_TYPE_BORROW_OUT, '领用出库'),
(MIO_TYPE_RETURN_IN, '退还入库'),
(MIO_TYPE_OTHER_IN, '其他入库'),
(MIO_TYPE_OTHER_OUT, '其他出库')
)
@ -124,9 +137,16 @@ class MIO(CommonBDModel):
last_number = 1
return f"{prefix}-{today_str}-{last_number:04d}"
class Pack(BaseModel):
"""
TN:装箱单
"""
index = models.PositiveSmallIntegerField('序号', default=1)
mio = models.ForeignKey(MIO, verbose_name='关联出入库记录', on_delete=models.CASCADE, related_name='pack_mio')
class MIOItem(BaseModel):
"""
出入库明细
TN:出入库明细
"""
mio = models.ForeignKey(MIO, verbose_name='关联出入库',
on_delete=models.CASCADE, related_name='item_mio')
@ -138,7 +158,8 @@ class MIOItem(BaseModel):
WareHouse, on_delete=models.CASCADE, verbose_name='仓库')
material = models.ForeignKey(
Material, verbose_name='物料', on_delete=models.CASCADE)
batch = models.TextField('批次号')
batch = models.TextField('批次号', db_index=True)
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
count = models.DecimalField('出入数量', max_digits=12, decimal_places=3)
count_tested = models.PositiveIntegerField('已检数', null=True, blank=True)
test_date = models.DateField('检验日期', null=True, blank=True)
@ -167,6 +188,11 @@ class MIOItem(BaseModel):
count_n_qt = models.PositiveIntegerField('其他', default=0)
is_testok = models.BooleanField('检验是否合格', null=True, blank=True)
note = models.TextField('备注', null=True, blank=True)
pack_index = models.PositiveSmallIntegerField('装箱序号', null=True, blank=True)
# 以下字段暂时不用
pack = models.ForeignKey(Pack, verbose_name='关联装箱单', on_delete=models.SET_NULL, related_name='mioitem_pack', null=True, blank=True)
@classmethod
def count_fields(cls):
@ -182,11 +208,11 @@ class MIOItem(BaseModel):
class MIOItemA(BaseModel):
"""
组合件出入库明细
TN:组合件出入库明细
"""
material = models.ForeignKey(
Material, verbose_name='物料', on_delete=models.CASCADE)
batch = models.CharField('批次号', max_length=50)
batch = models.TextField('批次号', db_index=True)
rate = models.PositiveIntegerField('比例', default=1)
mioitem = models.ForeignKey(
MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='a_mioitem')
@ -198,13 +224,14 @@ class MIOItemA(BaseModel):
class MIOItemw(BaseModel):
"""
单件记录
TN:单件记录
"""
number = models.TextField('编号')
number_out = models.TextField('对外编号', null=True, blank=True)
wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw'
, null=True, blank=True)
mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem')
ftest = models.ForeignKey("qm.ftest", verbose_name='关联检验记录', on_delete=models.PROTECT,
related_name='mioitemw_ftest', null=True, blank=True)
note = models.TextField('备注', null=True, blank=True)

View File

@ -8,10 +8,11 @@ from apps.system.models import Dept, User
from apps.utils.constants import EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS
from apps.utils.serializers import CustomModelSerializer
from apps.mtm.models import Material
from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw
from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw, Pack
from django.db import transaction
from server.settings import get_sysconfig
from apps.wpmw.models import Wpr
from decimal import Decimal
class WareHourseSerializer(CustomModelSerializer):
@ -29,6 +30,15 @@ class MaterialBatchAListSerializer(CustomModelSerializer):
fields = ['material', 'batch', 'rate', 'mb', 'id', 'material_']
class MaterialBatchAListSerializer2(CustomModelSerializer):
material_name = serializers.StringRelatedField(
source='material', read_only=True)
class Meta:
model = MaterialBatchA
fields = ['material', 'batch', 'rate', 'mb',
'id', 'material_name']
class MaterialBatchSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField(
source='warehouse.name', read_only=True)
@ -38,11 +48,18 @@ class MaterialBatchSerializer(CustomModelSerializer):
source='supplier', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True)
defect_name = serializers.CharField(source="defect.name", read_only=True)
count_mioing = serializers.IntegerField(read_only=True, label='正在出入库数量')
class Meta:
model = MaterialBatch
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS_BASE
def to_representation(self, instance):
ret = super().to_representation(instance)
if 'count' in ret and 'count_mioing' in ret:
ret['count_canmio'] = str(Decimal(ret['count']) - Decimal(ret['count_mioing']))
return ret
class MaterialBatchDetailSerializer(CustomModelSerializer):
@ -109,14 +126,15 @@ class MIOItemCreateSerializer(CustomModelSerializer):
class Meta:
model = MIOItem
fields = ['mio', 'warehouse', 'material',
'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm']
'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm', 'unit_price', 'note', "pack_index"]
extra_kwargs = {
'mio': {'required': True}, 'warehouse': {'required': False},
'material': {'required': False}, 'batch': {'required': False}}
'material': {'required': False}, 'batch': {'required': False, "allow_null": True, "allow_blank": True}}
def create(self, validated_data):
mio:MIO = validated_data['mio']
mio_type = mio.type
mb = validated_data.get('mb', None)
wm = validated_data.get('wm', None)
assemb = validated_data.pop('assemb', [])
@ -135,9 +153,14 @@ class MIOItemCreateSerializer(CustomModelSerializer):
validated_data["batch"] = wm.batch
material: Material = validated_data['material']
batch = validated_data['batch']
batch = validated_data.get("batch", None)
if not batch:
batch = ""
if material.is_hidden:
raise ParseError('隐式物料不可出入库')
if mio.type in [MIO.MIO_TYPE_RETURN_IN, MIO.MIO_TYPE_BORROW_OUT]:
if not material.into_wm:
raise ParseError('该物料不可领用或归还')
if mio.state != MIO.MIO_CREATE:
raise ParseError('出入库记录非创建中不可新增')
@ -148,50 +171,59 @@ class MIOItemCreateSerializer(CustomModelSerializer):
mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN])
if mis.exists() and (not mis.exclude(test_date=None).exists()):
raise ParseError('该批次的物料未经检验')
with transaction.atomic():
count = validated_data["count"]
batch = validated_data["batch"]
mioitemw = validated_data.pop('mioitemw', [])
instance = super().create(validated_data)
assemb_dict = {}
for i in assemb:
assemb_dict[i['material'].id] = i
if material.is_assemb and '_in' in mio.type: # 仅入库且是组合件的时候需要填写下一级
components = material.components
for k, v in components.items():
if k in assemb_dict:
mia = assemb_dict[k]
MIOItemA.objects.create(
mioitem=instance, rate=v, **mia)
count = validated_data["count"]
batch = validated_data["batch"]
mioitemw = validated_data.pop('mioitemw', [])
instance:MIOItem = super().create(validated_data)
assemb_dict = {}
for i in assemb:
assemb_dict[i['material'].id] = i
if material.is_assemb and '_in' in mio.type: # 仅入库且是组合件的时候需要填写下一级
components = material.components
for k, v in components.items():
if k in assemb_dict:
mia = assemb_dict[k]
MIOItemA.objects.create(
mioitem=instance, rate=v, **mia)
else:
raise ParseError('缺少组合件')
if material.tracking == Material.MA_TRACKING_SINGLE:
if len(mioitemw) == 0:
if mb:
wpr_qs = Wpr.get_qs_by_mb(mb)
if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else:
raise ParseError('缺少组合件')
if material.tracking == Material.MA_TRACKING_SINGLE:
if len(mioitemw) == 0:
if mb:
wpr_qs = Wpr.get_qs_by_mb(mb)
if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else:
raise ParseError('请提供产品明细编号')
elif wm:
wpr_qs = Wpr.get_qs_by_wm(wm)
if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else:
raise ParseError('请提供产品明细编号')
elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1:
MIOItemw.objects.create(mioitem=instance, number=batch)
raise ParseError('请提供产品明细编号')
elif wm:
wpr_qs = Wpr.get_qs_by_wm(wm)
if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else:
raise ParseError('不支持自动生成请提供产品明细')
elif len(mioitemw) >= 1:
mio_type = mio.type
for item in mioitemw:
if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in":
raise ParseError(f'{item["number"]}_请提供产品明细ID')
else:
MIOItemw.objects.create(mioitem=instance, **item)
raise ParseError('请提供产品明细编号')
elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1:
MIOItemw.objects.create(mioitem=instance, number=batch)
else:
raise ParseError('不支持自动生成请提供产品明细')
elif len(mioitemw) >= 1:
mio_type = mio.type
if mio_type != "pur_in" and mio_type != "other_in":
wprIds = [i["wpr"].id for i in mioitemw]
mb_ids = list(Wpr.objects.filter(id__in=wprIds).values_list("mb__id", flat=True).distinct())
if len(mb_ids) == 1 and mb_ids[0] == instance.mb.id:
pass
else:
raise ParseError(f'{batch}物料明细中存在{len(mb_ids)}个不同物料批次')
for item in mioitemw:
if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in":
raise ParseError(f'{item["number"]}_请提供产品明细ID')
elif item.get("number_out", None) is not None and mio_type != MIO.MIO_TYPE_SALE_OUT:
raise ParseError(f'{item["number"]}_非销售出库不可赋予产品对外编号')
else:
MIOItemw.objects.create(mioitem=instance, **item)
return instance
@ -202,16 +234,14 @@ class MIOItemAListSerializer(CustomModelSerializer):
class Meta:
model = MIOItemA
fields = ['material', 'batch', 'rate', 'mioitem',
'id', 'material_', 'material_name']
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE
class MIOItemSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField(
source='warehouse.name', read_only=True)
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True)
assemb = MIOItemAListSerializer(
source='a_mioitem', read_only=True, many=True)
assemb = serializers.SerializerMethodField(label="组合件信息")
material_name = serializers.StringRelatedField(
source='material', read_only=True)
inout_date = serializers.DateField(source='mio.inout_date', read_only=True)
@ -222,6 +252,24 @@ class MIOItemSerializer(CustomModelSerializer):
model = MIOItem
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["price"] = None
if ret["unit_price"] is not None:
ret["price"] = Decimal(ret["count"]) * Decimal(ret["unit_price"])
return ret
def get_assemb(self, obj):
qs = MIOItemA.objects.filter(mioitem=obj)
if qs.exists():
return MIOItemAListSerializer(qs, many=True).data
elif obj.mb and obj.mb.material.is_assemb:
return MaterialBatchAListSerializer2(MaterialBatchA.objects.filter(mb=obj.mb), many=True).data
return None
class MioItemDetailSerializer(MIOItemSerializer):
mio_ = MIOListSerializer(source='mio', read_only=True)
class MIODoSerializer(CustomModelSerializer):
@ -235,8 +283,11 @@ class MIODoSerializer(CustomModelSerializer):
class Meta:
model = MIO
fields = ['id', 'number', 'note', 'do_user',
'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user']
extra_kwargs = {'inout_date': {'required': True}, 'do_user': {'required': True}, 'number': {"required": False, "allow_blank": True}}
'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user', 'type']
extra_kwargs = {'inout_date': {'required': True},
'do_user': {'required': True},
'number': {"required": False, "allow_blank": True},
'type': {'required': True}}
def validate(self, attrs):
if 'mgroup' in attrs and attrs['mgroup']:
@ -246,10 +297,13 @@ class MIODoSerializer(CustomModelSerializer):
return attrs
def create(self, validated_data):
type = validated_data['type']
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]:
pass
else:
raise ValidationError('出入库类型错误')
if not validated_data.get("number", None):
validated_data["number"] = MIO.get_a_number(validated_data["type"])
if validated_data['type'] not in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_DO_IN]:
raise ValidationError('出入库类型错误')
return super().create(validated_data)
def update(self, instance, validated_data):
@ -294,11 +348,17 @@ class MIOPurSerializer(CustomModelSerializer):
class Meta:
model = MIO
fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user']
extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}}
fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user', 'type']
extra_kwargs = {'inout_date': {'required': True},
'number': {"required": False, "allow_blank": True},
'type': {'required': True}}
def create(self, validated_data):
validated_data['type'] = MIO.MIO_TYPE_PUR_IN
type = validated_data["type"]
if type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]:
pass
else:
raise ValidationError('出入库类型错误')
if not validated_data.get("number", None):
validated_data["number"] = MIO.get_a_number(validated_data["type"])
pu_order: PuOrder = validated_data.get('pu_order', None)
@ -383,3 +443,23 @@ class MIOItemPurInTestSerializer(CustomModelSerializer):
attrs['weight_kgs'] = [float(i) for i in weight_kgs]
attrs['count_sampling'] = len(attrs['weight_kgs'])
return super().validate(attrs)
class PackSerializer(CustomModelSerializer):
class Meta:
model = Pack
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE
def create(self, validated_data):
index = validated_data["index"]
mio = validated_data["mio"]
if Pack.objects.filter(mio=mio, index=index).exists():
raise ParseError('包装箱已存在')
return super().create(validated_data)
class PackMioSerializer(serializers.Serializer):
mioitems = serializers.ListField(child=serializers.CharField(), label="明细ID")
pack_index = serializers.IntegerField(label="包装箱序号")
# pack = serializers.CharField(label="包装箱ID")

View File

@ -5,13 +5,15 @@ from django.db import transaction
from rest_framework.exceptions import ParseError
from apps.wpmw.models import Wpr
from apps.mtm.models import Material
from rest_framework import serializers
class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
ftest = FtestProcessSerializer(required=False)
wpr_number_out = serializers.CharField(source="wpr.number_out", read_only=True)
class Meta:
model = MIOItemw
fields = ["id", "number", "wpr", "note", "mioitem", "ftest"]
fields = ["id", "number", "wpr", "note", "mioitem", "ftest", "wpr_number_out"]
def validate(self, attrs):
mioitem: MIOItem = attrs["mioitem"]
@ -43,7 +45,6 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
ftest_sr.update(instance=ftest, validated_data=ftest_data)
return mioitemw
@transaction.atomic
def create(self, validated_data):
wpr: Wpr = validated_data.get("wpr", None)
if wpr:
@ -56,7 +57,6 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
mioitemw = self.save_ftest(mioitemw, ftest_data)
return mioitemw
@transaction.atomic
def update(self, instance, validated_data):
validated_data.pop("mioitem")
ftest_data = validated_data.pop("ftest", None)

View File

@ -1,31 +1,38 @@
from apps.inm.models import (MIO, MIOItem,
MaterialBatch, MaterialBatchA,
MIOItemA, WareHouse, MIOItemw)
MIOItemA, MIOItemw)
from rest_framework.exceptions import ParseError
from apps.mtm.models import Material, Process
from apps.utils.tools import ranstr
from apps.utils.thread import MyThread
from apps.mtm.models import Material
from apps.mtm.services_2 import cal_material_count
from apps.wpm.models import WMaterial, BatchSt, BatchLog
from apps.wpm.services_2 import get_alldata_with_batch_and_store
from apps.wpm.services_2 import ana_batch_thread
from apps.wpmw.models import Wpr
from apps.qm.models import Ftest, Defect
from django.db.models import Count, Q
def do_out(item: MIOItem):
def do_out(item: MIOItem, is_reverse: bool = False):
"""
生产领料到车间
"""
if item.mb and item.mb.defect is not None:
raise ParseError("生产领料不支持不合格品")
from apps.inm.models import MaterialBatch
mio:MIO = item.mio
belong_dept = mio.belong_dept
mgroup = mio.mgroup
do_user = mio.do_user
material:Material = item.material
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
return
# 获取defect
defect:Defect = None
if item.wm and item.mb:
raise ParseError("车间和仓库库存不能同时存在")
if item.wm:
defect = item.wm.defect
elif item.mb:
defect = item.mb.defect
action_list = []
mias = MIOItemA.objects.filter(mioitem=item)
is_zhj = False # 是否组合件领料
@ -35,16 +42,18 @@ def do_out(item: MIOItem):
for i in range(len(mias_list)):
material, batch, rate = mias_list[i]
new_count = rate * item.count # 假设 item.count 存在
action_list.append([material, batch, new_count])
action_list.append([material, batch, new_count, None])
else:
action_list = [[item.material, item.batch, item.count]]
action_list = [[item.material, item.batch, item.count, defect]]
if is_zhj:
try:
mb = MaterialBatch.objects.get(
material=item.material,
warehouse=item.warehouse,
batch=item.batch
batch=item.batch,
state=WMaterial.WM_OK,
defect=None
)
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
raise ParseError(f"组合件批次错误!{e}")
@ -58,11 +67,17 @@ def do_out(item: MIOItem):
raise ParseError("组合件暂不支持追踪单件")
xbatches = []
if is_zhj:
xbatches = [item.batch]
for al in action_list:
xmaterial:Material = al[0]
xbatch:str = al[1]
xcount:str = al[2]
defect:Defect = al[3]
xbatches.append(xbatch)
if xcount <= 0:
raise ParseError("存在非正数!")
mb = None
if not is_zhj:
try:
@ -70,43 +85,48 @@ def do_out(item: MIOItem):
material=xmaterial,
warehouse=item.warehouse,
batch=xbatch,
state=10,
defect=None
state=WMaterial.WM_OK,
defect=defect
)
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
raise ParseError(f"批次错误!{e}")
mb.count = mb.count - xcount
if mb.count < 0:
raise ParseError("批次库存不足,操作失败")
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
else:
mb.save()
# 领到车间库存(或工段)
wm, new_create = WMaterial.objects.get_or_create(batch=xbatch, material=xmaterial,
belong_dept=belong_dept, mgroup=mgroup,
state=WMaterial.WM_OK)
if new_create:
wm.create_by = do_user
wm.batch_ofrom = mb.batch if mb else None
wm.material_ofrom = mb.material if mb else None
wm.count = wm.count + item.count
wm.update_by = do_user
wm.save()
if material.into_wm:
# 领到车间库存(或工段)
wm, new_create = WMaterial.objects.get_or_create(
batch=xbatch, material=xmaterial,
belong_dept=belong_dept, mgroup=mgroup,
state=WMaterial.WM_OK, defect=defect)
if new_create:
wm.create_by = do_user
wm.batch_ofrom = mb.batch if mb else None
wm.material_ofrom = mb.material if mb else None
wm.count = wm.count + item.count
wm.update_by = do_user
wm.save()
# 开始变动wpr
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
if material.into_wm is False:
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
mioitemws = MIOItemw.objects.filter(mioitem=item)
if mioitemws.count() != item.count:
raise ParseError("出入库与明细数量不一致,操作失败")
mb_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("mb__id", flat=True).distinct())
if len(mb_ids) == 1 and mb_ids[0] == mb.id:
pass
else:
raise ParseError(f'{xbatch}物料明细中存在{len(mb_ids)}个不同物料批次')
for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)
# 触发批次统计分析
xbatches = list(set(xbatches))
if xbatches:
for xbatch in xbatches:
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
ana_batch_thread(xbatches)
def do_in(item: MIOItem):
@ -114,70 +134,89 @@ def do_in(item: MIOItem):
生产入库后更新车间物料
"""
mio = item.mio
if item.wm and item.wm.defect is not None:
raise ParseError("不合格物料无法入库")
wmin:WMaterial = item.wm
if wmin and wmin.state != WMaterial.WM_OK:
raise ParseError("非合格物料无法入库")
belong_dept = mio.belong_dept
mgroup = mio.mgroup
do_user = mio.do_user
material = item.material
if material.into_wm is False: # 根据配置不进行入车间库存的处理
return
action_list = []
mias = MIOItemA.objects.filter(mioitem=item)
is_zhj = False # 是否组合件入仓库
# 获取defect
defect:Defect = None
if item.wm and item.mb:
raise ParseError("车间和仓库库存不能同时存在")
if item.wm:
defect = item.wm.defect
elif item.mb:
defect = item.mb.defect
if mias.exists():
is_zhj = True
mias_list = mias.values_list('material', 'batch', 'rate')
for i in mias_list:
material, batch, rate = i
new_count = rate * item.count # 假设 item.count 存在
action_list.append([material, batch, new_count])
action_list.append([material, batch, new_count, None])
else:
action_list = [[item.material, item.batch, item.count]]
action_list = [[item.material, item.batch, item.count, defect]]
production_dept = None
xbatchs = []
if is_zhj:
xbatchs = [item.batch]
for al in action_list:
xmaterial, xbatch, xcount = al
xbatchs.append(xbatch)
# 扣减车间库存
wm_qs = WMaterial.objects.filter(
batch=xbatch,
material=xmaterial,
belong_dept=belong_dept,
mgroup=mgroup,
state=WMaterial.WM_OK)
count_x = wm_qs.count()
if count_x == 1:
wm = wm_qs.first()
elif count_x == 0:
raise ParseError(
f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
else:
raise ParseError(
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
wm.update_by = do_user
wm.save()
else:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
xmaterial, xbatch, xcount, defect = al
if xcount <= 0:
raise ParseError("存在非正数!")
xbatchs.append(xbatch)
if material.into_wm:
wm_qs = WMaterial.objects.filter(
batch=xbatch,
material=xmaterial,
belong_dept=belong_dept,
mgroup=mgroup,
defect=defect,
state=WMaterial.WM_OK)
count_x = wm_qs.count()
if count_x == 1:
wm = wm_qs.first()
elif count_x == 0:
raise ParseError(
f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
else:
raise ParseError(
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
# 扣减车间库存
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
wm.update_by = do_user
wm.save()
else:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
if production_dept is None:
production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else None
if production_dept is None:
production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
# 增加mb
if not is_zhj:
mb, _ = MaterialBatch.objects.get_or_create(
material=xmaterial,
warehouse=item.warehouse,
batch=xbatch,
state=10,
defect=None,
state=WMaterial.WM_OK,
defect=defect,
defaults={
"count": 0,
"batch_ofrom": wm.batch_ofrom,
@ -185,14 +224,23 @@ def do_in(item: MIOItem):
"production_dept": production_dept
}
)
if mb.production_dept is None:
mb.production_dept = production_dept
mb.count = mb.count + xcount
mb.save()
# 开始变动wpr
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
if material.into_wm is False:
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
mioitemws = MIOItemw.objects.filter(mioitem=item)
if mioitemws.count() != item.count:
raise ParseError("出入库与明细数量不一致,操作失败")
wm_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("wm__id", flat=True).distinct())
if len(wm_ids) == 1 and wm_ids[0] == wm.id:
pass
else:
raise ParseError(f'{xbatch}物料明细中存在{len(wm_ids)}个不同物料批次')
for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)
@ -202,19 +250,21 @@ def do_in(item: MIOItem):
material=item.material,
warehouse=item.warehouse,
batch=item.batch,
defect=None,
state=WMaterial.WM_OK,
defaults={"count": 0, "production_dept": production_dept}
)
mb.count = mb.count + item.count
mb.save()
if not is_created:
raise ParseError("该批次组合件已存在")
if mb.production_dept is None:
mb.production_dept = production_dept
mb.count = mb.count + item.count
mb.save()
for mia in mias:
MaterialBatchA.objects.create(mb=mb, material=mia.material, batch=mia.batch, rate=mia.rate)
# 批次统计分析
xbatchs = list(set(xbatchs))
for xbatch in xbatchs:
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
ana_batch_thread(xbatchs)
class InmService:
@ -224,17 +274,17 @@ class InmService:
更新物料数量
"""
# 统计物料数量
m_ids = MIOItem.objects.filter(mio=instance).values_list('material_id', flat=True)
cal_material_count(m_ids)
m_ids = list(MIOItem.objects.filter(mio=instance).values_list('material_id', flat=True))
m_ids2 = list(MIOItemA.objects.filter(mioitem__mio=instance).values_list('material_id', flat=True))
cal_material_count(m_ids+m_ids2)
@classmethod
def update_inm(cls, instance: MIO, is_reverse: bool = False):
"""
更新库存, 支持反向操作
"""
in_or_out = 1
if is_reverse:
in_or_out = -1
if not MIOItem.objects.filter(mio=instance).exists():
raise ParseError("出入库记录缺失明细,无法操作")
if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单
# 这里还需要对入厂检验进行处理
@ -242,18 +292,33 @@ class InmService:
BatchLog.clear(mio=instance)
else:
for item in MIOItem.objects.filter(mio=instance):
BatchSt.g_create(batch=item.batch, mio=instance, material_start=item.material)
BatchSt.g_create(
batch=item.batch, mio=instance, material_start=item.material)
from apps.pum.services import PumService
cls.update_mb(instance, in_or_out)
PumService.mio_purin(instance, is_reverse)
if is_reverse:
cls.update_mb(instance, -1)
else:
cls.update_mb(instance, 1)
PumService.mio_pur(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_PUR_OUT:
from apps.pum.services import PumService
if is_reverse:
cls.update_mb(instance, 1)
else:
cls.update_mb(instance, -1)
PumService.mio_pur(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_OTHER_IN:
if is_reverse:
BatchLog.clear(mio=instance)
else:
for item in MIOItem.objects.filter(mio=instance):
BatchSt.g_create(batch=item, mio=instance, material_start=item.material)
cls.update_mb(instance, in_or_out)
elif instance.type == MIO.MIO_TYPE_DO_IN:
BatchSt.g_create(
batch=item.batch, mio=instance, material_start=item.material)
if is_reverse:
cls.update_mb(instance, -1)
else:
cls.update_mb(instance, 1)
elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
@ -261,20 +326,26 @@ class InmService:
else:
for item in mioitems:
do_in(item)
elif instance.type in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT]:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
do_in(item)
else:
for item in mioitems:
do_out(item)
elif instance.type == MIO.MIO_TYPE_SALE_OUT:
from apps.sam.services import SamService
cls.update_mb(instance, in_or_out)
if is_reverse:
cls.update_mb(instance, 1)
else:
cls.update_mb(instance, -1)
SamService.mio_saleout(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_OTHER_OUT:
cls.update_mb(instance, in_or_out)
elif instance.type == MIO.MIO_TYPE_DO_OUT:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
do_in(item)
cls.update_mb(instance, 1)
else:
for item in mioitems:
do_out(item)
cls.update_mb(instance, -1)
else:
raise ParseError('不支持该出入库操作')
@ -300,6 +371,7 @@ class InmService:
out = -1
默认使用count字段做加减
"""
mio_type = i.mio.type
material: Material = i.material
warehouse = i.warehouse
tracking = material.tracking
@ -332,11 +404,13 @@ class InmService:
defect = defects_map[defect_id]
m_list.append((material, warehouse, i.batch, xcount, defect, i))
xbatchs = []
for material, warehouse, batch, change_count, defect, mioitem in m_list:
if change_count <= 0:
continue
xbatchs.append(batch)
if change_count < 0:
raise ParseError("存在负数!")
state = WMaterial.WM_OK
if defect:
if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
state = WMaterial.WM_NOTOK
mb, _ = MaterialBatch.objects.get_or_create(
material=material,
@ -368,7 +442,7 @@ class InmService:
elif in_or_out == -1:
mb.count = mb.count - change_count
if mb.count < 0:
raise ParseError("批次库存不足,操作失败")
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
else:
mb.save()
if tracking == Material.MA_TRACKING_SINGLE:
@ -379,65 +453,34 @@ class InmService:
if mioitemws.count() != change_count:
raise ParseError("出入库与明细数量不一致,操作失败")
for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb)
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb, number_out=mioitemw.number_out)
else:
raise ParseError("不支持的操作")
# 批次统计分析
ana_batch_thread(xbatchs)
def daoru_mb(path: str):
"""
导入物料批次(如没有物料自动创建)
"""
# 注释的是初次导入时做的数据矫正
# objs1 = Material.objects.filter(specification__contains=' ')
# for i in objs1:
# i.specification = i.specification.replace(' ', '')
# i.save()
# objs2 = Material.objects.filter(specification__contains='×')
# for i in objs2:
# i.specification = i.specification.replace('×', '*')
# i.save()
# objs3 = (Material.objects.filter(
# specification__contains='优级') | Material.objects.filter(specification__contains='一级')).exclude(specification__contains='|')
# for i in objs3:
# i.specification = i.specification.replace(
# '优级', '|优级').replace('一级', '|一级')
# i.save()
type_dict = {"主要原料": 30, "半成品": 20, "成品": 10, "辅助材料": 40, "加工工具": 50, "辅助工装": 60, "办公用品": 70}
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path)
process_l = Process.objects.all()
process_d = {p.name: p for p in process_l}
warehouse_l = WareHouse.objects.all()
warehouse_d = {w.name: w for w in warehouse_l}
for sheet in wb.worksheets:
i = 3
while sheet[f"a{i}"].value:
try:
type = type_dict[sheet[f"a{i}"].value.replace(" ", "")]
name = sheet[f"b{i}"].value.replace(" ", "")
specification = sheet[f"c{i}"].value.replace(" ", "")
if sheet[f"d{i}"].value and sheet[f"d{i}"].value.replace(" ", ""):
specification = specification + "|" + sheet[f"d{i}"].value.replace(" ", "")
model = sheet[f"e{i}"].value.replace(" ", "")
process = process_d[sheet[f"f{i}"].value.replace(" ", "")]
batch = sheet[f"g{i}"].value.replace(" ", "")
count = int(sheet[f"h{i}"].value)
warehouse = warehouse_d[sheet[f"i{i}"].value.replace(" ", "")]
except KeyError as e:
raise ParseError(f"{i}行数据有误:{str(e)}")
material, _ = Material.objects.get_or_create(
type=type,
name=name,
specification=specification,
model=model,
process=process,
defaults={"type": type, "name": name, "specification": specification, "model": model, "process": process, "number": ranstr(6), "id": idWorker.get_id()},
)
MaterialBatch.objects.get_or_create(
material=material, batch=batch, warehouse=warehouse, defaults={"material": material, "batch": batch, "warehouse": warehouse, "count": count, "id": idWorker.get_id()}
)
cal_material_count([material.id])
i = i + 1
@classmethod
def revert_and_del(cls, mioitem: MIOItem):
mio = mioitem.mio
if mio.submit_time is None:
raise ParseError("未提交的出入库明细不允许撤销")
if mioitem.test_date is not None:
raise ParseError("已检验的出入库明细不允许撤销")
if mio.type == MIO.MIO_TYPE_PUR_IN:
from apps.pum.services import PumService
cls.update_mb_item(mioitem, -1)
BatchLog.clear(mioitem=mioitem)
PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem)
mioitem.delete()
elif mio.type == MIO.MIO_TYPE_OTHER_IN:
cls.update_mb_item(mioitem, -1)
BatchLog.clear(mioitem=mioitem)
mioitem.delete()
elif mio.type == MIO.MIO_TYPE_DO_OUT:
do_in(mioitem)
BatchLog.clear(mioitem=mioitem)
mioitem.delete()
else:
raise ParseError("不支持该出入库单明细撤销")

161
apps/inm/services_daoru.py Normal file
View File

@ -0,0 +1,161 @@
from rest_framework.exceptions import ParseError
from apps.mtm.models import Process, Material
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw, MIO
from apps.utils.tools import ranstr
from apps.mtm.services_2 import cal_material_count
def daoru_mb(path: str):
"""
导入物料批次(如没有物料自动创建)
"""
# 注释的是初次导入时做的数据矫正
# objs1 = Material.objects.filter(specification__contains=' ')
# for i in objs1:
# i.specification = i.specification.replace(' ', '')
# i.save()
# objs2 = Material.objects.filter(specification__contains='×')
# for i in objs2:
# i.specification = i.specification.replace('×', '*')
# i.save()
# objs3 = (Material.objects.filter(
# specification__contains='优级') | Material.objects.filter(specification__contains='一级')).exclude(specification__contains='|')
# for i in objs3:
# i.specification = i.specification.replace(
# '优级', '|优级').replace('一级', '|一级')
# i.save()
type_dict = {"主要原料": 30, "半成品": 20, "成品": 10, "辅助材料": 40, "加工工具": 50, "辅助工装": 60, "办公用品": 70}
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path)
process_l = Process.objects.all()
process_d = {p.name: p for p in process_l}
warehouse_l = WareHouse.objects.all()
warehouse_d = {w.name: w for w in warehouse_l}
for sheet in wb.worksheets:
i = 3
while sheet[f"a{i}"].value:
try:
type = type_dict[sheet[f"a{i}"].value.replace(" ", "")]
name = sheet[f"b{i}"].value.replace(" ", "")
specification = sheet[f"c{i}"].value.replace(" ", "")
if sheet[f"d{i}"].value and sheet[f"d{i}"].value.replace(" ", ""):
specification = specification + "|" + sheet[f"d{i}"].value.replace(" ", "")
model = sheet[f"e{i}"].value.replace(" ", "")
process = process_d[sheet[f"f{i}"].value.replace(" ", "")]
batch = sheet[f"g{i}"].value.replace(" ", "")
count = int(sheet[f"h{i}"].value)
warehouse = warehouse_d[sheet[f"i{i}"].value.replace(" ", "")]
except KeyError as e:
raise ParseError(f"{i}行数据有误:{str(e)}")
material, _ = Material.objects.get_or_create(
type=type,
name=name,
specification=specification,
model=model,
process=process,
defaults={"type": type, "name": name, "specification": specification, "model": model, "process": process, "number": ranstr(6), "id": idWorker.get_id()},
)
MaterialBatch.objects.get_or_create(
material=material, batch=batch, warehouse=warehouse, defaults={"material": material, "batch": batch, "warehouse": warehouse, "count": count, "id": idWorker.get_id()}
)
cal_material_count([material.id])
i = i + 1
def daoru_mioitem_test(path:str, mioitem:MIOItem):
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
qct = Qct.get(mioitem.material, tag="inm", type="in")
if qct is None:
raise ParseError("未找到检验表")
t_name_list = ["配套序号", "棒编号", "棒最大外径/mm", "锥度/mm", "管编号", "管最大内径/mm", "配合间隙"]
t_list = []
for name in t_name_list:
try:
t_list.append(TestItem.objects.get(name=name))
except TestItem.DoesNotExist:
raise ParseError(f"未找到检验项:{name}")
except TestItem.MultipleObjectsReturned:
raise ParseError(f"检验项重复:{name}")
test_user = mioitem.mio.mio_user
test_date = mioitem.mio.inout_date
wb = load_workbook(path, data_only=True)
if "Sheet1" in wb.sheetnames: # 检查是否存在
sheet = wb["Sheet1"] # 获取工作表
else:
raise ParseError("未找到Sheet1")
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
for ind, item in enumerate(mioitemws):
ftest:Ftest = item.ftest
if ftest is None:
ftest = Ftest.objects.create(
type="purin",
test_numer=item.number,
qct=qct,
test_user=test_user,
is_ok=True,
test_date=test_date)
item.ftest = ftest
item.save()
else:
FtestItem.objects.filter(ftest=ftest).delete()
FtestDefect.objects.filter(ftest=ftest).delete()
ftest.is_ok = True
ftest.defect_main = None
ftest.save()
i = ind + 4
if sheet[f"c{i}"].value:
ftestitems = []
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[0], test_val_json=sheet[f"b{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[1], test_val_json=sheet[f"c{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[2], test_val_json=sheet[f"e{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[3], test_val_json=sheet[f"f{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[4], test_val_json=sheet[f"g{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[5], test_val_json=sheet[f"j{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id()))
FtestItem.objects.bulk_create(ftestitems)
else:
break
def daoru_mioitems(path:str, mio:MIO):
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path, data_only=True)
if "Sheet1" in wb.sheetnames: # 检查是否存在
sheet = wb["Sheet1"] # 获取工作表
else:
raise ParseError("未找到Sheet1")
mioitems = []
ind = 2
while sheet[f"a{ind}"].value:
batch = sheet[f"b{ind}"].value
material_number = sheet[f"a{ind}"].value
try:
material = Material.objects.get(number=material_number)
except Exception as e:
raise ParseError(f"未找到物料:{material_number} {e}")
if batch:
pass
else:
batch = ""
count = sheet[f"c{ind}"].value
warehouse_name = sheet[f"d{ind}"].value
try:
warehouse = WareHouse.objects.get(name=warehouse_name)
except Exception as e:
raise ParseError(f"未找到仓库:{warehouse_name} {e}")
mioitems.append(MIOItem(mio=mio, warehouse=warehouse, material=material, batch=batch, count=count, id=idWorker.get_id()))
ind = ind + 1
MIOItem.objects.bulk_create(mioitems)

View File

@ -19,6 +19,7 @@ router.register('mio/pur', MioPurViewSet)
router.register('mio/other', MioOtherViewSet)
router.register('mioitem', MIOItemViewSet, basename='mioitem')
router.register('mioitemw', MIOItemwViewSet, basename='mioitemw')
# router.register('pack', PackViewSet, basename='pack')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -9,21 +9,25 @@ from django.utils import timezone
from rest_framework.response import Response
from django.db.models import Sum
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack
from apps.inm.serializers import (
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
MIOItemwSerializer)
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer)
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.inm.services import InmService, daoru_mb
from apps.inm.services import InmService
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test, daoru_mioitems
from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin,
CustomListModelMixin)
from apps.utils.permission import has_perm
from .filters import MaterialBatchFilter, MioFilter
from apps.qm.serializers import FtestProcessSerializer
from apps.mtm.models import Material
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from django.db import connection
# Create your views here.
@ -144,9 +148,19 @@ class MIOViewSet(CustomModelViewSet):
serializer_class = MIOListSerializer
retrieve_serializer_class = MIODetailSerializer
filterset_class = MioFilter
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model']
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model',
'item_mio__a_mioitem__batch']
data_filter = True
@classmethod
def lock_and_check_can_update(cls, mio:MIO):
if not connection.in_atomic_block:
raise ParseError("请在事务中调用该方法")
mio:MIO = MIO.objects.select_for_update().get(id=mio.id)
if mio.submit_time is not None:
raise ParseError("该记录已提交无法更改")
return mio
def add_info_for_list(self, data):
# 获取检验状态
mio_dict = {}
@ -167,7 +181,7 @@ class MIOViewSet(CustomModelViewSet):
if self.action in ['create', 'update', 'partial_update']:
type = self.request.data.get('type')
user = self.request.user
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT]:
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]:
if not has_perm(user, ['mio.do']):
raise PermissionDenied
return MIODoSerializer
@ -179,7 +193,7 @@ class MIOViewSet(CustomModelViewSet):
if not has_perm(user, ['mio.sale']):
raise PermissionDenied
return MIOSaleSerializer
elif type == MIO.MIO_TYPE_PUR_IN:
elif type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]:
if not has_perm(user, ['mio.pur']):
raise PermissionDenied
return MIOPurSerializer
@ -191,27 +205,29 @@ class MIOViewSet(CustomModelViewSet):
return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic
def submit(self, request, *args, **kwargs):
"""提交
提交
"""
ins = self.get_object()
ins:MIO = self.get_object()
if ins.inout_date is None:
raise ParseError('出入库日期未填写')
if ins.state != MIO.MIO_CREATE:
raise ParseError('记录状态异常')
with transaction.atomic():
ins.submit_time = timezone.now()
ins.state = MIO.MIO_SUBMITED
ins.submit_user = request.user
ins.update_by = request.user
ins.save()
InmService.update_inm(ins)
now = timezone.now()
ins.submit_user = request.user
ins.submit_time = now
ins.update_by = request.user
ins.state = MIO.MIO_SUBMITED
ins.save()
InmService.update_inm(ins)
InmService.update_material_count(ins)
return Response(MIOListSerializer(instance=ins).data)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic
def revert(self, request, *args, **kwargs):
"""撤回
@ -223,16 +239,84 @@ class MIOViewSet(CustomModelViewSet):
raise ParseError('记录状态异常')
if ins.submit_user != user:
raise ParseError('非提交人不可撤回')
with transaction.atomic():
ins.submit_time = None
ins.state = MIO.MIO_CREATE
ins.update_by = user
ins.save()
InmService.update_inm(ins, is_reverse=True)
ins.submit_user = None
ins.update_by = user
ins.state = MIO.MIO_CREATE
ins.submit_time = None
ins.save()
InmService.update_inm(ins, is_reverse=True)
InmService.update_material_count(ins)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
@transaction.atomic
def pack_mioitem(self, request, *args, **kwargs):
"""装箱
装箱
"""
mio:MIO = self.get_object()
if mio.submit_time is not None:
raise ParseError('该出入库已提交不可装箱')
sr = PackMioSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
pack_index = vdata["pack_index"]
mioitems = vdata["mioitems"]
if not mioitems:
raise ParseError('未选择明细')
for id in mioitems:
mioitem = MIOItem.objects.get(id=id)
if mioitem.mio != mio:
raise ParseError('存在明细不属于该箱')
mioitem.pack_index = pack_index
mioitem.save(update_fields=['pack_index', 'update_time'])
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
def daoru_mioitem(self, request, *args, **kwargs):
"""导入明细
导入明细
"""
daoru_mioitems(settings.BASE_DIR + request.data.get('path', ''), mio=self.get_object())
return Response()
class PackViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
"""
list: 装箱记录
装箱记录
"""
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
queryset = Pack.objects.all()
serializer_class = PackSerializer
filterset_fields = ["mio"]
ordering = ["mio", "index"]
@action(methods=['post'], detail=False, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
@transaction.atomic
def pack_mioitem(self, request, *args, **kwargs):
"""装箱
装箱
"""
vdata = PackMioSerializer(data=request.data)
packId = vdata["pack"]
pack:Pack = Pack.objects.get(id=packId)
mioitems = vdata["mioitems"]
if not mioitems:
raise ParseError('未选择明细')
for id in mioitems:
mioitem = MIOItem.objects.get(id=id)
if mioitem.mio != pack.mio:
raise ParseError('存在明细不属于该装箱记录')
mioitem.pack = pack
mioitem.save(update_fields=['pack', 'update_time'])
return Response()
class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
"""
list: 出入库明细
@ -242,6 +326,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
queryset = MIOItem.objects.all()
serializer_class = MIOItemSerializer
retrieve_serializer_class = MioItemDetailSerializer
create_serializer_class = MIOItemCreateSerializer
select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
filterset_fields = {
@ -251,21 +336,58 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
"mio__type": ["exact", "in"],
"mio__inout_date": ["gte", "lte", "exact"],
"material": ["exact"],
"material__type": ["exact"],
"test_date": ["isnull", "exact"]
}
ordering = ['create_time']
ordering_fields = ['create_time', 'test_date']
search_fields =['batch', 'a_mioitem__batch']
def add_info_for_list(self, data):
with_mio = self.request.query_params.get('with_mio', "no")
if with_mio == "yes" and isinstance(data, list):
mio_ids = [item['mio'] for item in data]
mio_qs = MIO.objects.filter(id__in=mio_ids)
mio_qs_= MIOListSerializer(mio_qs, many=True).data
mio_dict = {mio['id']: mio for mio in mio_qs_}
for item in data:
mioId = item['mio']
item['mio_'] = mio_dict[mioId]
return data
@swagger_auto_schema(manual_parameters=[
openapi.Parameter(name="with_mio", in_=openapi.IN_QUERY, description="是否返回出入库记录信息",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)",
type=openapi.TYPE_STRING, required=False)
])
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio'])
return super().perform_create(serializer)
def perform_destroy(self, instance):
if instance.mio.state != MIO.MIO_CREATE:
raise ParseError('出入库记录非创建中不可删除')
MIOViewSet.lock_and_check_can_update(instance.mio)
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
raise PermissionDenied('无权限删除')
return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
@transaction.atomic
def revert_and_del(self, request, *args, **kwargs):
"""撤回并删除
撤回并删除
"""
ins:MIOItem = self.get_object()
InmService.revert_and_del(ins)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer)
@transaction.atomic
def test(self, request, *args, **kwargs):
@ -286,6 +408,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
sr.save()
# 开始变动库存
InmService.update_mb_item(ins, -1, 'count_notok')
InmService.update_material_count(ins.mio)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=serializers.Serializer)
@ -303,6 +426,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
pass
ins.test_date = None
ins.save()
InmService.update_material_count(ins.mio)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemPurInTestSerializer)
@ -320,6 +444,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
sr = MIOItemPurInTestSerializer(instance=ins, data=request.data)
sr.is_valid(raise_exception=True)
sr.save()
InmService.update_material_count(ins.mio)
return Response()
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=MioItemAnaSerializer)
@ -346,13 +471,23 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
if res[i] is None:
res[i] = 0
return Response(res)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
@transaction.atomic
def test_daoru_bg(self, request, *args, **kwargs):
"""导入棒管检验
导入棒管检验
"""
daoru_mioitem_test(path=settings.BASE_DIR + request.data.get('path', ''), mioitem=self.get_object())
return Response()
class MIOItemwViewSet(CustomModelViewSet):
perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'}
queryset = MIOItemw.objects.all()
serializer_class = MIOItemwCreateUpdateSerializer
filterset_fields = ['mioitem']
filterset_fields = ['mioitem', 'wpr']
ordering = ["number", "create_time"]
ordering_fields = ["number", "create_time"]
@ -368,20 +503,20 @@ class MIOItemwViewSet(CustomModelViewSet):
mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count()
mioitem.save()
@transaction.atomic
def perform_create(self, serializer):
ins: MIOItemw = serializer.save()
mioitem: MIOItem = ins.mioitem
self.cal_mioitem_count(mioitem)
MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio)
ins:MIOItemw = serializer.save()
self.cal_mioitem_count(ins.mioitem)
@transaction.atomic
def perform_update(self, serializer):
mioitemw = serializer.save()
self.cal_mioitem_count(mioitemw.mioitem)
ins:MIOItemw = serializer.instance
MIOViewSet.lock_and_check_can_update(ins.mioitem.mio)
ins:MIOItemw = serializer.save()
self.cal_mioitem_count(ins.mioitem)
@transaction.atomic
def perform_destroy(self, instance: MIOItemw):
mioitem = instance.mioitem
MIOViewSet.lock_and_check_can_update(mioitem.mio)
ftest = instance.ftest
instance.delete()
if ftest:

View File

@ -5,6 +5,7 @@ from apps.utils.models import BaseModel
class AuditLog(models.Model):
"""TN:审计表"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
action = models.CharField('动作', max_length=20)
model_name = models.CharField('模型名', max_length=20)
@ -18,7 +19,7 @@ class AuditLog(models.Model):
class DrfRequestLog(BaseModel):
"""Logs Django rest framework API requests"""
"""TN:Logs Django rest framework API requests"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
user = models.ForeignKey(

View File

@ -1,10 +1,11 @@
from django_filters import rest_framework as filters
from apps.mtm.models import Goal, Material, Route
from apps.mtm.models import Goal, Material, Route, RoutePack
from django.db.models.expressions import F
from rest_framework.exceptions import ParseError
class MaterialFilter(filters.FilterSet):
tag = filters.CharFilter(method='filter_tag')
tag = filters.CharFilter(method='filter_tag', label="low_inm:库存不足")
class Meta:
model = Material
@ -27,7 +28,7 @@ class MaterialFilter(filters.FilterSet):
def filter_tag(self, queryset, name, value):
if value == 'low_inm':
queryset = queryset.exclude(count_safe=None).filter(
queryset = queryset.exclude(count_safe=None).exclude(count_safe__lte=0).filter(
count__lte=F('count_safe'))
return queryset
@ -45,6 +46,8 @@ class GoalFilter(filters.FilterSet):
class RouteFilter(filters.FilterSet):
nprocess_name = filters.CharFilter(method='filter_nprocess_name', label="nprocess_name")
material_in_has = filters.CharFilter(method='filter_material_in_has', label="material_in_has ID")
class Meta:
model = Route
fields = {
@ -58,5 +61,18 @@ class RouteFilter(filters.FilterSet):
"mgroup": ["exact", "in", "isnull"],
"mgroup__name": ["exact", "contains"],
"mgroup__belong_dept": ["exact"],
"mgroup__belong_dept__name": ["exact", "contains"]
"mgroup__belong_dept__name": ["exact", "contains"],
"from_route": ["exact", "isnull"],
}
def filter_nprocess_name(self, queryset, name, value):
return queryset
def filter_material_in_has(self, queryset, name, value):
nprocess_name = self.data.get('nprocess_name', None)
if nprocess_name:
routepack_qs = queryset.filter(material_in__id=value, routepack__isnull=False, routepack__state=RoutePack.RP_S_CONFIRM).values_list('routepack', flat=True)
qs = queryset.filter(routepack__in=routepack_qs, process__name=nprocess_name)
return qs
raise ParseError("nprocess_name is required")

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-04-21 02:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0055_auto_20250327_1239'),
]
operations = [
migrations.AddField(
model_name='mgroup',
name='batch_append_code',
field=models.BooleanField(default=False, verbose_name='批号追加工段标识'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-04-28 06:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0056_mgroup_batch_append_code'),
]
operations = [
migrations.AddField(
model_name='process',
name='number_to_batch',
field=models.BooleanField(default=False, verbose_name='个号转批号'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-05-16 07:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0057_process_number_to_batch'),
]
operations = [
migrations.AddField(
model_name='process',
name='wpr_number_rule',
field=models.TextField(blank=True, null=True, verbose_name='单个编号规则'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-06-18 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0058_process_wpr_number_rule'),
]
operations = [
migrations.AddField(
model_name='material',
name='bin_number_main',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='主库位号'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-08-07 02:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0059_material_bin_number_main'),
]
operations = [
migrations.AddField(
model_name='route',
name='params_json',
field=models.JSONField(blank=True, default=dict, verbose_name='工艺参数'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-08-21 09:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0060_route_params_json'),
]
operations = [
migrations.AddField(
model_name='material',
name='img',
field=models.TextField(blank=True, null=True, verbose_name='图片'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2025-09-02 03:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mtm', '0061_material_img'),
]
operations = [
migrations.AddField(
model_name='route',
name='from_route',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='route_f', to='mtm.route', verbose_name='来源路线'),
),
migrations.AlterField(
model_name='route',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='route_parent', to='mtm.route', verbose_name='上级路线'),
),
]

View File

@ -4,10 +4,12 @@ from rest_framework.exceptions import ParseError
from apps.utils.models import CommonBDModel
from collections import defaultdict, deque
from django.db.models import Q
from datetime import datetime, timedelta
from django.utils import timezone
class Process(CommonBModel):
"""
工序
TN:工序
"""
PRO_PROD = 10
PRO_TEST = 20
@ -16,6 +18,7 @@ class Process(CommonBModel):
PRO_NORMAL = 10
PRO_DIV = 20
PRO_MERGE = 30
name = models.CharField('工序名称', max_length=100)
type = models.PositiveSmallIntegerField("工序类型", default=PRO_PROD, choices=((PRO_PROD, '生产工序'), (PRO_TEST, '检验工序')))
mtype = models.PositiveSmallIntegerField("工序生产类型", default=PRO_NORMAL, choices=((PRO_NORMAL, '常规'), (PRO_DIV, '切分'), (PRO_MERGE, '合并')))
@ -30,14 +33,27 @@ class Process(CommonBModel):
mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False)
mstate_json = models.JSONField('中间状态', default=list, blank=True)
parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True)
number_to_batch = models.BooleanField('个号转批号', default=False)
wpr_number_rule = models.TextField("单个编号规则", null=True, blank=True)
class Meta:
verbose_name = '工序'
ordering = ['sort', 'create_time']
def get_canout_mat_ids(self):
"""获取可产出的materialIds
"""
return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct())
def get_canin_mat_ids(self):
"""获取可输入的materialIds
"""
return list(RouteMat.objects.filter(route__process=self).values_list("material__id", flat=True).distinct()) + \
list(Route.objects.filter(process=self).values_list("material_in__id", flat=True).distinct())
# Create your models here.
class Material(CommonAModel):
"""TN:物料"""
MA_TYPE_BASE = 0
MA_TYPE_GOOD = 10
MA_TYPE_HALFGOOD = 20
@ -93,6 +109,8 @@ class Material(CommonAModel):
brothers = models.JSONField('兄弟件', default=list, null=False, blank=True)
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
into_wm = models.BooleanField('是否进入车间库存', default=True)
bin_number_main = models.CharField('主库位号', max_length=50, null=True, blank=True)
img = models.TextField('图片', null=True, blank=True)
class Meta:
verbose_name = '物料表'
@ -103,7 +121,7 @@ class Material(CommonAModel):
class Shift(CommonBModel):
"""班次
"""TN:班次
"""
name = models.CharField('名称', max_length=50)
rule = models.CharField('所属规则', max_length=10, default='默认')
@ -116,13 +134,13 @@ class Shift(CommonBModel):
class Srule(CommonBDModel):
"""
班组规则
TN:班组规则
"""
rule = models.JSONField('排班规则', default=list, blank=True)
class Team(CommonBModel):
"""班组, belong_dept为所属车间
"""TN:班组, belong_dept为所属车间
"""
name = models.CharField('名称', max_length=50)
leader = models.ForeignKey(
@ -130,7 +148,7 @@ class Team(CommonBModel):
class Mgroup(CommonBModel):
"""测点集
"""TN:测点集
"""
M_SELF = 10
M_OTHER = 20
@ -150,6 +168,7 @@ class Mgroup(CommonBModel):
'直接材料', default=list, blank=True, help_text='material的ID列表')
test_materials = models.JSONField(
'检测材料', default=list, blank=True, help_text='material的ID列表')
batch_append_code = models.BooleanField('批号追加工段标识', default=False)
sort = models.PositiveSmallIntegerField('排序', default=1)
need_enm = models.BooleanField('是否进行能源监测', default=True)
is_running = models.BooleanField('是否正常运行', default=False)
@ -160,6 +179,32 @@ class Mgroup(CommonBModel):
def __str__(self) -> str:
return self.name
def get_shift(self, w_s_time:datetime):
# 如果没有时区信息,使用默认时区(东八区)
if not timezone.is_aware(w_s_time):
w_s_time = timezone.make_aware(w_s_time)
else:
w_s_time = timezone.localtime(w_s_time)
shifts = Shift.objects.filter(rule=self.shift_rule).order_by('sort')
if not shifts:
raise ParseError(f"工段{self.name}未配置班次")
# 处理跨天班次的情况
for shift in shifts:
# 如果开始时间小于结束时间,表示班次在同一天内
if shift.start_time_o < shift.end_time_o:
if shift.start_time_o <= w_s_time.time() < shift.end_time_o:
return w_s_time.date(), shift
else: # 班次跨天(如夜班从当天晚上到次日凌晨)
if w_s_time.time() >= shift.start_time_o or w_s_time.time() < shift.end_time_o:
# 如果当前时间在开始时间之后,属于当天
if w_s_time.time() >= shift.start_time_o:
return w_s_time.date(), shift
# 如果当前时间在结束时间之前,属于前一天
else:
return (w_s_time - timedelta(days=1)).date(), shift
# return w_s_time.date(), None
class TeamMember(BaseModel):
@ -174,7 +219,7 @@ class TeamMember(BaseModel):
class Goal(CommonADModel):
"""目标
"""TN:目标
"""
mgroup = models.ForeignKey(
Mgroup, verbose_name='关联工段', on_delete=models.CASCADE, null=True, blank=True)
@ -201,7 +246,7 @@ class Goal(CommonADModel):
class RoutePack(CommonADModel):
"""
加工工艺
TN:加工工艺
"""
RP_S_CREATE = 10
RP_S_AUDIT = 20
@ -315,7 +360,8 @@ class RoutePack(CommonADModel):
route_dict[r.id] = {
"label": r.process.name if r.process else "",
"source": r.material_in.id,
"target": r.material_out.id
"target": r.material_out.id,
"id": r.id
}
# 获取所有物料信息
@ -349,7 +395,7 @@ class RoutePack(CommonADModel):
return list(self.get_gjson().keys())
class Route(CommonADModel):
"""
加工路线
TN:加工路线
"""
routepack = models.ForeignKey(RoutePack, verbose_name='关联路线包', on_delete=models.CASCADE, null=True, blank=True)
material = models.ForeignKey(
@ -371,12 +417,22 @@ class Route(CommonADModel):
batch_bind = models.BooleanField('是否绑定批次', default=True)
materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials",
through="mtm.routemat", blank=True)
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True)
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True, related_name="route_parent")
params_json = models.JSONField('工艺参数', default=dict, blank=True)
from_route = models.ForeignKey('self', verbose_name='来源路线', on_delete=models.SET_NULL, null=True, blank=True, related_name="route_f")
def __str__(self):
x = ""
if self.material_in:
x = x + str(self.material_in) + "->"
if self.material_out:
x = x + str(self.material_out)
return x
@staticmethod
def get_routes(material: Material=None, routepack:RoutePack=None, routeIds=None):
"""
返回工艺路线带车间(不关联工艺包)
TN:返回工艺路线带车间(不关联工艺包)
"""
if material:
kwargs = {'material': material, 'routepack__isnull': True}
@ -397,7 +453,7 @@ class Route(CommonADModel):
@classmethod
def validate_dag(cls, final_material_out:Material, rqs):
"""
校验工艺路线是否正确
TN:校验工艺路线是否正确
- 所有 Route 必须有 material_in material_out
- 所有路径最终指向给定的 final_material_out
- 无循环依赖
@ -493,6 +549,7 @@ class Route(CommonADModel):
'source': source,
'target': target,
'label': rq.process.name,
'id': rq.id
})
# 将批次号排序
nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time")
@ -506,5 +563,6 @@ class Route(CommonADModel):
return {'nodes': nodes, 'edges': edges}
class RouteMat(BaseModel):
"""TN:工艺路线辅助物料"""
route = models.ForeignKey(Route, verbose_name='关联路线', on_delete=models.CASCADE, related_name="routemat_route")
material = models.ForeignKey(Material, verbose_name='辅助物料', on_delete=models.CASCADE)

View File

@ -24,7 +24,7 @@ class MaterialSimpleSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ['id', 'name', 'number', 'model',
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking"]
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking", "bin_number_main"]
def get_full_name(self, obj):
return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}'
@ -156,6 +156,7 @@ class RoutePackCopySerializer(serializers.Serializer):
material_out = serializers.CharField(label='产品ID')
class RouteSerializer(CustomModelSerializer):
name = serializers.CharField(source='__str__', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True)
routepack_name = serializers.StringRelatedField(source='routepack.name', read_only=True)
process_name = serializers.CharField(source='process.name', read_only=True)
@ -185,8 +186,8 @@ class RouteSerializer(CustomModelSerializer):
raise ParseError('未提供操作工序')
if process.parent is not None:
raise ParseError('操作工序不可为子工序')
if process.mtype == Process.PRO_DIV and attrs.get('div_number', 1) <= 1:
raise ParseError('切分数量必须大于1')
if process.mtype == Process.PRO_DIV and attrs.get('div_number', 1) < 1:
raise ParseError('切分数量必须大于等于1')
return super().validate(attrs)
@classmethod
@ -211,7 +212,8 @@ class RouteSerializer(CustomModelSerializer):
if material_out:
material_out.is_deleted = False
if material_out.parent is None:
material_out.parent = material
if material_out.id != material.id:
material_out.parent = material
material_out.cate = material.cate
material_out.tracking = material_out_tracking
material_out.save()
@ -244,30 +246,34 @@ class RouteSerializer(CustomModelSerializer):
# material = validated_data.get('material', None)
# if material and process and Route.objects.filter(material=material, process=process).exists():
# raise ValidationError('已选择该工序!!')
with transaction.atomic():
instance = super().create(validated_data)
material_out = instance.material_out
if material_out:
if material_out.process is None:
material_out.process = process
if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if instance.material:
material_out.parent = instance.material
material_out.save()
# elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择')
else:
if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save()
rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
if rx:
msg = ""
if rx.routepack:
msg = rx.routepack.name
raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
instance:Route = super().create(validated_data)
material_out = instance.material_out
if material_out:
if material_out.process is None:
material_out.process = process
if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if material_out.parent is None and instance.material:
material_out.parent = instance.material
material_out.save()
# elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择')
else:
if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save()
rx = Route.objects.filter(
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
if rx:
instance.from_route = rx
instance.save()
# msg = ""
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
def update(self, instance, validated_data):
validated_data.pop('material', None)
@ -275,30 +281,34 @@ class RouteSerializer(CustomModelSerializer):
material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH)
if material_out_tracking is None:
material_out_tracking = Material.MA_TRACKING_BATCH
with transaction.atomic():
instance = super().update(instance, validated_data)
material_out = instance.material_out
if material_out:
if material_out.process is None:
material_out.process = process
if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if instance.material:
material_out.parent = instance.material
material_out.save()
# elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择')
else:
if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save()
rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
if rx:
msg = ""
if rx.routepack:
msg = rx.routepack.name
raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
instance = super().update(instance, validated_data)
material_out = instance.material_out
if material_out:
if material_out.process is None:
material_out.process = process
if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if material_out.parent is None and instance.material:
material_out.parent = instance.material
material_out.save()
# elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择')
else:
if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save()
rx = Route.objects.filter(
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
if rx:
instance.from_route = rx
instance.save()
# msg = ""
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
def to_representation(self, instance):
res = super().to_representation(instance)
@ -327,4 +337,16 @@ class RouteMatSerializer(CustomModelSerializer):
class Meta:
model = RouteMat
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE
read_only_fields = EXCLUDE_FIELDS_BASE
def validate(self, attrs):
route:Route = attrs["route"]
if route.from_route is not None:
raise ParseError("该工艺步骤引用其他步骤,无法修改")
return attrs
class MaterialExportSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"]

View File

@ -51,33 +51,35 @@ def daoru_material(path: str):
'辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70}
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path)
sheet = wb['物料']
wb = load_workbook(path, read_only=True)
sheet = wb.active
process_l = Process.objects.all()
process_d = {p.name: p for p in process_l}
i = 3
if sheet['a2'].value != '物料编号':
raise ParseError('列错误导入失败')
while sheet[f'b{i}'].value is not None:
while sheet[f'b{i}'].value is not None or sheet[f'd{i}'].value is not None:
type_str = sheet[f'b{i}'].value.replace(' ', '')
try:
type = type_dict[type_str]
cate = sheet[f'c{i}'].value.replace(' ', '') if sheet[f'c{i}'].value else ""
number = str(sheet[f'a{i}'].value).replace(' ', '') if sheet[f'a{i}'].value else None
if sheet[f'c{i}'].value:
name = str(sheet[f'c{i}'].value).replace(' ', '')
if sheet[f'd{i}'].value:
name = str(sheet[f'd{i}'].value).replace(' ', '')
else:
raise ParseError(f'{i}行物料信息错误: 物料名称必填')
specification = str(sheet[f'd{i}'].value).replace(
'×', '*').replace(' ', '') if sheet[f'd{i}'].value else None
model = str(sheet[f'e{i}'].value).replace(' ', '') if sheet[f'e{i}'].value else None
unit = sheet[f'f{i}'].value.replace(' ', '')
count_safe = float(sheet[f'h{i}'].value) if sheet[f'h{i}'].value else None
unit_price = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None
specification = str(sheet[f'e{i}'].value).replace(
'×', '*').replace(' ', '') if sheet[f'e{i}'].value else None
model = str(sheet[f'f{i}'].value).replace(' ', '') if sheet[f'f{i}'].value else None
unit = sheet[f'g{i}'].value.replace(' ', '')
count_safe = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None
unit_price = float(sheet[f'j{i}'].value) if sheet[f'j{i}'].value else None
bin_number_main = sheet[f'k{i}'].value.replace(' ', '') if sheet[f'k{i}'].value else None
except Exception as e:
raise ParseError(f'{i}行物料信息错误: {e}')
if type in [20, 30]:
try:
process = process_d[sheet[f'g{i}'].value.replace(' ', '')]
process = process_d[sheet[f'h{i}'].value.replace(' ', '')]
except Exception as e:
raise ParseError(f'{i}行物料信息错误: {e}')
try:
@ -87,7 +89,7 @@ def daoru_material(path: str):
filters['process'] = process
default = {'type': type, 'name': name, 'specification': specification,
'model': model, 'unit': unit, 'number': number if number else f'm{type}_{ranstr(6)}', 'id': idWorker.get_id(),
'count_safe': count_safe, 'unit_price': unit_price}
'count_safe': count_safe, 'unit_price': unit_price, 'cate': cate, 'bin_number_main': bin_number_main}
material, is_created = Material.objects.get_or_create(
**filters, defaults=default)
if not is_created:
@ -153,12 +155,12 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
raise ParseError('缺少步骤')
r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
first_route = r_qs.first()
last_route = r_qs.last()
if first_route.batch_bind:
first_route.batch_bind = False
first_route.save(update_fields=['batch_bind'])
if last_route.material_out != routepack.material:
raise ParseError('最后一步产出与工艺包不一致')
# last_route = r_qs.last()
# if last_route.material_out != routepack.material:
# raise ParseError('最后一步产出与工艺包不一致')
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'routepack',
@ -169,8 +171,8 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
ticket.save()
if routepack.ticket is None:
routepack.ticket = ticket
routepack.state = RoutePack.RP_S_AUDIT
routepack.save()
routepack.state = RoutePack.RP_S_AUDIT
routepack.save()
def routepack_audit_end(ticket: Ticket):
@ -180,7 +182,7 @@ def routepack_audit_end(ticket: Ticket):
def routepack_ticket_change(ticket: Ticket):
routepack = RoutePack.objects.get(id=ticket.ticket_data['t_id'])
if ticket.act_state == Ticket.TICKET_ACT_STATE_DRAFT:
if ticket.act_state in [Ticket.TICKET_ACT_STATE_DRAFT, Ticket.TICKET_ACT_STATE_BACK, Ticket.TICKET_ACT_STATE_RETREAT]:
routepack.state = RoutePack.RP_S_CREATE
routepack.save()

View File

@ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer,
MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer,
ShiftSerializer, TeamSerializer, ProcessSerializer,
RouteSerializer, TeamMemberSerializer, RoutePackSerializer,
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer)
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer, MaterialExportSerializer)
from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin
@ -21,6 +21,8 @@ from django.db.models import Q
from apps.wf.models import Ticket
from django.utils import timezone
from rest_framework.permissions import IsAdminUser
from apps.utils.export import export_excel
from operator import itemgetter
# Create your views here.
class MaterialViewSet(CustomModelViewSet):
@ -32,14 +34,13 @@ class MaterialViewSet(CustomModelViewSet):
queryset = Material.objects.all()
serializer_class = MaterialSerializer
filterset_class = MaterialFilter
search_fields = ['name', 'code', 'number', 'specification', 'model']
search_fields = ['name', 'code', 'number', 'specification', 'model', 'bin_number_main']
select_related_fields = ['process']
ordering = ['name', 'model', 'specification',
'type', 'process', 'process__sort', 'sort', 'id', 'number']
ordering_fields = ['name', 'model', 'specification',
'type', 'process', 'process__sort', 'sort', 'id', 'number']
'type', 'process', 'process__sort', 'sort', 'id', 'number', 'create_time']
@transaction.atomic
def perform_destroy(self, instance):
from apps.inm.models import MaterialBatch
if MaterialBatch.objects.filter(material=instance).exists():
@ -59,6 +60,18 @@ class MaterialViewSet(CustomModelViewSet):
daoru_material(settings.BASE_DIR + request.data.get('path', ''))
return Response()
@action(methods=['post'], detail=True, serializer_class=Serializer, perms_map={'post': 'material.create'})
@transaction.atomic
def cal_count(self, request, *args, **kwargs):
"""统计数量
统计数量
"""
ins = self.get_object()
from apps.mtm.services_2 import cal_material_count
cal_material_count([ins.id])
return Response()
@action(methods=['put'], detail=True, serializer_class=Serializer, perms_map={'put': '*'})
@transaction.atomic
def set_week_esitimate_consume(self, request, *args, **kwargs):
@ -76,6 +89,23 @@ class MaterialViewSet(CustomModelViewSet):
def cates(self, request, *args, **kwargs):
res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct()
return Response(set(res))
@action(methods=['get'], detail=False, perms_map={'get': '*'})
def export_excel(self, request, pk=None):
"""导出excel
导出excel
"""
field_data = ['大类', '物料编号', '名称', '规格', '型号', '计量单位', '仓库位号', "安全库存", "单价"]
queryset = self.filter_queryset(self.get_queryset())
if queryset.count() > 1000:
raise ParseError('数据量超过1000,请筛选后导出')
odata = MaterialExportSerializer(queryset, many=True).data
# 处理数据
field_keys = ['cate', 'number', 'name', 'specification', 'model', 'unit',
'bin_number_main', 'count_safe', 'unit_price']
getter = itemgetter(*field_keys)
data = [list(getter(item)) for item in odata]
return Response({'path': export_excel(field_data, data, '物料清单')})
class ShiftViewSet(ListModelMixin, CustomGenericViewSet):
"""
@ -281,7 +311,7 @@ class RoutePackViewSet(CustomModelViewSet):
return Response({"id": route_new.id})
@transaction.atomic
@action(methods=['post'], detail=True, permission_classes = [IsAdminUser], serializer_class=Serializer)
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer)
def toggle_state(self, request, *args, **kwargs):
"""变更工艺路线状态
@ -333,15 +363,26 @@ class RouteViewSet(CustomModelViewSet):
serializer_class = RouteSerializer
filterset_class = RouteFilter
ordering = ['sort', 'process__sort', 'create_time']
ordering_fields = ['sort', 'process__sort', 'create_time', 'update_time']
select_related_fields = ['material',
'process', 'material_in', 'material_out', 'mgroup', 'routepack']
def update(self, request, *args, **kwargs):
obj:Route = self.get_object()
routepack = obj.routepack
def perform_update(self, serializer):
ins:Route = serializer.instance
if ins.from_route is not None:
raise ParseError('该工艺步骤引用其他步骤, 无法编辑')
old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process
routepack = ins.routepack
if routepack and routepack.state != RoutePack.RP_S_CREATE:
raise ParseError('该状态下不可编辑')
return super().update(request, *args, **kwargs)
raise ParseError('该工艺路线非创建中不可编辑')
ins_n:Route = serializer.save()
if Route.objects.filter(from_route__id=ins.id).exists() and (ins_n.material_in != old_m_in or ins_n.material_out != old_m_out or ins_n.process != process):
raise ParseError("该工艺步骤被其他步骤引用, 无法修改关键信息")
def perform_destroy(self, instance:Route):
if Route.objects.filter(from_route=instance).exists():
raise ParseError('该工艺步骤被其他步骤引用,无法删除')
return super().perform_destroy(instance)
class SruleViewSet(CustomModelViewSet):

0
apps/ofm/__init__.py Normal file
View File

3
apps/ofm/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/ofm/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class OfmConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.ofm'

32
apps/ofm/filters.py Normal file
View File

@ -0,0 +1,32 @@
from django_filters import rest_framework as filters
from apps.ofm.models import MroomBooking, BorrowRecord
from .models import LendingSeal
from apps.utils.filters import MyJsonListFilter
class MroomBookingFilterset(filters.FilterSet):
class Meta:
model = MroomBooking
fields = {
'slot_b__mroom': ['exact', 'in'],
'slot_b__booking': ['exact'],
'slot_b__mdate': ['exact', 'gte', 'lte'],
'create_by': ['exact'],
"id": ["exact"]
}
class SealFilter(filters.FilterSet):
seal = MyJsonListFilter(label='按印章名称查询', field_name="seal")
class Meta:
model = LendingSeal
fields = ['seal']
class BorrowRecordFilter(filters.FilterSet):
file_name = filters.CharFilter(label='按文件名称查询', field_name="borrow_file__name", lookup_expr='icontains')
borrow_user = filters.CharFilter(label='按借阅人查询', field_name="create_by__name", lookup_expr='icontains')
class Meta:
model = BorrowRecord
fields = ['file_name', 'borrow_user']

View File

@ -0,0 +1,66 @@
# Generated by Django 3.2.12 on 2025-06-25 09:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Mroom',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=50, unique=True, verbose_name='会议室名称')),
('location', models.CharField(max_length=100, verbose_name='位置')),
('capacity', models.PositiveIntegerField(verbose_name='容纳人数')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='MroomBooking',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('title', models.CharField(max_length=100, verbose_name='会议主题')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='MroomSlot',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('mdate', models.DateField(db_index=True, verbose_name='会议日期')),
('slot', models.PositiveIntegerField(help_text='0-47', verbose_name='时段')),
('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_b', to='ofm.mroombooking')),
('mroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_m', to='ofm.mroom')),
],
options={
'unique_together': {('mroom', 'mdate', 'slot')},
},
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 3.2.12 on 2025-09-05 03:07
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_state_filter_dept'),
('system', '0006_auto_20241213_1249'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ofm', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LendingSeal',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('seal', models.JSONField(default=list, help_text='{"seal_name": "印章名称"}', verbose_name='印章信息')),
('filename', models.TextField(verbose_name='文件名称')),
('file', models.TextField(verbose_name='文件内容')),
('file_count', models.PositiveIntegerField(verbose_name='用印份数')),
('is_lending', models.BooleanField(default=False, verbose_name='是否借出')),
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='联系方式')),
('lending_date', models.DateField(blank=True, null=True, verbose_name='借出日期')),
('return_date', models.DateField(blank=True, null=True, verbose_name='拟归还日期')),
('actual_return_date', models.DateField(blank=True, null=True, verbose_name='实际归还日期')),
('reason', models.CharField(blank=True, max_length=100, null=True, verbose_name='借用理由')),
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('submit_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seal_submit_user', to=settings.AUTH_USER_MODEL, verbose_name='提交人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seal_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2025-09-08 03:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ofm', '0002_lendingseal'),
]
operations = [
migrations.RemoveField(
model_name='lendingseal',
name='submit_user',
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.2.12 on 2025-09-10 06:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0002_alter_state_filter_dept'),
('ofm', '0003_remove_lendingseal_submit_user'),
]
operations = [
migrations.CreateModel(
name='Vehicle',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('start_time', models.DateField(blank=True, null=True, verbose_name='出车时间')),
('end_time', models.DateField(blank=True, null=True, verbose_name='还车时间')),
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='出发地点')),
('destination', models.CharField(blank=True, max_length=100, null=True, verbose_name='到达地点')),
('start_km', models.PositiveIntegerField(verbose_name='出发公里数')),
('end_km', models.PositiveIntegerField(verbose_name='归还公里数')),
('actual_km', models.PositiveIntegerField(editable=False, verbose_name='实际行驶公里数')),
('is_city', models.BooleanField(default=True, verbose_name='是否市内用车')),
('reason', models.CharField(max_length=100, verbose_name='用车事由')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-09-10 06:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0004_vehicle'),
]
operations = [
migrations.AddField(
model_name='vehicle',
name='via',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='途经地点'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2025-09-11 01:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
('ofm', '0005_vehicle_via'),
]
operations = [
migrations.AddField(
model_name='vehicle',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_belong_dept', to='system.dept', verbose_name='所属部门'),
),
]

View File

@ -0,0 +1,67 @@
# Generated by Django 3.2.12 on 2025-09-11 06:41
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ofm', '0006_vehicle_belong_dept'),
]
operations = [
migrations.AlterField(
model_name='lendingseal',
name='seal',
field=models.JSONField(default=list, help_text='[公章,法人章,财务章,合同章,业务章,其他章]', verbose_name='印章信息'),
),
migrations.CreateModel(
name='FileRecord',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=100, verbose_name='资料名称')),
('number', models.CharField(blank=True, max_length=50, null=True, verbose_name='档案编号')),
('counts', models.CharField(blank=True, max_length=10, null=True, verbose_name='文件份数')),
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='存放位置')),
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='存档人电话')),
('reciver', models.CharField(blank=True, max_length=50, null=True, verbose_name='接收人(综合办)')),
('remark', models.TextField(blank=True, max_length=200, null=True, verbose_name='备注')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='BorrowRecord',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('borrow_date', models.DateField(blank=True, null=True, verbose_name='借阅日期')),
('return_date', models.DateField(blank=True, null=True, verbose_name='归还日期')),
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='借阅人电话')),
('remark', models.JSONField(default=list, help_text=['借阅', '复印', '查阅'], verbose_name='用途')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_belong_dept', to='system.dept', verbose_name='所属部门')),
('borrow_file', models.ManyToManyField(related_name='borrow_records', to='ofm.FileRecord')),
('borrow_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='borrow_user', to=settings.AUTH_USER_MODEL)),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2025-09-12 06:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ofm', '0007_auto_20250911_1441'),
]
operations = [
migrations.RemoveField(
model_name='borrowrecord',
name='borrow_user',
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2025-09-12 07:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_state_filter_dept'),
('ofm', '0008_remove_borrowrecord_borrow_user'),
]
operations = [
migrations.AddField(
model_name='borrowrecord',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrow_ticket', to='wf.ticket', verbose_name='关联工单'),
),
]

View File

@ -0,0 +1,53 @@
# Generated by Django 3.2.12 on 2025-09-19 01:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('system', '0006_auto_20241213_1249'),
('ofm', '0009_borrowrecord_ticket'),
]
operations = [
migrations.AddField(
model_name='lendingseal',
name='seal_other',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='其他印章'),
),
migrations.CreateModel(
name='Publicity',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('number', models.CharField(max_length=50, verbose_name='记录编号')),
('title', models.CharField(max_length=100, verbose_name='送审稿件标题')),
('participants', models.CharField(max_length=50, verbose_name='所有撰稿人')),
('level', models.JSONField(default=list, help_text=['重要', '一般', '非涉密'], verbose_name='用途')),
('content', models.JSONField(default=list, help_text=['武器装备科研生产综合事项', '其它'], verbose_name='稿件内容涉及')),
('other_content', models.CharField(blank=True, max_length=100, null=True, verbose_name='其它内容')),
('report_purpose', models.CharField(blank=True, max_length=100, null=True, verbose_name='宣传报道目的')),
('channel', models.JSONField(default=list, help_text=['互联网', '信息平台', '官微', '公开发行物', '其它'], verbose_name='发布渠道')),
('channel_other', models.CharField(blank=True, max_length=50, null=True, verbose_name='其它渠道')),
('other_channel', models.CharField(blank=True, max_length=50, null=True, verbose_name='其它渠道')),
('report_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='报道名称')),
('review', models.JSONField(default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], verbose_name='第一撰稿人自审')),
('dept_opinion', models.JSONField(default=list, help_text=['同意', '不同意'], verbose_name='部门负责人意见')),
('dept_opinion_review', models.CharField(blank=True, max_length=100, null=True, verbose_name='部门审查意见')),
('publicity_opinion', models.JSONField(default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], verbose_name='宣传统战部审查意见')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.12 on 2025-09-24 05:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0010_auto_20250919_0921'),
]
operations = [
migrations.RemoveField(
model_name='publicity',
name='channel_other',
),
migrations.AddField(
model_name='publicity',
name='pfile',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='稿件路径'),
),
migrations.AddField(
model_name='publicity',
name='pub_dept',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='部室/研究院'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2025-09-24 06:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
('ofm', '0011_auto_20250924_1359'),
]
operations = [
migrations.AddField(
model_name='publicity',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_ticket', to='wf.ticket', verbose_name='关联工单'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2025-09-25 07:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
('ofm', '0012_publicity_ticket'),
]
operations = [
migrations.AddField(
model_name='mroomslot',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mrooms_ticket', to='wf.ticket', verbose_name='关联会议室'),
),
]

Some files were not shown because too many files have changed in this diff Show More