Compare commits

...

253 Commits

Author SHA1 Message Date
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
97 changed files with 4137 additions and 1315 deletions

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

@ -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
@ -234,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

@ -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

@ -62,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):

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):
"""

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

@ -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

@ -37,7 +37,9 @@ class MioFilter(filters.FilterSet):
"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

@ -129,7 +129,7 @@ class MIOItemCreateSerializer(CustomModelSerializer):
'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):
@ -153,7 +153,9 @@ 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]:
@ -169,52 +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')
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)
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

View File

@ -45,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:
@ -58,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

@ -10,7 +10,7 @@ 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):
"""
生产领料到车间
"""
@ -23,8 +23,6 @@ def do_out(item: MIOItem):
mgroup = mio.mgroup
do_user = mio.do_user
material:Material = item.material
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
return
# 获取defect
defect:Defect = None
@ -94,29 +92,36 @@ def do_out(item: MIOItem):
raise ParseError(f"批次错误!{e}")
mb.count = mb.count - xcount
if mb.count < 0:
raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
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, 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()
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)
@ -136,8 +141,7 @@ def do_in(item: MIOItem):
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 # 是否组合件入仓库
@ -172,38 +176,39 @@ def do_in(item: MIOItem):
raise ParseError("存在非正数!")
xbatchs.append(xbatch)
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}-存在多个相同批次!')
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}车间物料不属于同一车间')
# 扣减车间库存
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}车间物料不属于同一车间')
# 增加mb
if not is_zhj:
mb, _ = MaterialBatch.objects.get_or_create(
@ -226,9 +231,16 @@ def do_in(item: MIOItem):
# 开始变动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)
@ -398,7 +410,7 @@ class InmService:
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,
@ -430,7 +442,7 @@ class InmService:
elif in_or_out == -1:
mb.count = mb.count - change_count
if mb.count < 0:
raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
else:
mb.save()
if tracking == Material.MA_TRACKING_SINGLE:

View File

@ -138,13 +138,17 @@ def daoru_mioitems(path:str, mio:MIO):
mioitems = []
ind = 2
while sheet[f"b{ind}"].value:
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:

View File

@ -27,6 +27,7 @@ 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.
@ -151,6 +152,15 @@ class MIOViewSet(CustomModelViewSet):
'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 = {}
@ -195,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):
"""撤回
@ -227,12 +239,12 @@ 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()
@ -353,10 +365,13 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
])
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)
@ -488,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

@ -1,6 +1,7 @@
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):
@ -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,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,6 +4,8 @@ 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):
"""
@ -177,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):
@ -389,8 +417,9 @@ 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 = ""

View File

@ -246,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 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).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)
@ -277,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 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).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)
@ -330,6 +338,12 @@ class RouteMatSerializer(CustomModelSerializer):
model = RouteMat
fields = "__all__"
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):

View File

@ -58,7 +58,7 @@ def daoru_material(path: str):
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]
@ -155,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',
@ -171,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):
@ -182,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

@ -39,9 +39,8 @@ class MaterialViewSet(CustomModelViewSet):
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():
@ -312,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):
"""变更工艺路线状态
@ -368,12 +367,22 @@ class RouteViewSet(CustomModelViewSet):
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):

View File

@ -1,6 +1,8 @@
from django_filters import rest_framework as filters
from apps.ofm.models import MroomBooking
from apps.ofm.models import MroomBooking, BorrowRecord
from .models import LendingSeal
from apps.utils.filters import MyJsonListFilter
class MroomBookingFilterset(filters.FilterSet):
class Meta:
@ -11,4 +13,20 @@ class MroomBookingFilterset(filters.FilterSet):
'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,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='关联会议室'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 3.2.12 on 2025-09-28 02:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
('ofm', '0013_mroomslot_ticket'),
]
operations = [
migrations.AddField(
model_name='mroombooking',
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='关联会议室'),
),
migrations.AddField(
model_name='mroomslot',
name='is_inuse',
field=models.BooleanField(default=True, verbose_name='是否占用'),
),
migrations.AlterUniqueTogether(
name='mroomslot',
unique_together={('mroom', 'mdate', 'slot', 'is_inuse')},
),
migrations.RemoveField(
model_name='mroomslot',
name='ticket',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-09-28 06:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0014_auto_20250928_1023'),
]
operations = [
migrations.AlterField(
model_name='vehicle',
name='end_km',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='归还公里数'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.12 on 2025-09-29 07:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
('ofm', '0015_alter_vehicle_end_km'),
]
operations = [
migrations.AddField(
model_name='mroombooking',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AddField(
model_name='mroombooking',
name='key_participants',
field=models.TextField(blank=True, null=True, verbose_name='主要参会领导'),
),
migrations.AddField(
model_name='mroombooking',
name='note',
field=models.TextField(blank=True, null=True, verbose_name='备注'),
),
migrations.AddField(
model_name='mroombooking',
name='participant_count',
field=models.PositiveIntegerField(default=0, verbose_name='参会人数'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2025-10-10 08:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0016_auto_20250929_1551'),
]
operations = [
migrations.AddField(
model_name='publicity',
name='secret_period',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='秘密期限'),
),
migrations.AlterField(
model_name='publicity',
name='level',
field=models.JSONField(default=list, help_text=['重要', '一般', '非涉密'], verbose_name='涉密等级'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 3.2.12 on 2025-10-11 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0017_auto_20251010_1631'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意', '不同意'], null=True, verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='number',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='记录编号'),
),
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], null=True, verbose_name='宣传统战部审查意见'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True, verbose_name='第一撰稿人自审'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.12 on 2025-10-11 03:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0018_auto_20251011_0922'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(default=list, help_text=['同意', '不同意'], verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], verbose_name='宣传统战部审查意见'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], verbose_name='第一撰稿人自审'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.12 on 2025-10-11 06:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0019_auto_20251011_1128'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意', '不同意'], null=True, verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], null=True, verbose_name='宣传统战部审查意见'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True, verbose_name='第一撰稿人自审'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-10-13 01:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0020_auto_20251011_1427'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='宣传报道意见'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2025-10-17 06:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ofm', '0021_alter_publicity_publicity_opinion'),
]
operations = [
migrations.AlterUniqueTogether(
name='mroomslot',
unique_together=set(),
),
]

View File

@ -0,0 +1,49 @@
# Generated by Django 3.2.12 on 2025-10-21 06:08
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 = [
('wf', '0004_workflow_view_path2'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('system', '0006_auto_20241213_1249'),
('ofm', '0022_alter_mroomslot_unique_together'),
]
operations = [
migrations.CreateModel(
name='PatentInfo',
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='拟申请专利名称')),
('author', models.CharField(max_length=100, verbose_name='发明人(设计人)')),
('type', models.CharField(choices=[('invention', '发明专利'), ('utility', '实用新型专利'), ('design', '外观设计专利')], default='invention', max_length=50, verbose_name='专利类型')),
('is_public', models.BooleanField(default=False, verbose_name='是否公开')),
('area', models.CharField(choices=[('Domestic', '国内申请'), ('Foreign', '国外申请'), (' PCT', 'PCT申请')], default='Domestic', max_length=50, verbose_name='拟申请地域')),
('identified', models.BooleanField(default=False, verbose_name='是否进行过科技成果鉴定')),
('published_article', models.BooleanField(default=False, verbose_name='是否发表过文章')),
('exhibited', models.BooleanField(default=False, verbose_name='是否参与过展会展出')),
('applied_to_production', models.BooleanField(default=False, verbose_name='是否参与应用于生产/销售')),
('participated_in_exchange', models.BooleanField(default=False, verbose_name='是否参与过技术交流')),
('tech_background_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='技术背景材料页数')),
('tech_disclosure_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='技术交底材料页数')),
('novelty_report_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='查新检索报告页数')),
('diagrams_or_photos_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='图/照片页数或张数')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_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='patentinfo_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='patentInfo_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,16 +1,40 @@
from django.db import models
from apps.utils.models import CommonADModel, BaseModel
from django.db import models, transaction
from apps.utils.models import CommonADModel, BaseModel, CommonBDModel
from apps.system.models import User
from django.core.validators import RegexValidator
from datetime import datetime
from rest_framework.exceptions import ParseError
# Create your models here.
MTASK_CREATED = 10
MTASK_ASSGINED = 20
MTASK_STOP = 34
MTASK_SUBMIT = 40
MTASK_STATES = (
(MTASK_CREATED, '创建中'),
(MTASK_ASSGINED, '已下达'),
(MTASK_STOP, '已停止'),
(MTASK_SUBMIT, '已提交')
)
phone_validator = RegexValidator(r'^1[3456789]\d{9}$', '手机号码格式不正确')
class Mroom(CommonADModel):
"""TN: 会议室基本信息"""
name = models.CharField('会议室名称', max_length=50, unique=True)
location = models.CharField('位置', max_length=100)
capacity = models.PositiveIntegerField('容纳人数')
class MroomBooking(CommonADModel):
class MroomBooking(CommonBDModel):
"""TN: 会议室预定信息"""
# belong_dept 是预定部门
title = models.CharField('会议主题', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联会议室',
on_delete=models.SET_NULL, related_name='mrooms_ticket', null=True, blank=True, db_constraint=False)
note = models.TextField('备注', null=True, blank=True)
participant_count = models.PositiveIntegerField('参会人数', default=0)
key_participants = models.TextField("主要参会领导", null=True, blank=True)
class MroomSlot(BaseModel):
@ -19,7 +43,293 @@ class MroomSlot(BaseModel):
booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b")
mdate = models.DateField('会议日期', db_index=True)
slot = models.PositiveIntegerField('时段', help_text='0-47')
is_inuse = models.BooleanField('是否占用', default=True)
# class Seal(BaseModel):
# """TN: 印章类型"""
# name = models.CharField('印章名称', max_length=50, unique=True)
class LendingSeal(CommonBDModel):
"""TN: 印章外出用印信息"""
seal = models.JSONField('印章信息',default=list ,help_text='[公章,法人章,财务章,合同章,业务章,其他章]')
seal_other = models.CharField('其他印章', max_length=50, blank=True, null=True)
filename = models.TextField('文件名称')
file = models.TextField('文件内容')
file_count = models.PositiveIntegerField('用印份数')
is_lending= models.BooleanField('是否借出', default=False)
contacts = models.CharField('联系方式', max_length=50, validators=[phone_validator], blank=True, null=True)
lending_date = models.DateField('借出日期', blank=True, null=True)
return_date = models.DateField('拟归还日期', blank=True, null=True)
actual_return_date = models.DateField('实际归还日期', blank=True, null=True)
reason = models.CharField('借用理由', max_length=100, blank=True, null=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False)
note = models.TextField('备注', null=True, blank=True)
class Vehicle(CommonBDModel):
"""TN: 用车申请"""
start_time = models.DateField('出车时间', blank=True, null=True)
end_time = models.DateField('还车时间', blank=True, null=True)
location = models.CharField('出发地点', null=True, blank=True, max_length=100)
via = models.CharField('途经地点', null=True, blank=True, max_length=100)
destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
start_km = models.PositiveIntegerField('出发公里数')
end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True)
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
is_city = models.BooleanField('是否市内用车', default=True)
reason = models.CharField('用车事由', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False)
def save(self, *args, **kwargs):
if self.end_km:
if self.start_km <= self.end_km:
self.actual_km = self.end_km - self.start_km
else:
raise ParseError('归还公里数不能小于出发公里数')
else:
self.actual_km = 0
return super().save(*args, **kwargs)
class FileRecord(CommonBDModel):
"""TN: 档案台账"""
name = models.CharField('资料名称', max_length=100)
number = models.CharField('档案编号', max_length=50, null=True, blank=True)
counts = models.CharField('文件份数', max_length=10, null=True, blank=True)
location = models.CharField('存放位置', max_length=100, null=True, blank=True)
contacts = models.CharField('存档人电话', max_length=50, validators=[phone_validator], blank=True, null=True)
reciver = models.CharField('接收人(综合办)', max_length=50, null=True, blank=True)
remark = models.TextField('备注', max_length=200, null=True, blank=True)
class BorrowRecord(CommonBDModel):
"""TN: 借阅、复印、查阅记录"""
borrow_file = models.ManyToManyField(FileRecord, related_name="borrow_records")
borrow_date = models.DateField('借阅日期', null=True, blank=True)
return_date = models.DateField('归还日期', null=True, blank=True)
contacts = models.CharField('借阅人电话', max_length=50, validators=[phone_validator], null=True, blank=True)
remark = models.JSONField('用途', default=list, help_text=['借阅', '复印', '查阅'])
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='borrow_ticket', null=True, blank=True, db_constraint=False)
class Publicity(CommonBDModel):
"""TN: 公示栏"""
number = models.CharField('记录编号', max_length=50, blank=True, null=True)
title = models.CharField('送审稿件标题', max_length=100)
participants = models.CharField('所有撰稿人', max_length=50)
pub_dept = models.CharField('部室/研究院', null=True, blank=True, max_length=50)
pfile = models.CharField('稿件路径', null=True, blank=True, max_length=100)
level = models.JSONField('涉密等级', default=list, help_text=['重要', '一般', '非涉密'])
content = models.JSONField('稿件内容涉及', default=list, help_text=[
"武器装备科研生产综合事项",
"其它"
])
other_content = models.CharField('其它内容', max_length=100, blank=True, null=True)
report_purpose = models.CharField('宣传报道目的', max_length=100, blank=True, null=True)
channel = models.JSONField('发布渠道', default=list, help_text=['互联网', '信息平台', '官微', '公开发行物', '其它'])
other_channel = models.CharField('其它渠道', max_length=50, blank=True, null=True)
report_name = models.CharField('报道名称', max_length=50, blank=True, null=True)
review = models.JSONField('第一撰稿人自审', default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True,blank=True)
dept_opinion = models.JSONField('部门负责人意见', default=list, help_text=['同意', '不同意'], null=True, blank=True)
secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
# 记录编号自动生成
def save(self, *args, **kwargs):
if not self.number:
last_number = self.__class__.objects.filter(number__startswith=f"GXKG-{datetime.now().year}-").order_by('-number').first()
if last_number:
try:
last_num = int(last_number.number.split('-')[-1])
except ValueError:
last_num = 0
else:
last_num =0
# 格式化编号,带补零
self.number = f"GXKG-{datetime.now().year}-{last_num+1:02d}"
super().save(*args, **kwargs)
class PatentInfo(CommonBDModel):
"""TN: 专利申密审批表单样式"""
PATENT_TYPE_CHOICES = (
('invention', '发明专利'),
('utility', '实用新型专利'),
('design', '外观设计专利'),
)
APPLY_AREAS = (
('Domestic', '国内申请'),
('Foreign', '国外申请'),
(' PCT', 'PCT申请'),
)
name = models.CharField('拟申请专利名称', max_length=100)
author = models.CharField('发明人(设计人)', max_length=100)
type = models.CharField('专利类型', max_length=50, choices=PATENT_TYPE_CHOICES, default='invention')
is_public = models.BooleanField('是否公开', default=False)
area = models.CharField('拟申请地域', max_length=50, choices=APPLY_AREAS, default='Domestic')
identified = models.BooleanField('是否进行过科技成果鉴定', default=False)
published_article = models.BooleanField('是否发表过文章', default=False)
exhibited = models.BooleanField('是否参与过展会展出', default=False)
applied_to_production = models.BooleanField('是否参与应用于生产/销售', default=False)
participated_in_exchange = models.BooleanField('是否参与过技术交流', default=False)
tech_background_pages = models.PositiveIntegerField('技术背景材料页数', null=True, blank=True)
tech_disclosure_pages = models.PositiveIntegerField('技术交底材料页数', null=True, blank=True)
novelty_report_pages = models.PositiveIntegerField('查新检索报告页数', null=True, blank=True)
diagrams_or_photos_pages = models.PositiveIntegerField('图/照片页数或张数', null=True, blank=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='patentInfo_ticket', null=True, blank=True, db_constraint=False)
# class PaperOfm(CommonADModel):
# """TN: 论文申密审批表单"""
# PAPER_TYPE_CHOICES = (
# ('research', '研究论文'),
# ('comprehensive', '综合'),
# )
# name = models.CharField('拟申请专利名称', max_length=100)
# author = models.CharField('发明人(设计人)', max_length=100)
# paper_type = models.CharField('论文类型', max_length=50, choices=PAPER_TYPE_CHOICES, default='research')
# is_chinese_core = models.BooleanField('是否为中文核心', default=False)
# is_sci = models.BooleanField('是否被SCI/EI收录', default=False)
# has_appraisal = models.BooleanField('是否进行过科技成果鉴定', default=False)
# has_published_article = models.BooleanField('是否发表过文章', default=False)
# has_exhibited = models.BooleanField('是否参与过展会展出', default=False)
# has_applied_in_production = models.BooleanField('是否参与应用于生产/销售', default=False)
# has_technical_exchange = models.BooleanField('是否参与过技术交流', default=False)
# paper_page_count = models.PositiveIntegerField('论文页数', null=True, blank=True)
# image_count = models.PositiveIntegerField('图/照片张数', null=True, blank=True)
# class Platform(CommonADModel):
# name = models.CharField(max_length=100)
# def __str__(self):
# return self.name
# class Project(CommonADModel):
# name = models.CharField(max_length=100)
# def __str__(self):
# return self.name
class Meta:
unique_together = ('mroom', 'mdate', 'slot')
# class PatentRecord(CommonADModel):
# """TN: 专利台账登记"""
# volume_number = models.CharField(max_length=50, null=True, blank=True, verbose_name="卷号")
# application_number = models.CharField(max_length=50, verbose_name="申请号(交局后补登)")
# title = models.CharField(max_length=255, verbose_name="名称")
# patent_type = models.CharField(
# max_length=20,
# choices=[
# ("invention", "发明"),
# ("utility_model", "实用新型"),
# ("design", "外观设计")
# ],
# verbose_name="专利类型"
# )
# organization = models.CharField(max_length=100, verbose_name="单位")
# inventors = models.CharField(max_length=255, verbose_name="发明人")
# agent = models.CharField(max_length=255, null=True, blank=True, verbose_name="代理人")
# affiliated_platforms = models.ManyToManyField('Platform', blank=True, verbose_name="归属平台")
# affiliated_projects = models.ManyToManyField('Project', blank=True, verbose_name="归属项目")
# application_date = models.DateField(null=True, blank=True, verbose_name="申请日")
# authorization_date = models.DateField(null=True, blank=True, verbose_name="授权日")
# validity_years = models.IntegerField(null=True, blank=True, verbose_name="有效年限(年)")
# annuity_paid = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,verbose_name="年费缴纳")
# status = models.CharField(
# max_length=20,
# choices=[
# ("not_disclosed", "未公开"),
# ("under_examination", "实审中"),
# ("first_office_action", "一通"),
# ("second_office_action", "二通"),
# ("rejected", "驳回"),
# ("reexamination", "复审"),
# ("authorized", "授权")
# ],
# verbose_name="状态"
# )
# award_info = models.TextField(null=True, blank=True, verbose_name="报奖情况")
# bonus_amount = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,verbose_name="奖金金额(元)")
# class PaperRecord(models.Model):
# """TN: 论文台账登记"""
# index = models.PositiveIntegerField(verbose_name="序号")
# paper_code = models.CharField(max_length=100, blank=True, null=True, verbose_name="论文编号(投稿后补登)")
# title = models.CharField(max_length=255, verbose_name="名称")
# paper_type = models.CharField(max_length=100, verbose_name="论文类型")
# affiliation = models.CharField(max_length=255, verbose_name="单位")
# authors = models.CharField(max_length=255, verbose_name="作者")
# corresponding_author = models.CharField(max_length=255, blank=True, null=True, verbose_name="通讯作者")
# affiliated_platforms = models.ManyToManyField('Platform', blank=True, verbose_name="归属平台")
# affiliated_projects = models.ManyToManyField('Project', blank=True, verbose_name="归属项目")
# acceptance_date = models.DateField(blank=True, null=True, verbose_name="接受日期")
# publication_date = models.DateField(blank=True, null=True, verbose_name="发表日期")
# page_fee_paid = models.DecimalField(
# max_digits=10,
# decimal_places=2,
# blank=True,
# null=True,
# verbose_name="版面费缴纳"
# )
# status = models.CharField(
# max_length=50,
# choices=[
# ("under_review", "审稿中"),
# ("revise_1", "一修"),
# ("revise_2", "二修"),
# ("accepted", "接收"),
# ("published", "发表")
# ],
# default="under_review", verbose_name="状态"
# )
# award_status = models.CharField(max_length=255, blank=True, null=True, verbose_name="报奖情况")
# bonus_amount = models.DecimalField(max_digits=10,decimal_places=2,blank=True,null=True,verbose_name="奖金发放")
# class ProjectApproval(CommonBDModel):
# """TN: 立项审批表"""
# project_start_date = models.DateField("立项日期", null=True, blank=True)
# is_self_initiated = models.BooleanField("自立项目", default=False)
# is_city_level = models.BooleanField("市级项目", default=False)
# is_province_level = models.BooleanField("省级项目", default=False)
# construction_period = models.CharField("建设期", max_length=100, null=True, blank=True)
# project_members = models.TextField("项目组员", null=True, blank=True)
# project_budget = models.DecimalField("项目预算(万元)", max_digits=12, decimal_places=2, null=True, blank=True)
# project_description = models.TextField("项目基本情况", null=True, blank=True)
# project_performance = models.TextField("目标绩效", null=True, blank=True)
# class ProjectInfo(CommonBDModel):
# """TN: 项目信息表
# """
# serial_number = models.CharField("序号", max_length=50, null=True, blank=True)
# red_head_doc_no = models.CharField("红头发文号/公示页", max_length=100, null=True, blank=True)
# name = models.CharField("名称", max_length=200, null=True, blank=True)
# project_type = models.CharField("项目类型", max_length=100, null=True, blank=True)
# platform = models.CharField("所属平台", max_length=100, null=True, blank=True)
# project_source = models.CharField("项目来源", max_length=100, null=True, blank=True)
# construction_period = models.CharField("建设期", max_length=100, null=True, blank=True)
# project_funding = models.DecimalField("项目资金(财政与自筹)", max_digits=15, decimal_places=2, null=True, blank=True)
# support_period = models.CharField("项目支持期", max_length=100, null=True, blank=True)
# undertaking_unit = models.CharField("承担单位", max_length=200, null=True, blank=True)
# responsible_person = models.CharField("负责人", max_length=50, null=True, blank=True)
# project_members = models.TextField("项目人员", null=True, blank=True)
# milestone = models.TextField("里程碑节点", null=True, blank=True)
# mid_term_status = models.TextField("项目中期情况", null=True, blank=True)
# acceptance_status = models.TextField("项目验收情况", null=True, blank=True)
# sci_tech_achievements = models.TextField("科技成果", null=True, blank=True)

View File

@ -1,9 +1,11 @@
from .models import Mroom, MroomBooking, MroomSlot
from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo)
# Publicity, PatetInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
from apps.utils.serializers import CustomModelSerializer
from rest_framework import serializers
from django.db import transaction
from rest_framework.exceptions import ParseError
from apps.utils.constants import EXCLUDE_FIELDS
from apps.wf.serializers import TicketSimpleSerializer
class MroomSerializer(CustomModelSerializer):
@ -17,28 +19,30 @@ class MroomBookingSerializer(CustomModelSerializer):
mdate = serializers.DateField(write_only=True, label="预订日期")
slots = serializers.ListField(child=serializers.IntegerField(), write_only=True, label="时段索引")
create_by_name = serializers.CharField(source='create_by.username', read_only=True)
create_by_phone = serializers.CharField(source='create_by.phone', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = MroomBooking
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
extra_kwargs = {'belong_dept': {'required': True}}
@transaction.atomic
def create(self, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = MroomBooking.objects.create(**validated_data)
booking = super().create(validated_data)
MroomSlot.objects.filter(booking=booking).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
try:
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom)
except Exception as e:
raise ParseError(f"时段已预订,请刷新重选-{e}")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
if ms_exists:
raise ParseError("时段已预订,请刷新重选")
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking
@transaction.atomic
def update(self, instance, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
@ -48,17 +52,142 @@ class MroomBookingSerializer(CustomModelSerializer):
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
try:
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom)
except Exception as e:
raise ParseError(f"时段已预订,请刷新重选-{e}")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
if ms_exists:
raise ParseError("时段已预订,请刷新重选")
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking
class MroomSlotSerializer(CustomModelSerializer):
booking_title = serializers.CharField(source='booking.title', read_only=True)
class Meta:
model = MroomSlot
fields = '__all__'
class LendingSealSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = LendingSeal
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class VehicleSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Vehicle
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['actual_km']
class FileRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = FileRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class BorrowRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
borrow_file = serializers.PrimaryKeyRelatedField(queryset=FileRecord.objects.all(), many=True, write_only=True, label="借阅文件")
file_detail = FileRecordSerializer(source='borrow_file', many=True, read_only=True, label="借阅文件详情")
file_name = serializers.SerializerMethodField()
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = BorrowRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
def get_file_name(self, obj):
return [file.name for file in obj.borrow_file.all()]
class PublicitySerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Publicity
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class PatentInfoSerializer(CustomModelSerializer):
class Meta:
model = PatentInfo
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
# class PaperSerializer(CustomModelSerializer):
# class Meta:
# model = PaperOfm
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class PlatformSerializer(serializers.ModelSerializer):
# class Meta:
# model = Platform
# fields = ['id', 'name']
# class ProjectSerializer(serializers.ModelSerializer):
# class Meta:
# model = Project
# fields = ['id', 'name']
# class ProjectMemberSerializer(CustomModelSerializer):
# affiliated_platforms = serializers.PrimaryKeyRelatedField(
# many=True,
# queryset=Platform.objects.all(),
# write_only=True
# )
# affiliated_platforms_detail = PlatformSerializer(
# source='affiliated_platforms', many=True, read_only=True
# )
# affiliated_projects = serializers.PrimaryKeyRelatedField(
# many=True,
# queryset=Project.objects.all(),
# write_only=True
# )
# affiliated_projects_detail = ProjectSerializer(
# source='affiliated_projects', many=True, read_only=True
# )
# class Meta:
# model = PatentRecord
# fields = '__all__'
# class PaperRecordSerializer(CustomModelSerializer):
# class Meta:
# model = PaperRecord
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class ProjectApprovalSerializer(CustomModelSerializer):
# class Meta:
# model = ProjectApproval
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class ProjectInfoSerializer(CustomModelSerializer):
# class Meta:
# model = ProjectInfo
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS

155
apps/ofm/services.py Normal file
View File

@ -0,0 +1,155 @@
from apps.wf.models import Ticket
# TicketFlow, Transition, Workflow, CustomField, State,
from apps.ofm.models import LendingSeal, Vehicle, BorrowRecord, Publicity, MroomBooking, MroomSlot, PatentInfo
from rest_framework.exceptions import ParseError
def seal_submit_validate(ins: LendingSeal):
if ins.submit_time:
raise ParseError('该日志已提交!')
if ins.mtask and ins.mtask.state == LendingSeal.MTASK_STOP:
raise ParseError('该任务已停止!')
def bind_mroombooking(ticket: Ticket, transition, new_ticket_data: dict):
ins = MroomBooking.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'mroombooking',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def mroombooking_reject(ticket: Ticket, transition, new_ticket_data: dict):
ins = MroomBooking.objects.get(id=new_ticket_data['t_id'])
MroomSlot.objects.filter(booking=ins).update(is_inuse=False)
def bind_lendingseal(ticket: Ticket, transition, new_ticket_data: dict):
ins = LendingSeal.objects.get(id=new_ticket_data['t_id'])
ins.actual_return_date = None
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'LendingSeal',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
# 如果驳回到开始状态
def lending_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = LendingSeal.objects.get(id=new_ticket_data['t_id'])
except LendingSeal.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_vehicle(ticket: Ticket, transition, new_ticket_data: dict):
ins = Vehicle.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'Vehicle',
't_id': ins.id,
})
ins.actual_km = None
ins.end_time = None
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def vehicle_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = Vehicle.objects.get(id=new_ticket_data['t_id'])
except Vehicle.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_file(ticket: Ticket, transition, new_ticket_data: dict):
ins = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'BorrowRecord',
't_id': ins.id,
})
ins.return_date = None
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def file_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
except BorrowRecord.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_publicity(ticket: Ticket, transition, new_ticket_data: dict):
ins = Publicity.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'publicity',
't_id': ins.id,
})
ins.dept_opinion = None
ins.secret_period = None
ins.dept_opinion_review = None
ins.publicity_opinion = None
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = Publicity.objects.get(id=new_ticket_data['t_id'])
except Publicity.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_patent(ticket: Ticket, transition, new_ticket_data: dict):
ins = PatentInfo.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'patent',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()

View File

@ -1,6 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet)
from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet,
FileborrowViewSet, PublicityViewSet, PatentInfoViewSet)
# SealModelViewSet,
# , PublicityViewSet, , PaperViewSet, PlatformViewSet,
# ProjectViewSet, PatentRecordViewSet, PaperRecordViewSet, ProjectApprovalViewSet, ProjectInfoViewSet)
API_BASE_URL = 'api/ofm/'
HTML_BASE_URL = 'dhtml/ofm/'
@ -9,6 +13,20 @@ router = DefaultRouter()
router.register('mroom', MroomViewSet, basename='mroom')
router.register('mroombooking', MroomBookingViewSet, basename='mroombooking')
router.register('mroomslot', MroomSlotViewSet, basename='mroomslot')
# router.register('sealmanage', SealManageViewSet, basename='sealmanage')
router.register('lendingseal', LendingSealViewSet, basename='lendingseal')
router.register('vehicle', VehicleViewSet, basename='vehicle')
router.register('filerecord', FilerecordViewSet, basename='filerecord')
router.register('fileborrow', FileborrowViewSet, basename='fileborrow')
router.register('publicity', PublicityViewSet, basename='publicity')
router.register('patentinfo', PatentInfoViewSet, basename='patentinfo')
# router.register('paper', PaperViewSet, basename='paper')
# router.register('platform', PlatformViewSet, basename='platform')
# router.register('project', ProjectViewSet, basename='project')
# router.register('patentrecord', PatentRecordViewSet, basename='patentrecord')
# router.register('paperrecord', PaperRecordViewSet, basename='paperrecord')
# router.register('projectapproval', ProjectApprovalViewSet, basename='projectapproval')
# router.register('projectinfo', ProjectInfoViewSet, basename='projectinfo')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,11 +1,16 @@
from django.shortcuts import render
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
from .models import Mroom, MroomBooking, MroomSlot
from .serializers import MroomSerializer, MroomBookingSerializer, MroomSlotSerializer
from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo
# Publicity, , PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer,
VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer, PatentInfoSerializer)
# ,SealSerializer,
# LendingSealSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer,
# PatentInfoSerializer, PaperSerializer, PlatformSerializer, ProjectSerializer, ProjectMemberSerializer, PaperRecordSerializer, ProjectApprovalSerializer, ProjectInfoSerializer)
from rest_framework.decorators import action
from apps.utils.mixins import CustomListModelMixin
from rest_framework.exceptions import ParseError
from apps.ofm.filters import MroomBookingFilterset
from apps.ofm.filters import MroomBookingFilterset, SealFilter, BorrowRecordFilter
class MroomViewSet(CustomModelViewSet):
@ -24,7 +29,7 @@ class MroomBookingViewSet(CustomModelViewSet):
"""
queryset = MroomBooking.objects.all()
serializer_class = MroomBookingSerializer
select_related_fields = ["create_by"]
select_related_fields = ["create_by", "ticket", "belong_dept"]
filterset_class = MroomBookingFilterset
def add_info_for_list(self, data):
@ -62,7 +67,7 @@ class MroomBookingViewSet(CustomModelViewSet):
start_time = self._slot_to_time(info["current_slots"][0])
end_time = self._slot_to_time(info["current_slots"][-1] + 1)
info["time_ranges"].append(f"{start_time}-{end_time}")
info.pop("current_slots") # 清理临时数据
info["slots"] = info.pop("current_slots") # 清理临时数据
for item in data:
item.update(booking_info.get(item["id"], {}))
@ -77,21 +82,178 @@ class MroomBookingViewSet(CustomModelViewSet):
def perform_update(self, serializer):
ins:MroomBooking = self.get_object()
if ins.create_by != self.request.user:
ticket = ins.ticket
if ticket is None or ticket.state.type == 1:
pass
else:
raise ParseError("存在审批单,不允许修改")
if ins.create_by and ins.create_by != self.request.user:
raise ParseError("只允许创建者修改")
return super().perform_update(serializer)
def perform_destroy(self, instance):
if instance.create_by != self.request.user:
if instance.create_by and instance.create_by != self.request.user:
raise ParseError("只允许创建者删除")
return super().perform_destroy(instance)
ticket = instance.ticket
if ticket is None or ticket.state.type == 1:
pass
else:
raise ParseError("存在审批单,不允许删除")
if ticket:
ticket.delete()
instance.delete()
class MroomSlotViewSet(CustomListModelMixin, CustomGenericViewSet):
"""list: 会议室预订时段
"""list:
会议室预订时段
"""
queryset = MroomSlot.objects.all()
serializer_class = MroomSlotSerializer
filterset_fields = ["mroom", "mdate", "booking"]
filterset_fields = ["mroom", "mdate", "booking", "is_inuse"]
class LendingSealViewSet(CustomModelViewSet):
"""list: 印章外出
印章外出
"""
perms_map = {'get': '*', 'post': 'seal.update',
'put': 'seal.update', 'delete': 'seal.delete'}
queryset = LendingSeal.objects.all()
serializer_class = LendingSealSerializer
filterset_class = SealFilter
ordering = ["-create_time"]
data_filter = True
class VehicleViewSet(CustomModelViewSet):
"""list: 车辆
车辆
"""
queryset = Vehicle.objects.all()
serializer_class = VehicleSerializer
ordering = ["-create_time"]
class FilerecordViewSet(CustomModelViewSet):
"""list: 文件
文件
"""
queryset = FileRecord.objects.all()
serializer_class = FileRecordSerializer
filterset_fields = [ "name", "number"]
ordering = ["-create_time", "number", "name"]
class FileborrowViewSet(CustomModelViewSet):
"""list: 文件借阅
文件借阅
"""
queryset = BorrowRecord.objects.all()
serializer_class = BorrowRecordSerializer
filterset_class = BorrowRecordFilter
ordering = ["-create_time"]
class PublicityViewSet(CustomModelViewSet):
"""list: 公告
公告
"""
queryset = Publicity.objects.all()
serializer_class = PublicitySerializer
filterset_fields = ["title","number"]
ordering = ["-create_time", "number"]
class PatentInfoViewSet(CustomModelViewSet):
"""list: 专利
专利
"""
queryset = PatentInfo.objects.all()
serializer_class = PatentInfoSerializer
filterset_fields = ["name", "author", "type"]
ordering = ["-create_time", "name", "author", "type"]
# class PaperViewSet(CustomModelViewSet):
# """list: 论文申密审批
# 论文申密审批
# """
# queryset = PaperOfm.objects.all()
# serializer_class = PaperSerializer
# filterset_fields = ["name", "author"]
# ordering = ["create_time", "name"]
# class PlatformViewSet(CustomModelViewSet):
# """list: 平台
# 平台
# """
# queryset = Platform.objects.all()
# serializer_class = PlatformSerializer
# filterset_fields = ["name"]
# ordering = ["create_time", "name"]
# class ProjectViewSet(CustomModelViewSet):
# """list: 项目
# 项目
# """
# queryset = Project.objects.all()
# serializer_class = ProjectSerializer
# filterset_fields = ["name"]
# ordering = ["create_time", "name"]
# class PatentRecordViewSet(CustomModelViewSet):
# """list: 专利台账登记
# 专利台账登记
# """
# queryset = PatentRecord.objects.all()
# serializer_class = ProjectMemberSerializer
# filterset_fields = ["patent", "type"]
# ordering = ["create_time", "patent", "type"]
# class PaperRecordViewSet(CustomModelViewSet):
# """list: 论文台账登记
# 论文台账登记
# """
# queryset = PaperRecord.objects.all()
# serializer_class = ProjectMemberSerializer
# filterset_fields = ["index", "title", "paper_code","paper_type", "authors"]
# ordering = ["create_time", "paper", "type"]
# class ProjectApprovalViewSet(CustomModelViewSet):
# """list: 立项审批表
# 立项审批表
# """
# queryset = ProjectApproval.objects.all()
# serializer_class = ProjectApprovalSerializer
# filterset_fields = ["project_start_date"]
# ordering = ["project_start_date"]
# class ProjectInfoViewSet(CustomModelViewSet):
# """list: 项目信息
# 项目信息
# """
# queryset = ProjectInfo.objects.all()
# serializer_class = ProjectInfoSerializer
# filterset_fields = ["serial_number", "name", "platform", "project_source"]
# ordering = ["serial_number", "name"]

View File

@ -31,7 +31,6 @@ class UtaskSerializer(CustomModelSerializer):
"priority": {"required": False, "allow_null": True},
}
@transaction.atomic
def create(self, validated_data):
if not validated_data.get('number', None):
validated_data["number"] = Utask.get_a_number()

View File

@ -408,11 +408,14 @@ class PmService:
mtask.submit_time = now
mtask.submit_user = user
mtask.save()
utask = mtask.utask
if utask:
cls.utask_submit(utask, raise_e=False)
else:
raise ParseError('该任务状态不可提交')
@classmethod
def utask_submit(cls, utask: Utask):
def utask_submit(cls, utask: Utask, raise_e=True):
"""
生产大任务提交
"""
@ -420,4 +423,5 @@ class PmService:
utask.state = Utask.UTASK_SUBMIT
utask.save()
else:
raise ParseError('存在子任务未提交')
if raise_e:
raise ParseError('存在子任务未提交')

26
apps/pm/tasks.py Normal file
View File

@ -0,0 +1,26 @@
# Create your tasks here
from __future__ import absolute_import, unicode_literals
from apps.pm.models import Mtask, Utask
from apps.utils.tasks import CustomTask
from celery import shared_task
from datetime import datetime, timedelta
from django.db.models import F
from apps.pm.services import PmService
@shared_task(base=CustomTask)
def complete_mtask():
"""
将2天前未提交的任务且数量已达标的任务标记为已完成
"""
now = datetime.now().date()
Mtask.objects.filter(state=Mtask.MTASK_ASSGINED,
end_date__lte=now-timedelta(days=2),
count_ok__gte=F('count')).update(state=Mtask.MTASK_SUBMIT)
Mtask.objects.filter(state=Mtask.MTASK_ASSGINED,
end_date__lte=now-timedelta(days=7)).update(state=Mtask.MTASK_SUBMIT)
utasks = Utask.objects.filter(state__in=[Utask.UTASK_ASSGINED, Utask.UTASK_WORKING],
end_date__lte=now-timedelta(days=2))
for utask in utasks:
PmService.utask_submit(utask=utask, raise_e=False)

View File

@ -43,7 +43,6 @@ class PuPlanItemSerializer(CustomModelSerializer):
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['pu_order']
@transaction.atomic
def create(self, validated_data):
pu_plan = validated_data['pu_plan']
if pu_plan.state != PuPlan.PUPLAN_CREATE:
@ -65,7 +64,6 @@ class PuPlanItemSerializer(CustomModelSerializer):
else:
validated_data['belong_dept'] = belong_dept
@transaction.atomic
def update(self, instance, validated_data):
validated_data.pop('pu_plan')
pu_plan = instance.pu_plan
@ -114,7 +112,6 @@ class PuOrderItemSerializer(CustomModelSerializer):
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count']
@transaction.atomic
def create(self, validated_data):
pu_order = validated_data['pu_order']
material = validated_data['material']
@ -126,7 +123,6 @@ class PuOrderItemSerializer(CustomModelSerializer):
PumService.cal_pu_order_total_price(pu_order)
return ins
@transaction.atomic
def update(self, instance, validated_data):
validated_data.pop('material')
validated_data.pop('pu_order')

View File

@ -80,7 +80,6 @@ class PuPlanItemViewSet(CustomModelViewSet):
ordering_fields = ['create_time', 'material', 'need_date', 'need_count']
ordering = ['create_time']
@transaction.atomic
def perform_destroy(self, instance):
user = self.request.user
pu_plan = instance.pu_plan
@ -104,7 +103,6 @@ class PuOrderViewSet(CustomModelViewSet):
search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model']
select_related_fields = ['create_by', 'update_by', 'supplier']
@transaction.atomic
def perform_destroy(self, instance):
if instance.state != PuOrder.PUORDER_CREATE:
raise ParseError('采购订单非创建中不可删除')
@ -145,7 +143,6 @@ class PuOrderItemViewSet(CustomModelViewSet):
filterset_fields = ['material', 'pu_order']
ordering = ['create_time']
@transaction.atomic
def perform_destroy(self, instance):
pu_order = instance.pu_order
if pu_order.state != PuOrder.PUORDER_CREATE:

View File

@ -46,8 +46,8 @@ class FtestWorkFilter(filters.FilterSet):
fields = {
"material__process__name": ["exact", "contains"],
"material": ["exact"],
"wm": ["exact"],
"mb": ["exact"],
"wm": ["exact", "isnull"],
"mb": ["exact", "isnull"],
"batch": ["exact"],
"type": ["exact"],
"type2": ["exact"],

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-09-05 01:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qm', '0052_auto_20250718_1558'),
]
operations = [
migrations.AlterField(
model_name='ftest',
name='is_ok',
field=models.BooleanField(default=True, verbose_name='是否合格'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-10-10 01:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qm', '0053_alter_ftest_is_ok'),
]
operations = [
migrations.AlterField(
model_name='ptest',
name='val_xj',
field=models.CharField(blank=True, choices=[('S', '析晶'), ('R', '不析晶'), ('θ', '未化')], help_text=[('S', '析晶'), ('R', '不析晶'), ('θ', '未化')], max_length=10, null=True, verbose_name='析晶'),
),
]

View File

@ -337,7 +337,7 @@ class Ftest(CommonBDModel):
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user')
check_user = models.ForeignKey(
User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True)
is_ok = models.BooleanField('是否合格', default=False)
is_ok = models.BooleanField('是否合格', default=True)
note = models.TextField('备注', default='', blank=True)
ftest_work = models.ForeignKey(
FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True)
@ -353,13 +353,12 @@ class Ftest(CommonBDModel):
@classmethod
def init_by_qct(cls, qct, test_user, test_date):
with transaction.atomic():
ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date)
for testitem in qct.testitems.all():
FtestItem.objects.create(ftest=ftest, testitem=testitem)
for defect in qct.defects.all():
FtestDefect.objects.create(ftest=ftest, defect=defect)
return ftest
ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date)
for testitem in qct.testitems.all():
FtestItem.objects.create(ftest=ftest, testitem=testitem)
for defect in qct.defects.all():
FtestDefect.objects.create(ftest=ftest, defect=defect)
return ftest
# @classmethod
# def cal_count_notok(cls, ftestwork: FtestWork):
@ -446,7 +445,7 @@ class Ptest(CommonAModel):
val_tg = models.FloatField("Tg", help_text='', null=True, blank=True)
val_tf = models.FloatField("Tf", help_text='', null=True, blank=True)
val_xj = models.CharField(
'析晶', max_length=10, default='S', choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS))
'析晶', max_length=10, null=True, blank=True, choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS))
val_pzxs = models.FloatField(
'膨胀系数', help_text='30-300℃', null=True, blank=True)
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)

View File

@ -215,34 +215,33 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer):
def create(self, validated_data):
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
with transaction.atomic():
ins: FtestWork = super().create(validated_data)
for ftestworkdefect in ftestworkdefect:
if ftestworkdefect['count'] > 0:
FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect)
if ins.qct or ftestworkdefect:
ins.cal_count()
ins: FtestWork = super().create(validated_data)
for ftestworkdefect in ftestworkdefect:
if ftestworkdefect['count'] > 0:
FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect)
if ins.qct or ftestworkdefect:
ins.cal_count()
return ins
def update(self, instance, validated_data):
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
ins:FtestWork = super().update(instance, validated_data)
with transaction.atomic():
fd_ids = []
for item in ftestworkdefect:
if item["count"] > 0:
try:
ins = FtestworkDefect.objects.get(ftestwork=ins, defect=item["defect"])
except FtestworkDefect.DoesNotExist:
raise ParseError("新的缺陷项!")
except FtestworkDefect.MultipleObjectsReturned:
raise ParseError("缺陷项重复!")
ins.count = item["count"]
ins.save()
fd_ids.append(ins.id)
FtestworkDefect.objects.filter(ftestwork=ins).exclude(id__in=fd_ids).delete()
if ins.qct or ftestworkdefect:
ins.cal_count()
fd_ids = []
for item in ftestworkdefect:
if item["count"] > 0:
try:
ins = FtestworkDefect.objects.get(ftestwork=ins, defect=item["defect"])
except FtestworkDefect.DoesNotExist:
raise ParseError("新的缺陷项!")
except FtestworkDefect.MultipleObjectsReturned:
raise ParseError("缺陷项重复!")
ins.count = item["count"]
ins.save()
fd_ids.append(ins.id)
FtestworkDefect.objects.filter(ftestwork=ins).exclude(id__in=fd_ids).delete()
if ins.qct or ftestworkdefect:
ins.cal_count()
return ins
class FtestWorkSerializer(CustomModelSerializer):
@ -292,24 +291,24 @@ class FtestSerializer(CustomModelSerializer):
def create(self, validated_data):
ftestitems = validated_data.pop('ftestitems', [])
with transaction.atomic():
instance = super().create(validated_data)
for item in ftestitems:
FtestItem.objects.create(ftest=instance, **item)
instance = super().create(validated_data)
for item in ftestitems:
FtestItem.objects.create(ftest=instance, **item)
return instance
def update(self, instance, validated_data):
validated_data.pop('ftest_work', None)
ftestitems = validated_data.pop('ftestitems', [])
with transaction.atomic():
instance = super().update(instance, validated_data)
for item in ftestitems:
id = item.get('id', None)
if id:
ftestitem = FtestItem.objects.get(id=id)
ftestitem.test_val = item['test_val']
ftestitem.check_val = item['check_val']
ftestitem.save()
instance = super().update(instance, validated_data)
for item in ftestitems:
id = item.get('id', None)
if id:
ftestitem = FtestItem.objects.get(id=id)
ftestitem.test_val = item['test_val']
ftestitem.check_val = item['check_val']
ftestitem.save()
return instance
@ -364,75 +363,75 @@ class FtestProcessSerializer(CustomModelSerializer):
def create(self, validated_data):
ftestitems = validated_data.pop('ftestitems', [])
ftestdefects = validated_data.pop('ftestdefects', [])
with transaction.atomic():
instance = super().create(validated_data)
for item in ftestitems:
FtestItem.objects.create(ftest=instance, **item)
is_ok = True
defect_main = None
has_is_main = False
for item2 in ftestdefects:
defect:Defect = item2["defect"]
if defect.okcate in [Defect.DEFECT_NOTOK] and item2["has"]:
is_ok = False
if not has_is_main:
item2["is_main"] = True
has_is_main = True
defect_main = defect
else:
item2["is_main"] = False
FtestDefect.objects.create(ftest=instance, **item2)
if not is_ok:
instance.defect_main = defect_main
else:
instance.defect_main = None
instance.is_ok = is_ok
instance.save()
instance = super().create(validated_data)
for item in ftestitems:
FtestItem.objects.create(ftest=instance, **item)
is_ok = True
defect_main = None
has_is_main = False
for item2 in ftestdefects:
defect:Defect = item2["defect"]
if defect.okcate in [Defect.DEFECT_NOTOK] and item2["has"]:
is_ok = False
if not has_is_main:
item2["is_main"] = True
has_is_main = True
defect_main = defect
else:
item2["is_main"] = False
FtestDefect.objects.create(ftest=instance, **item2)
if not is_ok:
instance.defect_main = defect_main
else:
instance.defect_main = None
instance.is_ok = is_ok
instance.save()
return instance
def update(self, instance, validated_data):
ftestitems = validated_data.pop('ftestitems', [])
ftestdefects = validated_data.pop('ftestdefects', [])
with transaction.atomic():
instance = super().update(instance, validated_data)
for item in ftestitems:
try:
ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance)
except FtestItem.DoesNotExist:
ins = FtestItem.objects.create(ftest=instance, **item)
for k, v in item.items():
setattr(ins, k, v)
ins.save()
is_ok = True
defect_main = None
has_is_main = False
for item2 in ftestdefects:
try:
ins:FtestDefect = FtestDefect.objects.get(ftest=instance, defect=item2["defect"])
except FtestDefect.MultipleObjectsReturned:
myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}")
raise ParseError("获取到重复的缺陷项!")
except FtestDefect.DoesNotExist:
ins = FtestDefect.objects.create(ftest=instance, **item2)
for k, v in item2.items():
setattr(ins, k, v)
ins.save()
if ins.is_main:
instance = super().update(instance, validated_data)
for item in ftestitems:
try:
ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance)
except FtestItem.DoesNotExist:
ins = FtestItem.objects.create(ftest=instance, **item)
for k, v in item.items():
setattr(ins, k, v)
ins.save()
is_ok = True
defect_main = None
has_is_main = False
for item2 in ftestdefects:
try:
ins:FtestDefect = FtestDefect.objects.get(ftest=instance, defect=item2["defect"])
except FtestDefect.MultipleObjectsReturned:
myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}")
raise ParseError("获取到重复的缺陷项!")
except FtestDefect.DoesNotExist:
ins = FtestDefect.objects.create(ftest=instance, **item2)
for k, v in item2.items():
setattr(ins, k, v)
ins.save()
if ins.is_main:
has_is_main = True
defect_main = ins.defect
if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]:
is_ok = False
if not has_is_main:
ins.is_main = True
has_is_main = True
defect_main = ins.defect
if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]:
is_ok = False
if not has_is_main:
ins.is_main = True
has_is_main = True
defect_main = ins.defect
else:
ins.is_main = False
ins.save()
if not is_ok:
instance.defect_main = defect_main
else:
instance.defect_main = None
instance.is_ok = is_ok
instance.save()
else:
ins.is_main = False
ins.save()
if not is_ok:
instance.defect_main = defect_main
else:
instance.defect_main = None
instance.is_ok = is_ok
instance.save()
return instance

View File

@ -6,6 +6,7 @@ from django.utils import timezone
from apps.wf.models import Ticket
from apps.qm.models import NotOkOption, Defect
from apps.wpm.services_2 import ana_batch_thread
from apps.inm.models import MaterialBatch
def ftestwork_submit_validate(ins: FtestWork):
wm:WMaterial = ins.wm
@ -23,7 +24,7 @@ def ftestwork_submit_validate(ins: FtestWork):
def ftestwork_submit(ins:FtestWork, user: User):
wm:WMaterial = ins.wm
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
if ins.need_update_wm:
if wm and ins.need_update_wm:
if ins.qct is None and not fwd_qs.exists():
if wm.state == WMaterial.WM_TEST:
# 更新对应的车间库存
@ -99,7 +100,7 @@ def ftestwork_submit(ins:FtestWork, user: User):
else:
wm:WMaterial = ins.wm
# 此时调用了qct表
for item in FtestworkDefect.objects.filter(ftestwork=ins):
for item in fwd_qs:
item:FtestworkDefect = item
if item.count > 0:
wm.count = wm.count - item.count
@ -124,6 +125,32 @@ def ftestwork_submit(ins:FtestWork, user: User):
if not new_create:
wmx.count = wmx.count + item.count
wmx.save()
if ins.mb:
mb:MaterialBatch = ins.mb
for item in fwd_qs:
item:FtestworkDefect = item
if item.count > 0:
mb.count = mb.count - item.count
if mb.count < 0:
raise ParseError("数量不足,扣减失败")
mb.save()
mbstate = WMaterial.WM_OK
if item.defect.okcate == Defect.DEFECT_NOTOK:
mbstate = WMaterial.WM_NOTOK
mbx, new_create = MaterialBatch.objects.get_or_create(
material=mb.material,
warehouse=mb.warehouse,
batch=mb.batch,
defect=item.defect,
state=mbstate,
defaults={
'count': item.count,
}
)
if not new_create:
mbx.count = mbx.count + item.count
mbx.save()
ins.submit_user = user
ins.submit_time = timezone.now()
ins.save()

View File

@ -33,7 +33,6 @@ class DefectViewSet(CustomModelViewSet):
filterset_fields = ["cate", "okcate"]
search_fields = ["name", "code"]
@transaction.atomic
def perform_destroy(self, instance):
QctDefect.objects.filter(defect=instance).delete()
instance.delete()
@ -93,7 +92,6 @@ class QctDefectViewSet(CustomModelViewSet):
filterset_fields = ["qct", "defect"]
ordering = ["qct", "sort"]
@transaction.atomic
def perform_create(self, serializer):
ins: QctDefect = serializer.save()
if ins.is_default:
@ -151,7 +149,6 @@ class TestItemViewSet(CustomModelViewSet):
item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects])
return data
@transaction.atomic
def perform_destroy(self, instance):
QctTestItem.objects.filter(testitem=instance).delete()
instance.delete()
@ -238,19 +235,16 @@ class FtestViewSet(CustomModelViewSet):
ftest_work.count_notok = all_count - ok_count
ftest_work.save()
@transaction.atomic
def perform_create(self, serializer):
ins: Ftest = serializer.save()
if ins.ftest_work:
self.count_sampling(ins.ftest_work)
@transaction.atomic
def perform_update(self, serializer):
ins: Ftest = serializer.save()
if ins.ftest_work:
self.count_sampling(ins.ftest_work)
@transaction.atomic
def perform_destroy(self, instance):
ftest_work = instance.ftest_work
instance.delete()
@ -284,27 +278,32 @@ class FtestWorkViewSet(CustomModelViewSet):
select_related_fields = ['material', 'mb', 'mb__material']
filterset_class = FtestWorkFilter
@transaction.atomic
def update(self, request, *args, **kwargs):
ins:FtestWork = self.get_object()
partial = kwargs.pop('partial', False)
if ins.submit_time is not None:
raise ParseError('已提交无法修改')
if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START:
raise ParseError('审批单已进行,无法修改')
x = super().update(request, *args, **kwargs)
serializer = self.get_serializer(ins, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
# 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch])
return x
return Response(serializer.data)
@transaction.atomic
def destroy(self, request, *args, **kwargs):
ins:FtestWork = self.get_object()
if ins.submit_time is not None:
raise ParseError('已提交无法删除')
if ins.ticket:
raise ParseError('存在审批, 无法删除')
x = super().destroy(request, *args, **kwargs)
self.perform_destroy(ins)
# 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch])
return x
return Response(status=204)
def perform_create(self, serializer):
ins = serializer.save()
@ -322,8 +321,8 @@ class FtestWorkViewSet(CustomModelViewSet):
ins:FtestWork = self.get_object()
if ins.ticket:
raise ParseError('该检验工作存在审批!')
if ins.wm is None:
raise ParseError('该检验工作未关联车间库存')
if ins.wm is None and ins.mb is None:
raise ParseError('该检验工作未关联库存')
if ins.submit_time is None:
ftestwork_submit(ins, request.user)
else:

View File

@ -65,7 +65,6 @@ class OrderViewSet(CustomModelViewSet):
"state": ["exact", "in"],
}
@transaction.atomic
def perform_destroy(self, instance):
if instance.state != Order.ORDER_CREATE:
raise ParseError('订单非创建中不可删除')

View File

@ -275,13 +275,11 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
model = Dept
exclude = EXCLUDE_FIELDS + ['third_info']
@transaction.atomic
def create(self, validated_data):
ins = super().create(validated_data)
sync_dahua_dept(ins)
return ins
@transaction.atomic
def update(self, instance, validated_data):
ins = super().update(instance, validated_data)
sync_dahua_dept(ins)

View File

@ -8,8 +8,7 @@ from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
from django_celery_results.models import TaskResult
from rest_framework.decorators import action
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
ListModelMixin, RetrieveModelMixin)
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.parsers import (JSONParser,
MultiPartParser)
from rest_framework.serializers import Serializer
@ -20,7 +19,7 @@ from apps.hrm.models import Employee
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
from apps.system.filters import DeptFilterSet, UserFilterSet
# from django_q.models import Task as QTask, Schedule as QSchedule
from apps.utils.mixins import (CustomCreateModelMixin, MyLoggingMixin)
from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin)
from django.conf import settings
from apps.utils.permission import ALL_PERMS
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
@ -229,7 +228,7 @@ class PTaskViewSet(CustomModelViewSet):
return Response()
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
"""
list:任务执行结果列表
@ -373,7 +372,7 @@ class RoleViewSet(CustomModelViewSet):
ordering = ['create_time']
class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""岗位/角色关系
岗位/角色关系
@ -385,7 +384,7 @@ class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
filterset_fields = ['post', 'role']
class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""用户/岗位关系
用户/岗位关系
@ -398,37 +397,13 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
ordering = ['sort', 'create_time']
def perform_create(self, serializer):
with transaction.atomic():
instance = serializer.save()
user = instance.user
up = UserPost.objects.filter(user=user).order_by(
'sort', 'create_time').first()
if up:
user.belong_dept = up.dept
user.post = up.post
user.update_by = self.request.user
user.save()
# 更新人员表
ep = Employee.objects.get_queryset(
all=True).filter(user=user).first()
if ep:
ep.belong_dept = user.belong_dept
ep.post = user.post
ep.is_deleted = False
ep.save()
def perform_destroy(self, instance):
with transaction.atomic():
user = instance.user
instance.delete()
up = UserPost.objects.filter(user=user).order_by(
'sort', 'create_time').first()
if up:
user.belong_dept = up.dept
user.post = up.post
else:
user.belong_dept = None
user.post = None
instance = serializer.save()
user = instance.user
up = UserPost.objects.filter(user=user).order_by(
'sort', 'create_time').first()
if up:
user.belong_dept = up.dept
user.post = up.post
user.update_by = self.request.user
user.save()
# 更新人员表
@ -440,6 +415,28 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
ep.is_deleted = False
ep.save()
def perform_destroy(self, instance):
user = instance.user
instance.delete()
up = UserPost.objects.filter(user=user).order_by(
'sort', 'create_time').first()
if up:
user.belong_dept = up.dept
user.post = up.post
else:
user.belong_dept = None
user.post = None
user.update_by = self.request.user
user.save()
# 更新人员表
ep = Employee.objects.get_queryset(
all=True).filter(user=user).first()
if ep:
ep.belong_dept = user.belong_dept
ep.post = user.post
ep.is_deleted = False
ep.save()
class UserViewSet(CustomModelViewSet):
queryset = User.objects.get_queryset(all=True)
@ -610,7 +607,7 @@ class UserViewSet(CustomModelViewSet):
return Response()
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""文件上传
list:
@ -651,7 +648,7 @@ class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, Cu
instance.save()
class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet):
class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet):
perms_map = {'get': '*', 'post': 'apk.upload'}
serializer_class = ApkSerializer
@ -692,7 +689,7 @@ class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSe
return Response()
class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
perms_map = {'get': '*', 'post': '*',
'delete': 'myschedule.delete'}
serializer_class = MyScheduleSerializer
@ -717,7 +714,6 @@ class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, Cus
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
return ''
@transaction.atomic
def perform_create(self, serializer):
vdata = serializer.validated_data
vdata['create_by'] = self.request.user # 不可少

View File

@ -9,7 +9,6 @@ from django.utils.timezone import now
from user_agents import parse
import logging
from rest_framework.response import Response
from django.db import transaction
from rest_framework.exceptions import ParseError, ValidationError
from apps.utils.errors import PKS_ERROR
from rest_framework.generics import get_object_or_404
@ -19,6 +18,7 @@ from apps.utils.serializers import PkSerializer
from rest_framework.decorators import action
from apps.utils.serializers import ComplexSerializer
from django.db.models import F
from django.db import transaction
# 实例化myLogger
myLogger = logging.getLogger('log')
@ -81,7 +81,8 @@ class BulkCreateModelMixin(CreateModelMixin):
def after_bulk_create(self, objs):
pass
@transaction.atomic
def create(self, request, *args, **kwargs):
"""创建(支持批量)
@ -91,10 +92,9 @@ class BulkCreateModelMixin(CreateModelMixin):
many = False
if isinstance(rdata, list):
many = True
with transaction.atomic():
sr = self.get_serializer(data=rdata, many=many)
sr.is_valid(raise_exception=True)
self.perform_create(sr)
sr = self.get_serializer(data=rdata, many=many)
sr.is_valid(raise_exception=True)
self.perform_create(sr)
if many:
self.after_bulk_create(sr.data)
return Response(sr.data, status=201)
@ -105,6 +105,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
def after_bulk_update(self, objs):
pass
@transaction.atomic
def partial_update(self, request, *args, **kwargs):
"""部分更新(支持批量)
@ -113,6 +114,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
@transaction.atomic
def update(self, request, *args, **kwargs):
"""更新(支持批量)
@ -124,16 +126,15 @@ class BulkUpdateModelMixin(UpdateModelMixin):
queryset = self.filter_queryset(self.get_queryset())
objs = []
if isinstance(request.data, list):
with transaction.atomic():
for ind, item in enumerate(request.data):
obj = get_object_or_404(queryset, id=item['id'])
sr = self.get_serializer(obj, data=item, partial=partial)
if not sr.is_valid():
err_dict = { f'{ind+1}': sr.errors}
raise ValidationError(err_dict)
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
objs.append(sr.data)
self.after_bulk_update(objs)
for ind, item in enumerate(request.data):
obj = get_object_or_404(queryset, id=item['id'])
sr = self.get_serializer(obj, data=item, partial=partial)
if not sr.is_valid():
err_dict = { f'{ind+1}': sr.errors}
raise ValidationError(err_dict)
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
objs.append(sr.data)
self.after_bulk_update(objs)
else:
raise ParseError('提交数据非列表')
return Response(objs)
@ -148,6 +149,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
class BulkDestroyModelMixin(DestroyModelMixin):
@swagger_auto_schema(request_body=PkSerializer)
@transaction.atomic
def destroy(self, request, *args, **kwargs):
"""删除(支持批量)
@ -193,6 +195,8 @@ class CustomRetrieveModelMixin(RetrieveModelMixin):
给dict返回数据添加额外信息
"""
if hasattr(self, 'add_info_for_list'):
return self.add_info_for_list([data])[0]
return data
class CustomListModelMixin(ListModelMixin):
@ -241,6 +245,8 @@ class ComplexQueryMixin:
vdata = sr.validated_data
queryset = self.get_queryset()
querys = vdata.get('querys', [])
annotate_field_list = vdata.get('annotate_field_list', [])
if not querys:
new_qs = queryset
else:
@ -262,17 +268,29 @@ class ComplexQueryMixin:
new_qs = new_qs | one_qs
except Exception as e:
raise ParseError(str(e))
if annotate_field_list:
annotate_dict = getattr(self, "annotate_dict", {})
if annotate_dict:
filtered_annotate_dict = { key: annotate_dict[key] for key in annotate_field_list if key in annotate_dict}
new_qs = new_qs.annotate(**filtered_annotate_dict)
ordering = vdata.get('ordering', None)
if not ordering:
ordering = getattr(self, 'ordering', None)
if isinstance(ordering, str):
ordering = ordering.replace('\n', '').replace(' ', '')
ordering = ordering.split(',')
order_fields = []
if ordering:
for item in ordering:
if item.startswith('-'):
new_qs = new_qs.order_by(F(item[1:]).desc(nulls_last=True))
# JSONField 排序只能用字符串,不要 F
order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item)
else:
new_qs = new_qs.order_by(item)
order_fields.append(F(item).asc(nulls_last=True) if '__' not in item else item)
new_qs = new_qs.order_by(*order_fields)
page = self.paginate_queryset(new_qs)
if page is not None:
serializer = self.get_serializer(page, many=True)
@ -325,6 +343,7 @@ class MyLoggingMixin(object):
response = super().finalize_response(
request, response, *args, **kwargs
)
self.log["response_ms"] = self._get_response_ms()
# Ensure backward compatibility for those using _should_log hook
should_log = (
self._should_log if hasattr(self, "_should_log") else self.should_log
@ -353,7 +372,7 @@ class MyLoggingMixin(object):
"method": request.method,
"query_params": self._clean_data(request.query_params.dict()),
"user": self._get_user(request),
"response_ms": self._get_response_ms(),
# "response_ms": self._get_response_ms(),
"response": self._clean_data(rendered_content),
"status_code": response.status_code,
"agent": self._get_agent(request),
@ -447,7 +466,8 @@ class MyLoggingMixin(object):
By default, check if the request method is in logging_methods.
"""
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])\
or (self.log.get("response_ms", 0) > 2000)
def _clean_data(self, data):
"""

View File

@ -116,11 +116,26 @@ class BaseModel(models.Model):
@classmethod
def safe_get_or_create(cls, defaults=None, **kwargs):
defaults = defaults or {}
lock_data = {**kwargs, **defaults}
lock_hash = hashlib.md5(str(lock_data).encode()).hexdigest()
lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}"
with cache.lock(lock_key, timeout=10):
return cls.objects.get_or_create(**kwargs, defaults=defaults)
for attempt in range(3):
try:
# 先尝试获取(带锁)
try:
obj = cls.objects.select_for_update().get(**kwargs)
return obj, False
except cls.DoesNotExist:
# 不存在则创建
obj = cls(**kwargs, **defaults)
obj.save()
return obj, True
except IntegrityError:
# 发生唯一约束冲突时重试
if attempt == 2:
raise
time.sleep(0.1 * (attempt + 1))
except Exception:
# 其他异常直接抛出
raise
def handle_parent(self):
@ -132,32 +147,32 @@ class BaseModel(models.Model):
if not self.id:
is_create = True
self.id = idWorker.get_id()
with transaction.atomic():
old_parent = None
need_handle_parent = False
if hasattr(self, "parent"):
if is_create:
old_parent = None
need_handle_parent = False
if hasattr(self, "parent"):
if is_create:
need_handle_parent = True
else:
try:
old_parent = self.__class__.objects.get(id=self.id).parent
except Exception:
self.parent = None
need_handle_parent = True
else:
try:
old_parent = self.__class__.objects.get(id=self.id).parent
except Exception:
self.parent = None
need_handle_parent = True
if self.parent != old_parent:
need_handle_parent = True
try:
if self.parent != old_parent:
need_handle_parent = True
try:
ins = super().save(*args, **kwargs)
except IntegrityError as e:
if is_create:
time.sleep(0.01)
self.id = idWorker.get_id()
ins = super().save(*args, **kwargs)
except IntegrityError as e:
if is_create:
time.sleep(0.01)
self.id = idWorker.get_id()
ins = super().save(*args, **kwargs)
raise e
# 处理父级
if need_handle_parent:
self.handle_parent()
return ins
raise e
# 处理父级
if need_handle_parent:
self.handle_parent()
return ins
class SoftModel(BaseModel):

View File

@ -85,3 +85,4 @@ class ComplexSerializer(serializers.Serializer):
ordering = serializers.CharField(required=False)
querys = serializers.ListField(child=QuerySerializer(
many=True), label="查询列表", required=False)
annotate_field_list = serializers.ListField(child=serializers.CharField(), label="RawSQL字段列表", required=False)

View File

@ -9,7 +9,7 @@ from rest_framework.exceptions import ParseError
myLogger = logging.getLogger('log')
@auto_log(name='阿里云短信', raise_exception=True, send_mail=True)
@auto_log(name='阿里云短信', raise_exception=True, send_mail=False)
def send_sms(phone: str, template_code: int, template_param: dict):
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest

View File

@ -1,4 +1,6 @@
from django.db import connection
from django.utils import timezone
from datetime import datetime
def execute_raw_sql(sql: str, params=None):
"""执行原始sql并返回rows, columns数据
@ -23,7 +25,7 @@ def format_sqldata(columns, rows):
return [columns] + rows, [dict(zip(columns, row)) for row in rows]
def query_all_dict(sql, params=None):
def query_all_dict(sql, params=None, with_time_format=False):
'''
查询所有结果返回字典类型数据
:param sql:
@ -36,9 +38,19 @@ def query_all_dict(sql, params=None):
else:
cursor.execute(sql)
columns = [desc[0] for desc in cursor.description]
if with_time_format:
results = []
for row in cursor.fetchall():
row_dict = {}
for col, val in zip(columns, row):
if isinstance(val, datetime):
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
row_dict[col] = val
results.append(row_dict)
return results
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def query_one_dict(sql, params=None):
def query_one_dict(sql, params=None, with_time_format=False):
"""
查询一个结果返回字典类型数据
:param sql:
@ -49,6 +61,13 @@ def query_one_dict(sql, params=None):
cursor.execute(sql, params or ()) # 更简洁的参数处理
columns = [desc[0] for desc in cursor.description]
row = cursor.fetchone()
if with_time_format:
row_dict = {}
for col, val in zip(columns, row):
if isinstance(val, datetime):
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
row_dict[col] = val
return row_dict
return dict(zip(columns, row)) if row else None # 安全处理None情况
import pymysql

View File

@ -1,6 +1,6 @@
from django.core.cache import cache
from django.http import StreamingHttpResponse
from django.http import StreamingHttpResponse, Http404
from rest_framework.decorators import action
from rest_framework.exceptions import ParseError
from rest_framework.mixins import RetrieveModelMixin
@ -18,7 +18,9 @@ from apps.utils.serializers import ComplexSerializer
from rest_framework.throttling import UserRateThrottle
from drf_yasg.utils import swagger_auto_schema
import json
from django.db import connection
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import NotSupportedError
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
"""
@ -89,6 +91,36 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
elif hash_v_e:
return Response(hash_v_e)
def get_object(self, force_lock=False):
"""
智能加锁的get_object
- 只读请求普通查询
- 非只读请求且在事务中加锁查询
- 非只读请求但不在事务中普通查询带警告
"""
# 只读方法列表
read_only_methods = ['GET', 'HEAD', 'OPTIONS']
if self.request.method not in read_only_methods and connection.in_atomic_block:
if force_lock:
raise ParseError("当前操作需要在事务中进行,请使用事务装饰器")
# 非只读请求且在事务中:加锁查询
queryset = self.filter_queryset(self.get_queryset())
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
try:
obj = queryset.get(**filter_kwargs)
l_obj = queryset.model.objects.select_for_update().get(pk=obj.pk)
self.check_object_permissions(self.request, l_obj)
return l_obj
except queryset.model.DoesNotExist:
raise Http404
else:
# 其他情况:普通查询
return super().get_object()
def get_serializer_class(self):
action_serializer_name = f"{self.action}_serializer_class"
action_serializer_class = getattr(self, action_serializer_name, None)
@ -193,5 +225,4 @@ class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListM
CustomRetrieveModelMixin, BulkDestroyModelMixin, ComplexQueryMixin, CustomGenericViewSet):
"""
增强的ModelViewSet
"""
pass
"""

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-09-19 01:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_state_filter_dept'),
]
operations = [
migrations.AddField(
model_name='workflow',
name='view_path',
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-10-16 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
]
operations = [
migrations.AddField(
model_name='workflow',
name='view_path2',
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径2'),
),
]

View File

@ -21,6 +21,8 @@ class Workflow(CommonAModel):
'标题模板', max_length=50, default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}')
content_template = models.CharField(
'内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}')
view_path = models.TextField('前端自定义页面路径', null=True, blank=True)
view_path2 = models.TextField('前端自定义页面路径2', null=True, blank=True)
class Meta:
verbose_name = '工作流'

View File

@ -27,7 +27,7 @@ class StateSerializer(CustomModelSerializer):
class WorkflowSimpleSerializer(CustomModelSerializer):
class Meta:
model = Workflow
fields = ['id', 'name', 'key']
fields = ['id', 'name', 'key', 'view_path']
class StateSimpleSerializer(CustomModelSerializer):

View File

@ -77,7 +77,7 @@ class WfService(object):
"""
获取状态可执行的操作
"""
return Transition.objects.filter(is_deleted=False, source_state=state).all()
return Transition.objects.filter(is_deleted=False, source_state=state).all().order_by("-attribute_type", "-id")
@classmethod
def get_ticket_steps(cls, ticket: Ticket):
@ -442,11 +442,17 @@ class WfService(object):
last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or
ticket.in_add_node):
# 如果状态变化或是转交加签的情况再发送通知
Thread(target=send_ticket_notice_t, args=(ticket,), daemon=True).start()
cls.send_ticket_notice(ticketflow=last_log)
# 如果目标状态是脚本则异步执行
if state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
run_task.delay(ticket_id=ticket.id)
@classmethod
def send_ticket_notice(cls, ticketflow:TicketFlow):
# 根据ticketflow发送通知
Thread(target=send_ticket_notice_t, args=(ticketflow,), daemon=True).start()
@classmethod
def close_by_task(cls, ticket: Ticket, suggestion: str):
@ -479,10 +485,13 @@ class WfService(object):
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT,
participant=handler, transition=None)
def send_ticket_notice_t(ticket: Ticket):
cls.task_ticket(ticket=ticket)
def send_ticket_notice_t(ticketflow: TicketFlow):
"""
发送通知
"""
ticket = ticketflow.ticket
params = {'workflow': ticket.workflow.name, 'state': ticket.state.name}
if ticket.participant_type == 1:
# 发送短信通知

View File

@ -239,6 +239,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
raise ParseError('请指定查询分类')
return super().filter_queryset(queryset)
@transaction.atomic
def create(self, request, *args, **kwargs):
"""
新建工单
@ -263,25 +264,25 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
save_ticket_data[key] = ticket_data[key]
else:
save_ticket_data = ticket_data
with transaction.atomic():
ticket = serializer.save(state=start_state,
create_by=request.user,
create_time=timezone.now(),
act_state=Ticket.TICKET_ACT_STATE_DRAFT,
belong_dept=request.user.belong_dept,
ticket_data=save_ticket_data) # 先创建出来
# 更新title和sn
title = vdata.get('title', '')
title_template = ticket.workflow.title_template
if title_template:
all_ticket_data = {**rdata, **ticket_data}
title = title_template.format(**all_ticket_data)
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
ticket.sn = sn
ticket.title = title
ticket.save()
ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
handler=request.user, created=True)
ticket = serializer.save(state=start_state,
create_by=request.user,
create_time=timezone.now(),
act_state=Ticket.TICKET_ACT_STATE_DRAFT,
belong_dept=request.user.belong_dept,
ticket_data=save_ticket_data) # 先创建出来
# 更新title和sn
title = vdata.get('title', '')
title_template = ticket.workflow.title_template
if title_template:
all_ticket_data = {**rdata, **ticket_data}
title = title_template.format(**all_ticket_data)
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
ticket.sn = sn
ticket.title = title
ticket.save()
ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
handler=request.user, created=True)
return Response(TicketSerializer(instance=ticket).data)
@action(methods=['get'], detail=False, perms_map={'get': '*'})
@ -297,6 +298,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
return Response(ret)
@action(methods=['post'], detail=True, perms_map={'post': '*'})
@transaction.atomic
def handle(self, request, pk=None):
"""
处理工单
@ -307,13 +309,13 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
vdata = serializer.validated_data
new_ticket_data = ticket.ticket_data
new_ticket_data.update(**vdata['ticket_data'])
with transaction.atomic():
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
new_ticket_data=new_ticket_data, handler=request.user,
suggestion=vdata.get('suggestion', ''))
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
new_ticket_data=new_ticket_data, handler=request.user,
suggestion=vdata.get('suggestion', ''))
return Response(TicketSerializer(instance=ticket).data)
@action(methods=['post'], detail=True, perms_map={'post': '*'})
@transaction.atomic
def deliver(self, request, pk=None):
"""
转交工单
@ -325,15 +327,15 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
vdata = serializer.validated_data # 校验之后的数据
if not ticket.state.enable_deliver:
raise ParseError('不允许转交')
with transaction.atomic():
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.participant = vdata['target_user']
ticket.save()
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER,
participant=request.user, transition=None)
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.participant = vdata['target_user']
ticket.save()
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER,
participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response()
@action(methods=['get'], detail=True, perms_map={'get': '*'})
@ -381,11 +383,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save()
# 接单日志
# 更新工单流转记录
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT,
participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response()
else:
raise ParseError('无需接单')
@ -420,11 +423,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save()
# 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签说明
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE,
participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer)
@ -444,11 +448,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save()
# 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签意见
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE_END,
participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': '*'},

View File

@ -9,19 +9,18 @@ def correct_tuihuo_log():
mlogs = Mlog.objects.filter(mgroup__name='管料退火', count_use=0)
for mlog in mlogs:
try:
with transaction.atomic():
count_use = mlog.count_ok + mlog.count_notok
material_has = WMaterial.objects.get(
batch=mlog.batch, material=mlog.material_in, belong_dept=mlog.mgroup.belong_dept)
material_has.count = material_has.count - count_use
if material_has.count >= 0:
if material_has.count == 0:
material_has.delete()
else:
material_has.save()
mlog.count_real = count_use
mlog.count_use = count_use
mlog.save()
print(f'{mlog.id}-矫正成功')
count_use = mlog.count_ok + mlog.count_notok
material_has = WMaterial.objects.get(
batch=mlog.batch, material=mlog.material_in, belong_dept=mlog.mgroup.belong_dept)
material_has.count = material_has.count - count_use
if material_has.count >= 0:
if material_has.count == 0:
material_has.delete()
else:
material_has.save()
mlog.count_real = count_use
mlog.count_use = count_use
mlog.save()
print(f'{mlog.id}-矫正成功')
except Exception as e:
print(f'{mlog.id}-矫正出错:{e}')

View File

@ -61,8 +61,7 @@ class WMaterialFilter(filters.FilterSet):
qs = queryset.filter(material__id__in=matoutIds).exclude(state=WMaterial.WM_REPAIR)|queryset.filter(state=WMaterial.WM_REPAIRED, mgroup__id=mgroupId)
elif value == "canfix":
matoutIds = process.get_canout_mat_ids()
qs = queryset.filter(state=WMaterial.WM_REPAIR, mgroup__id=mgroupId)| queryset.filter(material__id__in=matoutIds, state=WMaterial.WM_NOTOK).exclude(
state=WMaterial.WM_REPAIR).exclude(state=WMaterial.WM_REPAIRED)
qs = queryset.filter(state=WMaterial.WM_REPAIR, mgroup__id=mgroupId)| queryset.filter(material__id__in=matoutIds, state=WMaterial.WM_NOTOK)
return qs
else:
raise ParseError("请提供工段查询条件")
@ -186,6 +185,7 @@ class MlogbFilter(filters.FilterSet):
"wm_in__state": ["exact"],
"material_in": ["exact", "isnull"],
"material_out": ["exact", "isnull"],
"parent": ["exact", "isnull"],
}
def filter_type(self, queryset, name, value):

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2025-08-27 07:22
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('wpm', '0121_auto_20250723_0912'),
]
operations = [
migrations.AlterField(
model_name='mlog',
name='create_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间'),
),
migrations.AlterField(
model_name='mlog',
name='handle_date',
field=models.DateField(blank=True, db_index=True, null=True, verbose_name='操作日期'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2025-09-04 08:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wpm', '0122_auto_20250827_1522'),
]
operations = [
migrations.AddField(
model_name='mlogbdefect',
name='count_has',
field=models.DecimalField(decimal_places=1, default=0, max_digits=11, verbose_name='含有该缺陷的数量'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.12 on 2025-10-20 02:09
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('em', '0022_equipment_cd_req_addr'),
('wpm', '0123_mlogbdefect_count_has'),
]
operations = [
migrations.AddField(
model_name='mloguser',
name='equipment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mloguser_equipment', to='em.equipment', verbose_name='生产设备'),
),
migrations.AddField(
model_name='mloguser',
name='work_start_time',
field=models.DateTimeField(blank=True, null=True, verbose_name='生产开始时间'),
),
]

View File

@ -15,6 +15,8 @@ from django.db import transaction
from django.db.models import Max
import re
from django.db.models import Q
import django.utils.timezone as timezone
from apps.utils.sql import query_all_dict
# Create your models here.
class SfLog(CommonADModel):
@ -198,7 +200,7 @@ class Mlog(CommonADModel):
mtask = models.ForeignKey(
Mtask, verbose_name='关联任务', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_mtask')
mgroup = models.ForeignKey(
Mgroup, verbose_name='工段', on_delete=models.CASCADE, null=True, blank=True)
Mgroup, verbose_name='工段', on_delete=models.CASCADE, null=True, blank=True, db_index=True)
wm_in = models.ForeignKey(WMaterial, verbose_name='投入物料所在库存', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlog_wm_in')
material_in = models.ForeignKey(
Material, verbose_name='消耗物', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_material_in')
@ -267,10 +269,10 @@ class Mlog(CommonADModel):
count_n_b = models.DecimalField('', default=0, max_digits=11, decimal_places=1) # 光芯七车间
count_n_qt = models.DecimalField('其他', default=0, max_digits=11, decimal_places=1)
handle_date = models.DateField('操作日期', null=True, blank=True)
handle_date = models.DateField('操作日期', null=True, blank=True, db_index=True)
team = models.ForeignKey(Team, verbose_name='班组', on_delete=models.SET_NULL, null=True, blank=True)
handle_user = models.ForeignKey(
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True) # 成型人
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True, db_index=True) # 成型人
handle_user_2 = models.ForeignKey(
User, verbose_name='操作人2', on_delete=models.CASCADE, related_name='mlog_handle_user_2', null=True, blank=True) # 切料人
handle_users = models.ManyToManyField(
@ -292,7 +294,9 @@ class Mlog(CommonADModel):
test_user = models.ForeignKey(
User, verbose_name='检验人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_test_user')
test_time = models.DateTimeField('检验时间', null=True, blank=True)
create_time = models.DateTimeField(
default=timezone.now, verbose_name='创建时间', help_text='创建时间', db_index=True)
@property
def mlogb(self):
return Mlogb.objects.filter(mlog=self).exclude(material_out=None)
@ -364,6 +368,9 @@ class MlogUser(BaseModel):
mlog = models.ForeignKey(Mlog, verbose_name='关联日志', on_delete=models.CASCADE)
handle_user = models.ForeignKey(User, verbose_name='操作人', on_delete=models.CASCADE)
process = models.ForeignKey(Process, verbose_name='子工序', on_delete=models.CASCADE)
work_start_time = models.DateTimeField('生产开始时间', null=True, blank=True)
equipment = models.ForeignKey(
Equipment, verbose_name='生产设备', on_delete=models.CASCADE, null=True, blank=True, related_name='mloguser_equipment')
shift = models.ForeignKey(Shift, verbose_name='关联班次', on_delete=models.CASCADE)
handle_date = models.DateField('操作日期')
@ -473,6 +480,7 @@ class MlogbDefect(BaseModel):
mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE)
defect = models.ForeignKey("qm.Defect", verbose_name='缺陷', on_delete=models.CASCADE, null=True, blank=True)
count = models.DecimalField('数量', default=0, max_digits=11, decimal_places=1)
count_has = models.DecimalField('含有该缺陷的数量', default=0, max_digits=11, decimal_places=1)
@classmethod
def get_defect_qs(cls, ftype="all"):
@ -508,7 +516,7 @@ class Mlogbw(BaseModel):
@classmethod
def cal_count_notok(cls, mlogb: Mlogb):
from apps.qm.models import Defect
from apps.qm.models import Defect, FtestDefect
# 锁定mlogb以防止并发修改
# mlogb:Mlogb = Mlogb.objects.select_for_update().get(pk=mlogb.pk)
count = Mlogbw.objects.filter(mlogb=mlogb).count()
@ -519,9 +527,13 @@ class Mlogbw(BaseModel):
mlogb.count_real = count
count_notok = 0
count_notok_full = 0
tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__is_ok=False)
tqs_a = Mlogbw.objects.filter(mlogb=mlogb, ftest__is_ok=False).values("ftest__defect_main").annotate(xcount=Count('id'))
defects = {defect.id: defect for defect in Defect.objects.filter(id__in=tqs.values_list("ftest__defect_main", flat=True))}
# 个追踪的合格b类不分开这里会导致count_ok_full与count_ok一样了暂时不做处理
tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False)
tqs_a = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False).values("ftest__defect_main").annotate(xcount=Count('id'))
defect_ids = tqs.values_list("ftest__defect_main", flat=True)
tqs_has_a = FtestDefect.objects.filter(ftest__mlogbw_ftest__mlogb=mlogb, has=True).values("defect").annotate(xcount=Count('id'))
defect_ids = defect_ids.union(tqs_has_a.values_list("defect", flat=True))
defects = {defect.id: defect for defect in Defect.objects.filter(id__in=defect_ids)}
md_ids = []
for t in tqs_a:
md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t["ftest__defect_main"]])
@ -531,6 +543,13 @@ class Mlogbw(BaseModel):
count_notok += t["xcount"]
if defects[t["ftest__defect_main"]].okcate != 10:
count_notok_full += t["xcount"]
for t2 in tqs_has_a:
md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t2["defect"]], defaults={"count": 0})
md.count_has = t2["xcount"]
md.save()
md_ids.append(md.id)
MlogbDefect.objects.filter(mlogb=mlogb).exclude(id__in=md_ids).delete()
mlogb.count_notok = count_notok
mlogb.count_ok = count - mlogb.count_notok
@ -722,7 +741,6 @@ class BatchSt(BaseModel):
# return ins, True
@classmethod
@transaction.atomic
def init_dag(cls, batch:str):
"""
更新批次数据关系链(初步)
@ -800,7 +818,30 @@ class BatchLog(BaseModel):
@classmethod
def batches_to(cls, batch:str):
batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct()
# query = """
# SELECT batch FROM wpm_batchst
# WHERE batch ~ %s
# """
query = """
SELECT batch
FROM wpm_batchst
WHERE batch ~ %s
ORDER BY
-- 先按前缀部分排序例如 'A'
SUBSTRING(batch FROM '^(.*)-') DESC,
-- 再按后缀的数值部分排序 '2', '11' 转为整数
CAST(SUBSTRING(batch FROM '-([0-9]+)$') AS INTEGER) DESC
""" # 排序可在sql层处理
query_ = """SELECT batch FROM wpm_batchst WHERE batch ~ %s"""
pattern = f'^{batch}-[0-9]+$'
"""可以用如下方法直接查询
"""
# batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct()
# batches = sorted(list(batches), key=custom_key)
batches_r = query_all_dict(query_, params=(pattern,))
batches = [b["batch"] for b in batches_r]
batches = sorted(list(batches), key=custom_key)
last_batch_num = None
if batches:

View File

@ -1,6 +1,6 @@
from apps.wpm.models import BatchSt
import logging
from apps.wpm.models import Mlogb, MlogbDefect
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect
from apps.mtm.models import Mgroup
import decimal
from django.db.models import Sum
@ -41,10 +41,14 @@ def main(batch: str, mgroup_obj:Mgroup=None):
for item in mlogb1_qs:
# 找到对应的输入
mlogb_from:Mlogb = item.mlogb_from
mlogbw_from:Mlogbw = item.mlogbw_from
if mlogb_from:
mlogb_q_ids.append(mlogb_from.id)
data[f"{mgroup_name}_count_use"] += mlogb_from.count_use
data[f"{mgroup_name}_count_pn_jgqbl"] += mlogb_from.count_pn_jgqbl
if mlogbw_from:
data[f"{mgroup_name}_count_use"] += 1
data[f"{mgroup_name}_count_pn_jgqbl"] += 0
if item.mlog.handle_user:
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
if item.mlog.handle_date:
@ -65,6 +69,7 @@ def main(batch: str, mgroup_obj:Mgroup=None):
data[f"{mgroup_name}_合格率"] = 0
mlogbd1_qs = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
mlogbd1_qs_x = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count_has__gt=0).values("defect__name").annotate(total=Sum("count_has"))
mlogbd1_q_qs = MlogbDefect.objects.filter(mlogb__id__in=mlogb_q_ids, count__gt=0).values("defect__name").annotate(total=Sum("count"))
for item in mlogbd1_q_qs:
@ -75,6 +80,10 @@ def main(batch: str, mgroup_obj:Mgroup=None):
data[f"{mgroup_name}_缺陷_{item['defect__name']}"] = item["total"]
data[f"{mgroup_name}_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2)
for item in mlogbd1_qs_x:
data[f"{mgroup_name}_含缺陷_{item['defect__name']}"] = item["total"]
data[f"{mgroup_name}_含缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2)
data[f"{mgroup_name}_日期"] = list(set(data[f"{mgroup_name}_日期"]))
data[f"{mgroup_name}_小日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
data[f"{mgroup_name}_大日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")

View File

@ -29,6 +29,7 @@ def main(batch: str, mgroup_obj):
if mlogb1_qs.exists():
data[f"{mgroup_name}_日期"] = []
data[f"{mgroup_name}_操作人"] = []
data[f"{mgroup_name}_班次"] = []
data[f"{mgroup_name}_count_use"] = 0
data[f"{mgroup_name}_count_real"] = 0
data[f"{mgroup_name}_count_ok"] = 0
@ -47,6 +48,8 @@ def main(batch: str, mgroup_obj):
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
if item.mlog.handle_date:
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
if item.mlog.shift:
data[f"{mgroup_name}_班次"].append(item.mlog.shift.name)
data[f"{mgroup_name}_count_real"] += item.count_real
data[f"{mgroup_name}_count_ok"] += item.count_ok
data[f"{mgroup_name}_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0
@ -80,6 +83,8 @@ def main(batch: str, mgroup_obj):
data[f"{mgroup_name}_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data[f"{mgroup_name}_日期"]])
data[f"{mgroup_name}_操作人"] = list(set(data[f"{mgroup_name}_操作人"]))
data[f"{mgroup_name}_操作人"] = ";".join([item.name for item in data[f"{mgroup_name}_操作人"]])
data[f"{mgroup_name}_班次"] = list(set(data[f"{mgroup_name}_班次"]))
data[f"{mgroup_name}_班次"] = ";".join([item for item in data[f"{mgroup_name}_班次"]])
mlogb2_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ from apps.system.models import User
from apps.pm.models import Mtask
from apps.mtm.models import Mgroup, Shift, Material, Route, RoutePack, Team, Srule
from .models import SfLog, WMaterial, Mlog, Mlogb, Mlogbw, Handover, Handoverb, Handoverbw, MlogbDefect, BatchLog, BatchSt
from .models import SfLog, WMaterial, Mlog, Mlogb, Mlogbw, Handover, Handoverb, Handoverbw, MlogbDefect, BatchLog, BatchSt, MlogUser
from apps.mtm.services_2 import cal_material_count
from apps.wf.models import Ticket
from apps.wf.services import WfService
@ -147,17 +147,19 @@ def get_pcoal_heat(year_s: int, month_s: int, day_s: int):
myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True)
return 25000
@lock_model_record_d_func(Mlog)
# @lock_model_record_d_func(Mlog)
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
"""
生产日志提交后需要执行的操作
"""
if mlog.work_start_time and mlog.work_start_time > timezone.now():
raise ParseError('操作开始时间不能晚于当前时间')
if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time:
raise ParseError('操作结束时间不能早于操作开始时间')
if mlog.count_real == 0:
raise ParseError('产出数量不能为0')
if mlog.submit_time is not None:
return
# if mlog.submit_time is not None:
# return
if now is None:
now = timezone.now()
if mlog.handle_date is None:
@ -203,7 +205,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
for item in m_ins:
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
for itemx in mbd_qs:
if itemx.defect:
if itemx.defect and itemx.count > 0:
m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item))
else:
m_ins_list = [(material_in, mlog.batch, mlog.count_use, None, mlog)]
@ -282,7 +284,8 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# Mlogbw.cal_count_notok(item)
for itemx in mbd_qs:
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
if itemx.count > 0:
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
# # 获取所有主要的不合格项/先暂时保留
# bw_qs = Mlogbw.objects.filter(mlogb=item)
# defectIds= Ftest.objects.filter(mlogbw_ftest__in=bw_qs).exclude(defect_main=None).values_list("defect_main__id", flat=True).distinct()
@ -382,6 +385,8 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mlog.submit_user = user
mlog.stored_notok = stored_notok
mlog.stored_mgroup = stored_mgroup
if mlog.work_end_time is None and mlog.work_start_time is not None:
mlog.work_end_time = now
mlog.save()
# 更新任务进度
@ -392,19 +397,19 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 触发批次统计分析
xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True))
ana_batch_thread(xbatches)
ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup)
# 触发单个统计
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True))
if wprIds:
ana_wpr_thread(wprIds, mlog.mgroup)
@lock_model_record_d_func(Mlog)
# @lock_model_record_d_func(Mlog)
def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
"""日志撤回
"""
if mlog.submit_time is None:
return
# if mlog.submit_time is None:
# return
if now is None:
now = timezone.now()
@ -435,7 +440,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# Mlogbw.cal_count_notok(item)
for itemx in mbd_qs:
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
if itemx.count > 0:
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# # 获取所有主要的不合格项
# bw_qs = Mlogbw.objects.filter(mlogb=item)
@ -530,7 +536,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
for item in m_ins:
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
for itemx in mbd_qs:
if itemx.defect:
if itemx.defect and itemx.count > 0:
m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item))
else:
m_ins_list = [(material_in, mlog.batch, mlog.count_use, mlog.wm_in, mlog)]
@ -613,7 +619,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 触发批次统计分析
xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True))
ana_batch_thread(xbatches)
ana_batch_thread(xbatches, mgroup=mlog.mgroup)
# 触发单个统计
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True))
@ -707,8 +713,8 @@ def update_mtask(mtask: Mtask, fill_way: int = 10):
utask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0
utask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0
utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0
if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
utask.state = Utask.UTASK_SUBMIT
# if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
# utask.state = Utask.UTASK_SUBMIT
utask.save()
@lock_model_record_d_func(Handover)
@ -733,6 +739,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
recive_mgroup = handover.recive_mgroup
recive_dept = handover.recive_dept
if handover.type == Handover.H_CHANGE:
handover.mtype = Handover.H_MERGE
new_batch = handover.new_batch
if new_batch and mtype != Handover.H_MERGE:
raise ParseError("只有合并时才能提供新批次号")
@ -752,7 +760,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
raise ParseError('拆批请选择车间库存')
batches_to_limit = BatchLog.batches_to(batch=handover.wm.batch)["batches"]
source_b, _ = BatchSt.g_create(batch=handover.wm.batch)
for item in handoverb_list:
for indx, item in enumerate(handoverb_list):
wmId, xcount, handover_or_b = item
if xcount <= 0:
raise ParseError("存在非正数!")
@ -922,9 +930,16 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
handoverbws = Handoverbw.objects.filter(handoverb=handover_or_b)
if handoverbws.count() != xcount:
raise ParseError("交接与明细数量不一致,操作失败")
wm_ids = list(Wpr.objects.filter(wpr_handoverbw__in=handoverbws).values_list("wm_id", flat=True).distinct())
if len(wm_ids) == 1 and wm_ids[0] == wm_from.id:
pass
else:
raise ParseError(f'{batch}物料明细中存在{len(wm_ids)}个不同物料批次')
for item in handoverbws:
wpr:Wpr = item.wpr
Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
if wm_to.count != Wpr.objects.filter(wm=wm_to).count():
raise ParseError("交接与明细数量不一致2,操作失败")
handover.submit_user = user
handover.submit_time = now
@ -1011,6 +1026,10 @@ def mlog_submit_validate(ins: Mlog):
raise ParseError('该日志未指定消耗!')
if Mlogb.objects.filter(material_out__isnull=False, count_real=0, mlog=ins).exists():
raise ParseError('产出数量不能为0!')
if ins.is_fix is False and ins.route:
process = ins.route.process
if Process.objects.filter(parent=process).exists() and not MlogUser.objects.filter(mlog=ins).exists():
raise ParseError('该日志子工序信息未完善!')
def bind_mlog(ticket: Ticket, transition, new_ticket_data: dict):
ins = Mlog.objects.get(id=new_ticket_data['t_id'])

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,16 @@
from django_filters import rest_framework as filters
from apps.wpm.models import (Handoverbw, Mlogbw)
from apps.wpm.models import (Handoverbw, Mlogbw, WMaterial)
from apps.inm.models import MIOItemw
from apps.wpmw.models import Wpr
from apps.mtm.models import Route, Material
from apps.mtm.models import Route, Material, Mgroup
from django.db.models import Q
from rest_framework.exceptions import ParseError
class WprFilter(filters.FilterSet):
can_use = filters.CharFilter(method='filter_can_use')
mgroupx = filters.CharFilter(label='MgroupId', method='filter_mgroupx')
tag = filters.CharFilter(label="todo/done", method="filter_tag")
class Meta:
model = Wpr
fields = {
@ -19,6 +21,30 @@ class WprFilter(filters.FilterSet):
"defects": ["exact"],
"number": ["exact"]
}
def filter_mgroupx(self, queryset, name, value):
return queryset.filter(wm__in=WMaterial.ava_qs(mgroup=Mgroup.objects.get(id=value)))
def filter_tag(self, queryset, name, value):
mgroupId = self.data.get("mgroupx", None)
if mgroupId:
process = Mgroup.objects.get(id=mgroupId).process
queryset = queryset.filter(material__type__in=[Material.MA_TYPE_MAINSO, Material.MA_TYPE_HALFGOOD, Material.MA_TYPE_GOOD])
if value == "todo":
matIds = process.get_canin_mat_ids()
qs = queryset.filter(material__id__in=matIds).exclude(state=WMaterial.WM_REPAIRED)|queryset.filter(state=WMaterial.WM_REPAIR, wm__mgroup__id=mgroupId)
return qs
elif value == "done":
matoutIds = process.get_canout_mat_ids()
qs = queryset.filter(material__id__in=matoutIds).exclude(state=WMaterial.WM_REPAIR)|queryset.filter(state=WMaterial.WM_REPAIRED, wm__mgroup__id=mgroupId)
return qs
elif value == "canfix":
matoutIds = process.get_canout_mat_ids()
qs = queryset.filter(state=WMaterial.WM_REPAIR, wm__mgroup__id=mgroupId)| queryset.filter(material__id__in=matoutIds, state=WMaterial.WM_NOTOK)
return qs
else:
raise ParseError("请提供工段查询条件")
def filter_can_use(self, queryset, name, value):
if value == 'yes':

View File

@ -13,13 +13,24 @@ class WprDefectSerializer(CustomModelSerializer):
class WprSerializer(CustomModelSerializer):
wm_batch = serializers.CharField(source="wm.batch", read_only=True)
mb_batch = serializers.CharField(source="mb.batch", read_only=True)
material_name = serializers.StringRelatedField(
source='material', read_only=True)
# material_start_name = serializers.StringRelatedField(source='material_start', read_only=True)
wm_material_ofrom_name = serializers.StringRelatedField(source="wm.material_ofrom", read_only=True)
wprdefect = WprDefectSerializer(many=True, read_only=True)
class Meta:
model = Wpr
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
material_name = ret.get("material_name", "")
if material_name:
ret["process_name"] = material_name.split("|")[-1]
return ret
class WprDetailSerializer(WprSerializer):
mb_ = MaterialBatchSerializer(source='mb', read_only=True)

View File

@ -10,6 +10,7 @@ from rest_framework.exceptions import ParseError
from django.db import transaction
from apps.wpmw.filters import WprFilter
from apps.utils.sql import query_one_dict
from django.db.models.expressions import RawSQL
class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet):
@ -17,8 +18,9 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
动态产品
"""
perms_map = {"get": "*"}
select_related_fields = ["wm", "mb", "material"]
select_related_fields = ["wm", "mb", "material", "wm__material_ofrom"]
prefetch_related_fields = ["defects"]
queryset = Wpr.objects.all()
serializer_class = WprSerializer
@ -27,6 +29,10 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
ordering = ["number", "create_time"]
ordering_fields = ["number", "create_time", "update_time"]
search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"]
annotate_dict = {
"number_prefix": RawSQL("regexp_replace(number, '(\\d+)$', '')", []),
"number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []),
}
def filter_queryset(self, queryset):
qs = super().filter_queryset(queryset)
@ -36,7 +42,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
qs.exclude(mb=None, wm=None)
return qs
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=WprNewSerializer)
@action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=WprNewSerializer)
def new_number(self, request, *args, **kwargs):
"""获取新的编号"""
data = request.data
@ -47,16 +53,17 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
wpr_last = wps_qs.order_by("number").last()
count = wps_qs.count()
last_number = wpr_last.number if count > 0 else None
last_number_count = int(last_number.split("-")[-1].lstrip('0'))
last_number_count = int(last_number.split("-")[-1].lstrip("0"))
mat = Material.objects.get(id=material_start)
return Response({"count": count, "last_number": last_number, "material_model":mat.model, "last_number_count": last_number_count})
@action(methods=['get'], detail=False, perms_map={'get': '*'})
return Response({"count": count, "last_number": last_number, "material_model": mat.model, "last_number_count": last_number_count})
@action(methods=["get"], detail=False, perms_map={"get": "*"})
def number_out_last(self, request, *args, **kwargs):
"""获取最新的出库对外编号
获取最新的出库对外编号(get请求传入prefix参数和with_unsubmit参数with_unsubmit默认为yes表示是否包含未出库的记录prefix为前缀'WPR-2023-0)"""
from apps.inm.models import MIOItemw
prefix = request.query_params.get("prefix", None)
if not prefix:
raise ParseError("请传入前缀参数")
@ -67,7 +74,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
SELECT id, number_out FROM wpmw_wpr
WHERE number_out ~ %s order by number_out desc limit 1
"""
pattern = f'^{prefix}[0-9]+$'
pattern = f"^{prefix}[0-9]+$"
number_outs = []
wpr_qs_last = query_one_dict(query, [pattern])
if wpr_qs_last:
@ -87,7 +94,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
if number_outs:
number_outs.sort()
number_out_last = number_outs[-1]
number_int_last = number_out_last.lstrip(prefix).lstrip('0')
number_int_last = number_out_last.lstrip(prefix).lstrip("0")
try:
number_int_last = int(number_int_last)
except ValueError:
@ -95,9 +102,8 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
return Response({"number_out_last": number_out_last, "number_out_int_last": int(number_int_last)})
else:
return Response({"number_out_last": None})
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=WproutListSerializer)
@action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=WproutListSerializer)
@transaction.atomic
def assgin_number_out(self, request, *args, **kwargs):
"""分配出库对外编号
@ -108,7 +114,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
vdata = sr.validated_data
items = vdata["items"]
number_outs = [i["number_out"] for i in items]
existing_numbers = Wpr.objects.filter(number_out__in=number_outs, number_out__isnull=False).values_list('number_out', flat=True)
existing_numbers = Wpr.objects.filter(number_out__in=number_outs, number_out__isnull=False).values_list("number_out", flat=True)
if existing_numbers.exists():
used_numbers = list(existing_numbers)
raise ParseError(f"以下对外编号已被使用: {used_numbers[0]}{len(used_numbers)}")
@ -116,4 +122,4 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
wpr = Wpr.objects.get(id=i["id"])
wpr.number_out = i["number_out"]
wpr.save()
return Response()
return Response()

View File

@ -1,3 +1,151 @@
## 2.8.2025101011
- feat: 新增功能
- Ptest val_xj可为空 [caoqianming]
- quick调用serializer时传入request [caoqianming]
- ofm-修改model [zty]
- Mroombooking添加字段 [caoqianming]
- base add_info_for_item 可复用list逻辑 [caoqianming]
- 添加wpr查询参数 [caoqianming]
- base cquery支持annotate [caoqianming]
- mroombooking 返回slots [caoqianming]
- fix: 问题修复
- p_create_after 自动创建mlogbw时关于exclude语句导致的查询错误 [caoqianming]
## 2.8.2025092816
- feat: 新增功能
- handover revert撤回时做校验 [caoqianming]
- ofm-models 修改车辆表的字段信息 [zty]
- iofm-serializer 增加ticket_ [zty]
- 会议室预定修改 [caoqianming]
- p_create_after 优化 [caoqianming]
- mlog quick增加wprs_in传参 [caoqianming]
- 车间领料时完善提示添加物料名 [caoqianming]
- ofm-修改会议室信息 [zty]
- 改版需提供新批次号 [caoqianming]
- ofm 修改publicity 的model [zty]
- 修改ofm-services.py [zty]
- base send_sms auto_log send_mail使用False [caoqianming]
- 优化bind_routepack [caoqianming]
- base 优化wf通知发送 [caoqianming]
- routepack_ticket_change 变为创建中状态 [caoqianming]
- route增加查询条件以支持获取后续工段的信息 [caoqianming]
- 导入物料优化一下 [caoqianming]
- 优化的mplogxview [caoqianming]
- base sql querydict可传入是否格式化时间参数 [caoqianming]
- base workflow list 添加并返回view_path [caoqianming]
- 优化handover list接口 [caoqianming]
- 玻纤拉丝采集问题 [caoqianming]
- base system和wf优化事务处理 [caoqianming]
- 优化cd.py [caoqianming]
- wpr list 返回process_name [caoqianming]
- fix: 问题修复
- mroombooking 创建时未create_by [caoqianming]
## 2.8.2025091616
- feat: 新增功能
- mioitem处理事务处理 [caoqianming]
- base 移除基础model层事务 [caoqianming]
- inm和wpm关于事务处理的修改 [caoqianming]
- 批次的DAG数据只获取直接的上下级 [caoqianming]
- wpm优化事务和悲观锁 [caoqianming]
- cdview优化校验 [caoqianming]
- base 优化get_object [caoqianming]
- mlogbout serializer update校验mlog [caoqianming]
- 生产日志子表全部加mlog锁 [caoqianming]
- ofm-serializer fix [zty]
- base 优化get_object事务 [caoqianming]
- ofm-修改字段 [zty]
- ofm - 档案管理修改 serializer [zty]
- ofm-档案管理-serializer 更新 [zty]
- wpm修改为自动事务 [caoqianming]
- 修改wpm事务 [caoqianming]
- wpm 取消 transaction [caoqianming]
- base CustomGenericViewSet 添加自动事务 [caoqianming]
- ofm-修改 -档案借 filterset_class [zty]
- ofm 增加档案台账模糊查询 [zty]
- 默认提交时间为结束时间 [caoqianming]
- 快速报工的bug [caoqianming]
- base 日志默认记录耗时大于2s的 [caoqianming]
- ofm 增加档案台账 [zty]
- mlog submit/revert 改用乐观锁 [caoqianming]
- ofm 修改车辆model [zty]
- ofm 修改 车辆model [zty]
- wpr list 返回wm_material_ofrom_name [caoqianming]
- wpr list 返回wm_material_name [caoqianming]
- wpr list 返回material_start_name [caoqianming]
- vehicle 增加serializer 返回字段 [zty]
- wpr list返回batch [caoqianming]
- 增加wpr tag查询条件以支持todo等 [caoqianming]
- 修改车辆的字段 [zty]
- ofm 车辆管理迁移文件 [zty]
- ofm 车辆审批 [zty]
- 出入库记录和交接记录提交处理时都进行单个的强校验 [caoqianming]
- 添加物料明细与批次不匹配的校验2 [caoqianming]
- 添加物料明细与批次不匹配的校验 [caoqianming]
- gen_number_with_rule 优化一下 [caoqianming]
- handover_submit增强校验 [caoqianming]
- mlog list 支持返回mlogbw number [caoqianming]
- gen_number_with_rule按生产日志中的工段过滤 [caoqianming]
- gen_number_with_rule 完善 [caoqianming]
- 工段未配置班次提醒 [caoqianming]
- 板段号生成逻辑修改 [caoqianming]
- ofm-印章绑定审批修改 [zty]
- fix: 问题修复
- wpm取消事务时出现的缩进bug [caoqianming]
- 次批作为输入不会影响输出批次号 [caoqianming]
- lock_and_check_can_update [caoqianming]
- lock_and_check_can_update [caoqianming]
- base 在create update destroy添加自动事务 [caoqianming]
- base 修改_should_use_transaction [caoqianming]
- 产生编号时存在bug [caoqianming]
- 交接记录已提交不可变动 [caoqianming]
- mlog change 导致的bug问题 [caoqianming]
- wpr list filter bug [caoqianming]
- 出入库记录和交接记录提交处理时都进行单个的强校验 [caoqianming]
## 2.8.2025090815
- feat: 新增功能
- batch bxerp含缺陷统计 [caoqianming]
- mlog list 添加update_time筛选 [caoqianming]
- 工艺步骤中辅料使用的校验 [caoqianming]
- mlogb添加parent isnull查询 [caoqianming]
- 次批触发输出产生 [caoqianming]
- 修改印章的model 和 serializer [zty]
- 修改 ofm sevice 印章模块 [zty]
- base 优化safe_get_or_create [caoqianming]
- 修改ofm seal 过滤查询功能 [zty]
- 行政管理 -印章管理 [zty]
- ftest默认为合格 [caoqianming]
- mlogbdefect添加count_has 字段及处理 [caoqianming]
- update_mb_item defect处理更严谨 [caoqianming]
- 取消最后一步产出与工艺包不一致 的校验 [caoqianming]
- 出入库记录添加乐观锁 [caoqianming]
- batch_bxerp考虑mlogbw_from [caoqianming]
- mlog_submit mlogbdefect增加筛选条件 [caoqianming]
- 导入物料明细时可默认批次号为无 [caoqianming]
- 光芯批次统计增加班次返回 [caoqianming]
- mlog_submit 进行操作时间校验 [caoqianming]
- route update 时from_route存在则不可修改关键信息 [caoqianming]
- mlogchange支持work_start_time [caoqianming]
- route采用引用方式允许重复创建 [caoqianming]
- MlogSerializer 处理 work_start_time 的 bug [caoqianming]
- 修改能管采集的点位 [zty]
- mlog work_start_time必填 [caoqianming]
- ftestwork submit支持mb [zty]
- toggle_state 使用 routepack.update权限 [caoqianming]
- ftestwork支持对materialbatch检查 [caoqianming]
- batchst返回material_start相关信息 [caoqianming]
- fix: 问题修复
- ftestwork submit 校验wm和mb bug [caoqianming]
- MlogbInUpdateSerializer 联动count_use 和 count_real [caoqianming]
- batchlog batches_to 优化 [caoqianming]
## 2.7.2025082816
- feat: 新增功能
- 添加定时任务以标记mtask为已完成 [caoqianming]
- update_mtask 不再触发状态改变 [caoqianming]
- 子任务提交会触发大任务提交 [caoqianming]
- 优化mlog list查询 [caoqianming]
- 增加直接userid获取token的接口 [caoqianming]
- mlog 添加索引 [caoqianming]
- 增加mlogbw start_test接口以优化批量操作 [caoqianming]
- 优化mlogbw bulk update [caoqianming]
## 2.7.2025082616
- feat: 新增功能
- 添加mlog和handover的锁 [caoqianming]

View File

@ -75,46 +75,42 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
with sc_lock: # 加锁,防止竞争
sc = sc_all[addr]
try:
if sc is None:
sc = socket.socket()
sc.settimeout(5)
sc.connect((host, int(port)))
with sc_lock: # 再次加锁,更新 sc_all
try:
if sc is None:
sc = socket.socket()
sc.settimeout(5)
sc.connect((host, int(port)))
sc_all[addr] = sc
sc.settimeout(0.5)
while True:
try:
data = sc.recv(65536)
if not data:
sc.settimeout(0.5)
while True:
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5)
sc.sendall(b"R")
data = bytearray()
while len(data) < 8:
chunk = sc.recv(1024)
if not chunk:
raise ConnectionError("连接中断")
data.extend(chunk)
return sc, bytes(data)
except Exception as e:
if sc is not None:
try:
sc.close()
except Exception:
pass
with sc_lock: # 加锁,清除无效连接
sc.settimeout(5)
sc.sendall(b"R")
data = bytearray()
while len(data) < 8:
chunk = sc.recv(1024)
if not chunk:
raise ConnectionError("连接中断")
data.extend(chunk)
return sc, bytes(data)
except Exception as e:
if sc is not None:
try:
sc.close()
except Exception:
pass
sc_all[addr] = None
self.error(f'采集器通信失败: {e}')
print(traceback.format_exc())
return None, None
self.error(f'采集器通信失败: {e}')
return None, None
sc, resp = connect_and_send()
if sc is None or resp is None:
@ -122,7 +118,17 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
res = handle_bytes(resp)
if isinstance(res, str):
self.error(res)
if res == "数据头不正确":
sc, resp = connect_and_send()
if sc is None or resp is None:
return
res = handle_bytes(resp)
if isinstance(res, str):
self.error(res)
else:
self.ok(res)
else:
self.error(res)
else:
self.ok(res)

143
out_service/insert_kvt.py Normal file
View File

@ -0,0 +1,143 @@
import requests
import os
import sys
import django
import json
import logging
import time
import threading
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(CUR_DIR)
sys.path.insert(0, BASE_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
django.setup()
from apps.enm.services import insert_mplogx_item
from django.utils import timezone
from apps.utils.tasks import send_mail_task
from datetime import datetime, timedelta
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(CUR_DIR)
sys.path.insert(0, BASE_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
django.setup()
SERVER = '192.168.1.37'
PORT = '8089'
PATH = f'http://{SERVER}:{PORT}/GetTagList'
SERVER2 = '192.168.1.43'
PORT2 = '8081'
PATH2 = f'http://{SERVER2}:{PORT2}/GetTagList'
#MIN_LIST = set(range(0,61,5))
MIN_LIST = [0]
myLogger = logging.getLogger("log")
class MailController:
def __init__(self):
self.last_sent_time = None
self.interval = timedelta(days=1)
self.lock = threading.Lock()
self._is_sending = False
def should_send_mail(self):
now = datetime.now()
# thread_name = threading.current_thread().name
with self.lock:
if self._is_sending:
# myLogger.info(f"线程 {thread_name}: 邮件正在发送中,跳过")
return False
if self.last_sent_time is None:
# myLogger.info(f"线程 {thread_name}: 首次发送邮件")
self._is_sending = True
return True
time_since_last = now - self.last_sent_time
if time_since_last > self.interval:
# myLogger.info(f"线程 {thread_name}: 距离上次发送已超过间隔,允许发送")
self._is_sending = True
return True
else:
# myLogger.info(f"线程 {thread_name}: 距离上次发送不足间隔,跳过")
return False
def mark_as_sent(self):
with self.lock:
self.last_sent_time = datetime.now()
self._is_sending = False
myLogger.info("邮件发送状态已重置")
mail_controller = MailController()
def send_error_notification(error_message):
"""
发送错误通知
"""
if mail_controller.should_send_mail():
try:
send_mail_task.delay(subject='insert_kvt_error', message=str(error_message))
myLogger.error(f"请求组态王失败:{str(error_message)}")
except Exception as e:
myLogger.exception(f"发送错误邮件失败: {e}")
finally:
mail_controller.mark_as_sent()
def fetch_data(timex, enp_mpoint_dict, path):
"""
从数据库转存到超表
"""
response = None
try:
response = requests.get(path, timeout=5)
response.raise_for_status() # 如果响应码不是 200将触发异常
except requests.RequestException as e:
send_error_notification(e)
if response is None:
return
try:
lines = response.text
json_line = [line.strip() for line in lines.split('\n') if line.strip() ]
current_object = []
# 将碎片分组为完整的 JSON 对象
for line in json_line:
current_object.append(line.strip()) # 将当前行加入对象
if line.strip() == '}': # 遇到结束大括号时
try:
obj_str = ' '.join(current_object).replace(',}', '}')
obj_dict = json.loads(obj_str)
insert_mplogx_item(obj_dict.get('strVarName'), obj_dict.get('VarValue'), timex, enp_mpoint_dict)
except json.JSONDecodeError as e:
send_error_notification(e)
current_object = [] # 重置,准备处理下一个对象
except Exception as e:
send_error_notification(e)
def get_data():
last_triggered = None
while True:
now = timezone.now()
now = now.replace(microsecond=0)
if now.second in MIN_LIST:
if last_triggered != now:
last_triggered = now
enp_mpoint_dict= {}
threads = [
threading.Thread(target=fetch_data, args=(now, enp_mpoint_dict, PATH), daemon=True),
threading.Thread(target=fetch_data, args=(now, enp_mpoint_dict, PATH2), daemon=True)
]
for t in threads:
t.start()
for t in threads:
t.join(timeout=10) # 设置超时防止线程挂起
time.sleep(0.5)
if __name__ == '__main__':
get_data()

View File

@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
ALLOWED_HOSTS = ['*']
SYS_NAME = '星途工厂综合管理系统'
SYS_VERSION = '2.7.2025082616'
SYS_VERSION = '2.8.2025101011'
X_FRAME_OPTIONS = 'SAMEORIGIN'
# Application definition

View File

@ -21,10 +21,11 @@ from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework.documentation import include_docs_urls
from django.views.generic import TemplateView
from server.settings import get_sysconfig
schema_view = get_schema_view(
openapi.Info(
title=settings.SYS_NAME,
title=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}',
default_version=settings.SYS_VERSION,
contact=openapi.Contact(email="caoqianming@foxmail.com"),
license=openapi.License(name="MIT License"),

View File

@ -1,7 +1,7 @@
#!/bin/bash
# 设置默认版本号 (格式: 2.7.YYYYMMDDHH)
DEFAULT_VERSION="2.7.$(date '+%Y%m%d%H')"
DEFAULT_VERSION="2.8.$(date '+%Y%m%d%H')"
# 获取参数 (起始tag)
TARGET_TAG="$1"