Compare commits

...

473 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
caoqianming c7514836cb release: 2.7.2025082616 2025-08-26 16:51:51 +08:00
caoqianming 316931fb40 feat: 添加mlog和handover的锁 2025-08-26 16:34:49 +08:00
caoqianming 8a039094cf feat: base 添加悲观锁及其装饰器 2025-08-26 16:34:25 +08:00
caoqianming f0b24da22f feat: number_to_batch 支持结合工序 2025-08-26 10:35:21 +08:00
caoqianming 5496058226 feat: 物料清单的导出 2025-08-25 15:37:05 +08:00
caoqianming 3810860f13 feat: mlogbw create 优化 2025-08-25 11:17:50 +08:00
caoqianming 0a6959a36c fix: mlogbw create 调换一下if顺序 2025-08-25 09:44:32 +08:00
caoqianming 4823acb004 fix: mlogbw create cal_count_notok bug 2025-08-25 08:58:47 +08:00
caoqianming 8b6d6fd668 fix: handover revert 可传入handler2 2025-08-21 20:17:02 +08:00
caoqianming c58c79a18a fix: handover revert 可传入handler 2025-08-21 20:13:09 +08:00
caoqianming 41a3418110 feat: material添加img字段 2025-08-21 17:34:39 +08:00
caoqianming 05955abd9c fix: base ComplexQueryMixin 默认null值排最后 2025-08-21 09:24:14 +08:00
caoqianming 0cf04c50b3 feat: 过滤来料未完成的优化 2025-08-20 17:39:43 +08:00
caoqianming 01e7d73bee feat: mlog quick添加mlogb 2025-08-20 15:03:58 +08:00
caoqianming 58024cc33d feat: batch_gzerp添加库存等字段 2025-08-19 16:01:18 +08:00
caoqianming 4921383de1 feat: check_sql_safe 优化一下 2025-08-19 15:11:03 +08:00
caoqianming bbfa512bb1 feat: quick返回mlogId 2025-08-19 11:32:48 +08:00
caoqianming d019e708cd feat: mlog submit revert支持放置到车间 2025-08-19 11:22:21 +08:00
caoqianming d9326bc264 feat: 可根据work_start_time 推出handle_date 2025-08-19 10:39:46 +08:00
caoqianming a8ed1c3083 feat: MlogInitSerializer 可传入handle_date 2025-08-19 10:35:45 +08:00
caoqianming 2bb1cf2844 feat: 去除commands命令权限 2025-08-19 08:56:29 +08:00
caoqianming 0dab1714f3 feat: 交接记录在审批状态下对撤销的处理 2025-08-18 16:52:23 +08:00
caoqianming fbce5ec64f feat: base retreat 撤回功能提到wfservice里 2025-08-18 16:51:23 +08:00
caoqianming 69e606bccc release: 2.7.2025081813 2025-08-18 13:45:47 +08:00
caoqianming efd25b040b fix: 导入出入库明细 2025-08-18 13:34:21 +08:00
caoqianming ee9fce675d feat: 导入出入库明细 2025-08-18 11:04:24 +08:00
caoqianming 7f619a36b0 feat: mlogbw增加type查询区分inout 2025-08-14 14:58:50 +08:00
caoqianming 84c701bdeb fix: 快速创建日志接口 2025-08-14 11:09:42 +08:00
caoqianming ca3e811b1b feat: 快速创建日志接口 2025-08-14 11:05:33 +08:00
caoqianming 1d778dbdf0 feat: 添加wamterial ava_qs方法 2025-08-14 09:48:23 +08:00
caoqianming c69c33e85a feat: mlogbw添加排序以支持直接展示 2025-08-14 08:44:23 +08:00
caoqianming 8ecd7d25b0 fix: mlogb__batch bug 2025-08-13 14:45:21 +08:00
caoqianming e03faeffc4 feat: WMaterialFilter filter tag优化 2025-08-13 10:03:00 +08:00
caoqianming c7b1c8a8b8 feat: mlogbw添加筛选条件2 2025-08-13 09:15:46 +08:00
caoqianming ddefbd49b2 feat: mlogbw添加筛选条件 2025-08-13 08:44:56 +08:00
caoqianming 10745394e3 feat: MIOItemCreateSerializer 添加pack_index4 2025-08-08 16:44:59 +08:00
caoqianming 82f6f4ee5e feat: MIOItemCreateSerializer 添加pack_index3 2025-08-08 14:34:57 +08:00
caoqianming 46c9c5abab feat: MIOItemCreateSerializer 添加pack_index2 2025-08-08 13:53:21 +08:00
caoqianming 09e390b37b feat: MIOItemCreateSerializer 添加pack_index 2025-08-08 13:47:25 +08:00
caoqianming afb27246d5 release: 2.7.2025080716 2025-08-07 16:06:17 +08:00
caoqianming 98a9806c4b feat: MaterialBatch返回can_mio2 2025-08-07 15:44:04 +08:00
caoqianming d3f83605aa feat: MaterialBatch返回can_mio 2025-08-07 15:39:43 +08:00
caoqianming 783f288926 feat: wpr 不做删除 2025-08-07 15:07:03 +08:00
caoqianming 40eb07eb12 feat: number_out_last 改为原始sql查询 2025-08-07 14:38:06 +08:00
caoqianming 835c5f78dd feat: base query_one_dict优化 2025-08-07 14:37:41 +08:00
caoqianming e30241dc22 feat: mioitemw 返回wpr_number_out 2025-08-07 14:25:55 +08:00
caoqianming b9376cc2d6 feat: 捕获number_out_last异常 2025-08-07 13:47:57 +08:00
caoqianming 05fcc4a00a feat: mlogbw返回wpr_number_out 2025-08-07 13:44:17 +08:00
caoqianming cf25d0d280 feat: 获取物料的缺陷项列表 2025-08-07 11:08:29 +08:00
caoqianming c7f6abf6a6 feat: route添加params_json字段 2025-08-07 10:37:59 +08:00
caoqianming a42d48775c feat: wpr增加can_use筛选条件2 2025-08-07 10:05:33 +08:00
caoqianming aa72953d17 feat: wpr增加can_use筛选条件 2025-08-07 09:31:53 +08:00
caoqianming 925666d3ef fix: number_out_last获取bug2 2025-08-06 14:29:08 +08:00
caoqianming b7823d56a8 fix: number_out_last获取bug 2025-08-06 08:59:29 +08:00
caoqianming 0a6de249ee feat: 光子批次统计增加组合件信息7 2025-08-05 16:02:58 +08:00
caoqianming d130f75076 feat: 光子批次统计增加组合件信息6 2025-08-05 16:01:01 +08:00
caoqianming dbde5a1327 feat: mlog 创建和编辑时可变更team字段 2025-08-05 15:45:30 +08:00
caoqianming 9b4a44be51 feat: get_qct 未有检验模板 2025-08-05 15:10:00 +08:00
caoqianming 05ea28f334 fix: wpr number_out_last 前缀参数 2025-08-05 14:31:34 +08:00
caoqianming c14229f6f5 feat: 光子批次统计增加组合件信息4 2025-08-05 13:19:45 +08:00
caoqianming 94b0719f38 feat: 光子批次统计增加组合件信息3 2025-08-05 11:24:36 +08:00
caoqianming 24a389d566 feat: 光子批次统计增加组合件信息2 2025-08-05 11:17:27 +08:00
caoqianming 8a2244a0cc feat: ftestwork_submit 触发批次统计分析 2025-08-05 10:09:46 +08:00
caoqianming 1c24e9948b feat: 光子批次统计增加组合件信息 2025-08-04 15:33:22 +08:00
caoqianming 763c64d5d0 feat: wmaterial添加筛选条件defect__isnull 2025-08-04 15:18:38 +08:00
caoqianming 3251c87eb3 fix: mioitem 返回信息时展示组合件信息2 2025-08-04 11:32:50 +08:00
caoqianming c790b3bd4a fix: mioitem 返回信息时展示组合件信息 2025-08-04 11:30:46 +08:00
caoqianming 884b10d6bc feat: mioitem 返回信息时展示组合件信息 2025-08-04 11:21:35 +08:00
caoqianming 7d2fee8409 feat: 仅合格品支持退回 2025-08-04 10:09:23 +08:00
caoqianming 6a93ca31af release: 2.7.2025080409 2025-08-04 09:19:45 +08:00
caoqianming 7f89e8a72d feat: ftestwork_submit 关于不合格逻辑的bug 2025-08-01 17:11:09 +08:00
caoqianming 011577f9a5 fix: mioitem 检索bug 2025-08-01 15:31:43 +08:00
caoqianming 9b5e1bd9a7 feat: 出入库记录支持对子件批次
的搜索
2025-08-01 15:05:45 +08:00
caoqianming 28900d3218 feat: 装箱字段简化 2025-08-01 14:16:54 +08:00
caoqianming 393e528434 feat: 注册装箱功能路由2 2025-08-01 13:50:43 +08:00
caoqianming 2f0ce5c171 feat: 注册装箱功能路由 2025-08-01 13:47:46 +08:00
caoqianming 04d4c560ef feat: 装箱操作 2025-08-01 11:31:02 +08:00
caoqianming 5514e8b561 feat: 支持消耗物料的检验3 2025-08-01 11:12:46 +08:00
caoqianming 264083ebd5 feat: 支持消耗物料的检验2 2025-08-01 10:57:18 +08:00
caoqianming 808b8ac229 feat: 支持消耗物料的检验 2025-08-01 10:44:03 +08:00
caoqianming 70069a5a07 feat: WproutSerializer 支持传入none 2025-07-31 15:12:15 +08:00
caoqianming 33709bbb60 feat: assgin_number_out 支持置空 2025-07-31 15:02:57 +08:00
caoqianming bbef553bd5 fix: assgin_number_out bug 2025-07-31 14:40:53 +08:00
caoqianming 1fccdd042c feat: 添加发货单 2025-07-31 14:05:02 +08:00
caoqianming de953085cd fix: 销售发货时update_mb_item的bug 2025-07-31 13:46:39 +08:00
caoqianming 54345d2bce release: 2.7.2025073110 2025-07-31 10:54:58 +08:00
caoqianming ad9ef9efa5 feat: 通过fmlog查询batch 2025-07-31 10:41:16 +08:00
caoqianming 06de49f335 feat: 优化number_out_last 2025-07-31 09:31:10 +08:00
caoqianming eebc5238ae feat: MIOItem序列化器添加note 2025-07-30 16:02:18 +08:00
caoqianming 5755c508d7 feat: wpr分配出库对外编号 序列化器改动 2025-07-30 15:55:04 +08:00
caoqianming 84198f4d91 feat: wpr分配出库对外编号 2025-07-30 15:35:06 +08:00
caoqianming b5889374eb feat: 来料未完成的筛选控制物料类型为半成品、成品 2025-07-30 13:27:35 +08:00
caoqianming c98ce36c16 feat: 添加number_out_last的接口说明 2025-07-29 10:03:59 +08:00
caoqianming 5c21d6042b feat: 销售发货时可以设置对外编号 2025-07-28 16:56:55 +08:00
caoqianming 7f31ec8add feat: 物料导入模板及逻辑修改 2025-07-28 14:58:27 +08:00
caoqianming 739c4b0d65 fix: purout操作后续的bug 2025-07-28 14:31:33 +08:00
caoqianming b45eadc53c fix: 出入库类型错误bug 2025-07-28 14:25:55 +08:00
caoqianming 5d335fdd6a feat: mio创建时type得传入并校验 2025-07-28 13:50:28 +08:00
caoqianming 382a5a827c feat: 支持领用出库和退还入库 2025-07-28 13:41:16 +08:00
caoqianming b3b77db0c1 feat: wm todo排除repaired 2025-07-27 13:15:35 +08:00
caoqianming fa38cfdbdd feat: 交接记录关联审批暂不支持撤销 2025-07-25 22:53:49 +08:00
caoqianming 45c224c10e feat: handover返回ticket_字段 2025-07-25 16:18:53 +08:00
caoqianming b1f6798c65 feat: dataset添加enabled字段 2025-07-25 11:34:56 +08:00
caoqianming 0c7c1e4f17 feat: 支持采购退货 2025-07-24 10:50:11 +08:00
caoqianming 2fbe1620fb feat: wpr_bxerp 返工的不纳入统计 2025-07-23 13:43:11 +08:00
caoqianming 66a1d6df7f feat: 交接记录添加审批工单 2025-07-23 09:20:38 +08:00
caoqianming 3cacea1b06 feat: 批次统计分析时logerror后return 2025-07-22 15:56:26 +08:00
caoqianming a91b75ae7a feat: 获取qct type参数获取错误 2025-07-22 13:38:48 +08:00
caoqianming c35cdb6d0a feat: qct 增加筛选条件 2025-07-18 16:32:47 +08:00
caoqianming d732652c9f feat: qctmat添加字段以支持消耗物料参数填写 2025-07-18 16:01:32 +08:00
caoqianming 3985401361 feat: mlogbw编辑时可删除ftest3 2025-07-18 10:15:43 +08:00
caoqianming ec83674c6a feat: mlogbw编辑时可删除ftest2 2025-07-18 10:10:10 +08:00
caoqianming f955c8bd8c feat: mlogbw编辑时可删除ftest 2025-07-18 10:07:47 +08:00
caoqianming 357b1fdfa0 feat: wpr_bxerp只统计提交的日志 2025-07-18 09:53:43 +08:00
caoqianming 9c391add48 feat: wpr_bxerp调整 2025-07-18 09:23:22 +08:00
caoqianming 4e1c33317c fix: mlogbw返回equip相关信息 2025-07-17 15:04:17 +08:00
caoqianming 80b265b243 feat: mlogbw返回equip相关信息 2025-07-17 14:56:26 +08:00
caoqianming 1bc9cd2c60 feat: 批次统计分析中关于decimal的处理 2025-07-17 10:26:17 +08:00
caoqianming c0d504ecfe feat: batch_bxerp优化 2025-07-17 10:21:08 +08:00
caoqianming 7fd47c2a97 feat: 修改batchst data默认为dict 2025-07-17 10:20:48 +08:00
caoqianming e2a8da9eb4 feat: 优化cd.py 2025-07-17 09:58:56 +08:00
caoqianming 47cfa7ce17 feat: cd.py清空缓冲区后再执行 2025-07-17 09:45:41 +08:00
caoqianming 17996a0b86 feat: 单个统计优化 2025-07-17 09:01:49 +08:00
caoqianming 69c7da883f feat: gx bx的批次统计添加大小日期 2025-07-16 16:29:20 +08:00
caoqianming 811e971b4b fix: wpr_data 副本 2025-07-16 16:25:33 +08:00
caoqianming e8eeb14766 feat: 批次统计分析重构 2025-07-16 14:53:40 +08:00
caoqianming bdf69a6635 release: 2.7.2025071516 2025-07-15 16:04:20 +08:00
caoqianming dbfe308154 fix: 生成dag时为防止边重复给edge设置ID 2025-07-15 15:39:47 +08:00
caoqianming 18abcc5c9f feat: ftestwork添加cbatch查询条件 2025-07-15 14:23:44 +08:00
caoqianming 5eac4240fb feat: 出入库记录缺少明细不让操作 2025-07-15 13:47:01 +08:00
caoqianming e6850c92cb feat: 光子batch统计增加大小日期 2025-07-15 13:46:41 +08:00
caoqianming c3337cfa2a feat: 导入daoru_mioitem_test单元格公式的处理 2025-07-15 10:21:29 +08:00
caoqianming fc572d30e3 feat: ftestprocess update 支持新的检测项和缺陷项 2025-07-15 09:49:58 +08:00
caoqianming 528604d24f fix: ftestprocess update 更详细的报错信息 2025-07-15 09:43:49 +08:00
caoqianming 5285bf0252 feat: ftestprocess update 更详细的报错信息 2025-07-15 09:42:03 +08:00
caoqianming 8ba233f2fd feat: 数采做成单独服务3 2025-07-14 16:39:11 +08:00
caoqianming 9e62bf8d41 feat: 数采做成单独服务2 2025-07-14 13:50:50 +08:00
caoqianming 3fe723b397 feat: 数采做成单独服务 2025-07-14 13:42:29 +08:00
caoqianming b8c799b938 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-07-14 10:53:24 +08:00
caoqianming 5a98d1bea7 feat: 测试cd 2025-07-14 10:48:31 +08:00
caoqianming 4b52c28b5e fix: batchst并发创建的处理 2025-07-10 13:24:00 +08:00
caoqianming e2c7847c74 feat: gen_number_with_rule 偏离6小时 2025-07-08 16:41:25 +08:00
caoqianming 024cae6fa3 feat: base complexquery value支持多种类型 2025-07-07 15:38:59 +08:00
caoqianming 523b4b2a42 feat: complexquery 支持isnull 2025-07-07 15:27:04 +08:00
caoqianming d76d77fa49 feat: wpr 增加复杂查询接口 2025-07-07 14:49:57 +08:00
caoqianming 3858b3b9f9 feat: 返工不限制物料选择 2025-07-07 14:49:38 +08:00
caoqianming 05ebb25dc7 feat: 按个选入消耗时统计mlog数据优化2 2025-07-07 11:16:55 +08:00
caoqianming 45647d2149 feat: 按个选入消耗时统计mlog数据优化 2025-07-07 11:08:29 +08:00
caoqianming 28acc48768 feat: mlog添加操作时间校验 2025-07-07 10:44:31 +08:00
caoqianming 30bb96cf6c feat: 按个选入消耗时统计mlog数据 2025-07-07 10:34:40 +08:00
caoqianming e57e087006 fix: 优化ana_wpr4 2025-07-04 22:17:02 +08:00
caoqianming 33f79b5cc5 feat: 优化ana_wpr3 2025-07-04 20:14:58 +08:00
caoqianming 14f3b215c4 feat: BatchSt.g_create取消传入check_mat 2025-07-04 13:22:45 +08:00
caoqianming e9059c93bd feat: 优化ana_wpr2 2025-07-04 11:02:34 +08:00
caoqianming 662bef04ab feat: 优化ana_wpr 2025-07-04 10:51:22 +08:00
caoqianming 403477afd0 feat: wpr_bxerp返回批号 2025-07-04 10:08:16 +08:00
caoqianming 1e128041cd fix: 日志多步骤导致的返工填写bug2 2025-07-04 08:50:03 +08:00
caoqianming 433218feed fix: 日志多步骤导致的返工填写bug 2025-07-04 08:35:46 +08:00
caoqianming e3067e115a feat: 获取该批次的DAG图数据 支持仅直接关系 2025-07-03 14:54:20 +08:00
caoqianming 79f1322c27 feat: 批次追踪链优化2 2025-07-03 14:38:16 +08:00
caoqianming 17284dcf4f feat: 批次追踪链优化 2025-07-03 13:58:31 +08:00
caoqianming 89aca50f44 fix: 正常交接收料工段与送料工段不能相同 2025-07-03 11:36:18 +08:00
caoqianming 0284117297 release: 2.7.2025070310 2025-07-03 11:01:01 +08:00
caoqianming dd5ef95fa4 feat: ana_wpr忽略模块导入错误 2025-07-03 10:57:10 +08:00
caoqianming 1a621aca34 feat: 生产日志返回提交人姓名 2025-07-02 16:41:54 +08:00
caoqianming 4b2bcc6d29 feat: 修改mlog_submit和mlog_revert以支持日志多个工艺步骤 2025-07-02 15:40:59 +08:00
caoqianming 1d840e7c9d feat: 正常交接收料工段与送料工段不能相同 2025-07-02 10:46:11 +08:00
caoqianming e2f3c95748 feat: handover_revert自己交给自己无需处理 2025-07-02 10:38:01 +08:00
caoqianming 864fb783dc fix: base ticket list 添加create_by_name 2025-07-01 09:34:30 +08:00
caoqianming 8a4ddd0307 feat: base ticket list 添加create_by_name 2025-07-01 09:09:18 +08:00
caoqianming 620e547da5 feat: 单日志支持多工艺步骤 2025-06-30 16:17:09 +08:00
caoqianming a3b0455fb4 feat: 单个统计的触发 2025-06-30 15:07:00 +08:00
caoqianming fdf717aecb fix: 针对未知bug先添加拦截校验 2025-06-30 09:41:08 +08:00
caoqianming a5e8fe3dde feat: wpr data字段数据库同步 2025-06-27 17:14:02 +08:00
caoqianming 5ae9030152 feat: 优化wpr change_or_new 2025-06-27 16:52:15 +08:00
caoqianming e86f4e79d4 feat: mlog添加cnumber查询条件 2025-06-27 16:49:50 +08:00
caoqianming 76cdd08e9a fix: wpr change_or_new时根据number获取最后一个 2025-06-27 15:09:27 +08:00
caoqianming a0ed107491 fix: 出入库明细撤回bug 2025-06-27 15:02:01 +08:00
caoqianming e6d8509971 feat: 出入库明细支持撤回并删除-在一个事务2 2025-06-27 13:34:38 +08:00
caoqianming c054095f94 feat: 出入库明细支持撤回并删除-在一个事务 2025-06-27 13:33:34 +08:00
caoqianming 10b63cdad5 feat: 出入库明细支持撤回并删除2 2025-06-27 13:31:54 +08:00
caoqianming 30379bdab5 feat: 出入库明细支持撤回并删除 2025-06-27 13:29:39 +08:00
caoqianming 1bb449c0db feat: batch_gxerp添加count_notok 2025-06-26 13:56:39 +08:00
caoqianming e25f392175 feat: handover_revert尝试支持全部撤回2 2025-06-26 11:02:29 +08:00
caoqianming c4cde1c5ea feat: handover_revert尝试支持全部撤回 2025-06-26 10:54:32 +08:00
caoqianming a6d53471fc fix: batch_gxerp添加count_pn_jgqbl 2 2025-06-26 09:14:52 +08:00
caoqianming d03f106055 feat: batch_gxerp 添加need_inout查询条件 2025-06-26 09:07:12 +08:00
caoqianming b500ad6615 feat: wpr添加对外编号字段 2025-06-25 18:19:16 +08:00
caoqianming f3263caed0 feat:: batch_gxerp添加count_pn_jgqbl 2025-06-25 17:33:50 +08:00
caoqianming ab22415e0f feat: ofm 完善1 2025-06-25 17:29:46 +08:00
caoqianming 31f4e2869d feat: sql查询有风险提示更完善 2025-06-25 11:23:59 +08:00
caoqianming 43e9642917 fix: 光子get_alldata_with_batch 其他入库获取错误2 2025-06-24 14:34:14 +08:00
caoqianming c338509854 fix: 光子get_alldata_with_batch 人员获取错误2 2025-06-24 14:28:22 +08:00
caoqianming 5dddbfef81 release: 2.7.2025062414 2025-06-24 14:08:15 +08:00
caoqianming 037008d7c9 fix: 光子get_alldata_with_batch 人员获取错误 2025-06-24 13:52:44 +08:00
caoqianming 9ff72b2da1 fix: 光子get_alldata_with_batch优化2 2025-06-24 11:23:45 +08:00
caoqianming 53972fd22a feat: 光子get_alldata_with_batch优化 2025-06-24 09:30:09 +08:00
caoqianming cb09cee753 feat: 添加行政管理App 2025-06-24 08:47:22 +08:00
caoqianming b0df18f01a feat: cd 设置清空缓冲区2 2025-06-23 17:49:02 +08:00
caoqianming c5e4cc48a4 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-23 17:00:41 +08:00
caoqianming 8e33e084b4 feat: cd 设置清空缓冲区 2025-06-23 17:00:39 +08:00
zty ce54f326ee Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-23 13:55:55 +08:00
zty 1242b3d43a feat:enm/service修改排班记录生成时间 2025-06-23 13:55:53 +08:00
caoqianming 108fe0ae9f feat: mlogbout单个产品不支持直接修改 2025-06-23 13:44:28 +08:00
caoqianming f0cacf6777 feat: mioitem添加material__type查询条件 2025-06-20 15:28:49 +08:00
caoqianming 50bf3e78d4 release: 2.6.2025062015 2025-06-20 15:20:49 +08:00
caoqianming 307935cb16 fix: wpr不合格的话先save再创建wm.defect 2025-06-20 13:28:55 +08:00
caoqianming 3683ddbdeb Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-20 11:11:32 +08:00
caoqianming fec244765a fix: 该交接单不支持撤销的bug 2025-06-20 11:11:31 +08:00
zty 6a5ec34ef8 Merge branch 'master' of https://e.coding.net/ctcdevteam/ehs/ehs_server 2025-06-20 09:44:57 +08:00
zty 7942a5168b fix: wpm/service 优化煤粉热值获取方式 2025-06-20 09:44:52 +08:00
caoqianming db2e8934ba feat: 增加交接撤回功能 2025-06-19 17:25:33 +08:00
caoqianming 4f08374ca6 feat: mlogbin的bug 2025-06-19 15:43:02 +08:00
caoqianming dce573f892 fix: mioitem price 计算bug 2025-06-19 14:23:49 +08:00
caoqianming 3b04bdf067 feat: mioitem返回mio数据 2025-06-19 14:11:24 +08:00
caoqianming 741b64142b feat: mioitem添加单价字段 2025-06-19 10:37:09 +08:00
caoqianming 25c1cce41a feat: materialsimpleserializer返回bin_number_main 2025-06-18 16:30:41 +08:00
caoqianming 8805237a44 feat: 物料增加主库位号 2025-06-18 16:29:38 +08:00
caoqianming a819566c23 fix: wmaterial添加can_handover字段 2025-06-18 10:56:39 +08:00
caoqianming 6af708d4f0 feat: wmaterial添加can_handover字段 2025-06-18 10:31:35 +08:00
135 changed files with 7236 additions and 2191 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

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

View File

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

View File

@ -12,9 +12,12 @@ def check_sql_safe(sql: str):
"""检查sql安全性
"""
sql_upper = sql.upper()
# 将SQL按空格和分号分割成单词
words = [word for word in sql_upper.replace(';', ' ').split() if word]
for kw in forbidden_keywords:
if kw in sql_upper:
raise ParseError('sql查询有风险')
# 检查关键字是否作为独立单词出现
if kw in words:
raise ParseError(f'sql查询有风险-{kw}')
return sql
def format_json_with_placeholders(json_str, **kwargs):

View File

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

View File

@ -102,7 +102,7 @@ class LabelTemplateViewSet(CustomModelViewSet):
serializer_class = LabelTemplateSerializer
filterset_class = LabelTemplateFilter
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer)
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"})
def commands(self, request, *args, **kwargs):
"""
获取标签指令

View File

@ -4,9 +4,10 @@ import json
import time
from django.core.cache import cache
from apps.utils.thread import MyThread
import struct
import uuid
import logging
import threading
import requests
myLogger = logging.getLogger('log')
@ -15,7 +16,7 @@ def get_checksum(body_msg):
def handle_bytes(arr):
if len(arr) < 8:
return "返回数据长度错误"
return f"返回数据长度错误-{arr}"
if arr[0] != 0xEB or arr[1] != 0x90:
return "数据头不正确"
@ -133,8 +134,9 @@ def get_tyy_data_2(*args, retry=1):
sc_all = {}
sc_lock = threading.Lock()
def get_tyy_data(*args):
def get_tyy_data_1(*args, retry=1):
host, port = args[0], int(args[1])
global sc_all
sc = None
@ -148,6 +150,17 @@ def get_tyy_data(*args):
sc.settimeout(5) # 设置超时
sc.connect((host, port))
sc_all[f"{host}_{port}"] = sc
else:
# 清空接收缓冲区
sc.settimeout(0.1) # 设置短暂超时
for _ in range(5):
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5) # 恢复原超时设置
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
@ -185,12 +198,55 @@ def get_tyy_data(*args):
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接超时-{str(e)}")
with sc_lock:
connect_and_send()
resp = sc.recv(1024)
res = handle_bytes(resp)
# myLogger.error(res)
if isinstance(res, str):
raise ParseError(f'采集器返回数据错误-{res}')
else:
return res
def get_tyy_data_3(*args, retry=2):
host, port = args[0], int(args[1])
for attempt in range(retry):
try:
# 每次请求都新建连接(确保无共享状态)
with socket.create_connection((host, port), timeout=10) as sc:
sc.sendall(b"R")
# 接收完整响应(避免数据不完整)
# resp = b""
# while True:
# chunk = sc.recv(4096)
# if not chunk:
# break
# resp += chunk
resp = sc.recv(4096)
if not resp:
raise ParseError("设备未启动")
res = handle_bytes(resp)
if isinstance(res, str):
raise ParseError(f"采集器返回数据错误: {res}")
return res
except (socket.timeout, ConnectionError) as e:
if attempt == retry - 1: # 最后一次尝试失败才报错
raise ParseError(f"采集器连接失败: {str(e)}")
time.sleep(0.5) # 失败后等待 1s 再重试
except ParseError:
raise
except Exception as e:
raise ParseError(f"未知错误: {str(e)}")
connect_and_send()
resp = sc.recv(1024)
res = handle_bytes(resp)
# myLogger.error(res)
if isinstance(res, str):
raise ParseError(f'采集器返回数据错误-{res}')
else:
return res
def get_tyy_data(*args):
host, port = args[0], int(args[1])
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")
res = r.json()
if "err_msg" in res:
raise ParseError(res["err_msg"])
return res

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
@ -271,6 +283,9 @@ class InmService:
"""
更新库存, 支持反向操作
"""
if not MIOItem.objects.filter(mio=instance).exists():
raise ParseError("出入库记录缺失明细,无法操作")
if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单
# 这里还需要对入厂检验进行处理
if is_reverse:
@ -278,25 +293,32 @@ class InmService:
else:
for item in MIOItem.objects.filter(mio=instance):
BatchSt.g_create(
batch=item.batch, mio=instance, material_start=item.material, reuse_node=True)
batch=item.batch, mio=instance, material_start=item.material)
from apps.pum.services import PumService
if is_reverse:
cls.update_mb(instance, -1)
else:
cls.update_mb(instance, 1)
PumService.mio_purin(instance, is_reverse)
PumService.mio_pur(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_PUR_OUT:
from apps.pum.services import PumService
if is_reverse:
cls.update_mb(instance, 1)
else:
cls.update_mb(instance, -1)
PumService.mio_pur(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_OTHER_IN:
if is_reverse:
BatchLog.clear(mio=instance)
else:
for item in MIOItem.objects.filter(mio=instance):
BatchSt.g_create(
batch=item.batch, mio=instance, material_start=item.material, reuse_node=True)
batch=item.batch, mio=instance, material_start=item.material)
if is_reverse:
cls.update_mb(instance, -1)
else:
cls.update_mb(instance, 1)
elif instance.type == MIO.MIO_TYPE_DO_IN:
elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
@ -304,6 +326,14 @@ class InmService:
else:
for item in mioitems:
do_in(item)
elif instance.type in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT]:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
do_in(item)
else:
for item in mioitems:
do_out(item)
elif instance.type == MIO.MIO_TYPE_SALE_OUT:
from apps.sam.services import SamService
if is_reverse:
@ -316,14 +346,6 @@ class InmService:
cls.update_mb(instance, 1)
else:
cls.update_mb(instance, -1)
elif instance.type == MIO.MIO_TYPE_DO_OUT:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
do_in(item)
else:
for item in mioitems:
do_out(item)
else:
raise ParseError('不支持该出入库操作')
@ -349,6 +371,7 @@ class InmService:
out = -1
默认使用count字段做加减
"""
mio_type = i.mio.type
material: Material = i.material
warehouse = i.warehouse
tracking = material.tracking
@ -387,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,
@ -419,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:
@ -430,7 +453,7 @@ class InmService:
if mioitemws.count() != change_count:
raise ParseError("出入库与明细数量不一致,操作失败")
for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb)
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb, number_out=mioitemw.number_out)
else:
raise ParseError("不支持的操作")
@ -438,3 +461,26 @@ class InmService:
ana_batch_thread(xbatchs)
@classmethod
def revert_and_del(cls, mioitem: MIOItem):
mio = mioitem.mio
if mio.submit_time is None:
raise ParseError("未提交的出入库明细不允许撤销")
if mioitem.test_date is not None:
raise ParseError("已检验的出入库明细不允许撤销")
if mio.type == MIO.MIO_TYPE_PUR_IN:
from apps.pum.services import PumService
cls.update_mb_item(mioitem, -1)
BatchLog.clear(mioitem=mioitem)
PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem)
mioitem.delete()
elif mio.type == MIO.MIO_TYPE_OTHER_IN:
cls.update_mb_item(mioitem, -1)
BatchLog.clear(mioitem=mioitem)
mioitem.delete()
elif mio.type == MIO.MIO_TYPE_DO_OUT:
do_in(mioitem)
BatchLog.clear(mioitem=mioitem)
mioitem.delete()
else:
raise ParseError("不支持该出入库单明细撤销")

View File

@ -1,6 +1,6 @@
from rest_framework.exceptions import ParseError
from apps.mtm.models import Process, Material
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw, MIO
from apps.utils.tools import ranstr
from apps.mtm.services_2 import cal_material_count
@ -67,7 +67,7 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
from openpyxl import load_workbook
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
qct = Qct.get(mioitem.material, tag="inm")
qct = Qct.get(mioitem.material, tag="inm", type="in")
if qct is None:
raise ParseError("未找到检验表")
@ -84,8 +84,11 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
test_user = mioitem.mio.mio_user
test_date = mioitem.mio.inout_date
wb = load_workbook(path)
sheet = wb["Sheet1"]
wb = load_workbook(path, data_only=True)
if "Sheet1" in wb.sheetnames: # 检查是否存在
sheet = wb["Sheet1"] # 获取工作表
else:
raise ParseError("未找到Sheet1")
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
@ -120,4 +123,39 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id()))
FtestItem.objects.bulk_create(ftestitems)
else:
break
break
def daoru_mioitems(path:str, mio:MIO):
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path, data_only=True)
if "Sheet1" in wb.sheetnames: # 检查是否存在
sheet = wb["Sheet1"] # 获取工作表
else:
raise ParseError("未找到Sheet1")
mioitems = []
ind = 2
while sheet[f"a{ind}"].value:
batch = sheet[f"b{ind}"].value
material_number = sheet[f"a{ind}"].value
try:
material = Material.objects.get(number=material_number)
except Exception as e:
raise ParseError(f"未找到物料:{material_number} {e}")
if batch:
pass
else:
batch = ""
count = sheet[f"c{ind}"].value
warehouse_name = sheet[f"d{ind}"].value
try:
warehouse = WareHouse.objects.get(name=warehouse_name)
except Exception as e:
raise ParseError(f"未找到仓库:{warehouse_name} {e}")
mioitems.append(MIOItem(mio=mio, warehouse=warehouse, material=material, batch=batch, count=count, id=idWorker.get_id()))
ind = ind + 1
MIOItem.objects.bulk_create(mioitems)

View File

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

View File

@ -9,22 +9,25 @@ from django.utils import timezone
from rest_framework.response import Response
from django.db.models import Sum
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack
from apps.inm.serializers import (
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
MIOItemwSerializer)
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer)
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.inm.services import InmService
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test, daoru_mioitems
from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin,
CustomListModelMixin)
from apps.utils.permission import has_perm
from .filters import MaterialBatchFilter, MioFilter
from apps.qm.serializers import FtestProcessSerializer
from apps.mtm.models import Material
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from django.db import connection
# Create your views here.
@ -145,9 +148,19 @@ class MIOViewSet(CustomModelViewSet):
serializer_class = MIOListSerializer
retrieve_serializer_class = MIODetailSerializer
filterset_class = MioFilter
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model']
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model',
'item_mio__a_mioitem__batch']
data_filter = True
@classmethod
def lock_and_check_can_update(cls, mio:MIO):
if not connection.in_atomic_block:
raise ParseError("请在事务中调用该方法")
mio:MIO = MIO.objects.select_for_update().get(id=mio.id)
if mio.submit_time is not None:
raise ParseError("该记录已提交无法更改")
return mio
def add_info_for_list(self, data):
# 获取检验状态
mio_dict = {}
@ -168,7 +181,7 @@ class MIOViewSet(CustomModelViewSet):
if self.action in ['create', 'update', 'partial_update']:
type = self.request.data.get('type')
user = self.request.user
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT]:
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]:
if not has_perm(user, ['mio.do']):
raise PermissionDenied
return MIODoSerializer
@ -180,7 +193,7 @@ class MIOViewSet(CustomModelViewSet):
if not has_perm(user, ['mio.sale']):
raise PermissionDenied
return MIOSaleSerializer
elif type == MIO.MIO_TYPE_PUR_IN:
elif type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]:
if not has_perm(user, ['mio.pur']):
raise PermissionDenied
return MIOPurSerializer
@ -192,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):
"""撤回
@ -224,16 +239,84 @@ class MIOViewSet(CustomModelViewSet):
raise ParseError('记录状态异常')
if ins.submit_user != user:
raise ParseError('非提交人不可撤回')
with transaction.atomic():
ins.submit_time = None
ins.state = MIO.MIO_CREATE
ins.update_by = user
ins.save()
InmService.update_inm(ins, is_reverse=True)
ins.submit_user = None
ins.update_by = user
ins.state = MIO.MIO_CREATE
ins.submit_time = None
ins.save()
InmService.update_inm(ins, is_reverse=True)
InmService.update_material_count(ins)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
@transaction.atomic
def pack_mioitem(self, request, *args, **kwargs):
"""装箱
装箱
"""
mio:MIO = self.get_object()
if mio.submit_time is not None:
raise ParseError('该出入库已提交不可装箱')
sr = PackMioSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
pack_index = vdata["pack_index"]
mioitems = vdata["mioitems"]
if not mioitems:
raise ParseError('未选择明细')
for id in mioitems:
mioitem = MIOItem.objects.get(id=id)
if mioitem.mio != mio:
raise ParseError('存在明细不属于该箱')
mioitem.pack_index = pack_index
mioitem.save(update_fields=['pack_index', 'update_time'])
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
def daoru_mioitem(self, request, *args, **kwargs):
"""导入明细
导入明细
"""
daoru_mioitems(settings.BASE_DIR + request.data.get('path', ''), mio=self.get_object())
return Response()
class PackViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
"""
list: 装箱记录
装箱记录
"""
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
queryset = Pack.objects.all()
serializer_class = PackSerializer
filterset_fields = ["mio"]
ordering = ["mio", "index"]
@action(methods=['post'], detail=False, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
@transaction.atomic
def pack_mioitem(self, request, *args, **kwargs):
"""装箱
装箱
"""
vdata = PackMioSerializer(data=request.data)
packId = vdata["pack"]
pack:Pack = Pack.objects.get(id=packId)
mioitems = vdata["mioitems"]
if not mioitems:
raise ParseError('未选择明细')
for id in mioitems:
mioitem = MIOItem.objects.get(id=id)
if mioitem.mio != pack.mio:
raise ParseError('存在明细不属于该装箱记录')
mioitem.pack = pack
mioitem.save(update_fields=['pack', 'update_time'])
return Response()
class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
"""
list: 出入库明细
@ -243,6 +326,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
queryset = MIOItem.objects.all()
serializer_class = MIOItemSerializer
retrieve_serializer_class = MioItemDetailSerializer
create_serializer_class = MIOItemCreateSerializer
select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
filterset_fields = {
@ -252,22 +336,58 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
"mio__type": ["exact", "in"],
"mio__inout_date": ["gte", "lte", "exact"],
"material": ["exact"],
"material__type": ["exact"],
"test_date": ["isnull", "exact"]
}
ordering = ['create_time']
ordering_fields = ['create_time', 'test_date']
search_fields =['batch', 'a_mioitem__batch']
def add_info_for_list(self, data):
with_mio = self.request.query_params.get('with_mio', "no")
if with_mio == "yes" and isinstance(data, list):
mio_ids = [item['mio'] for item in data]
mio_qs = MIO.objects.filter(id__in=mio_ids)
mio_qs_= MIOListSerializer(mio_qs, many=True).data
mio_dict = {mio['id']: mio for mio in mio_qs_}
for item in data:
mioId = item['mio']
item['mio_'] = mio_dict[mioId]
return data
@swagger_auto_schema(manual_parameters=[
openapi.Parameter(name="with_mio", in_=openapi.IN_QUERY, description="是否返回出入库记录信息",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)",
type=openapi.TYPE_STRING, required=False)
])
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio'])
return super().perform_create(serializer)
def perform_destroy(self, instance):
if instance.mio.state != MIO.MIO_CREATE:
raise ParseError('出入库记录非创建中不可删除')
MIOViewSet.lock_and_check_can_update(instance.mio)
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
raise PermissionDenied('无权限删除')
return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
@transaction.atomic
def revert_and_del(self, request, *args, **kwargs):
"""撤回并删除
撤回并删除
"""
ins:MIOItem = self.get_object()
InmService.revert_and_del(ins)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer)
@transaction.atomic
def test(self, request, *args, **kwargs):
@ -367,7 +487,7 @@ class MIOItemwViewSet(CustomModelViewSet):
perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'}
queryset = MIOItemw.objects.all()
serializer_class = MIOItemwCreateUpdateSerializer
filterset_fields = ['mioitem']
filterset_fields = ['mioitem', 'wpr']
ordering = ["number", "create_time"]
ordering_fields = ["number", "create_time"]
@ -383,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,18 @@
# Generated by Django 3.2.12 on 2025-06-18 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0058_process_wpr_number_rule'),
]
operations = [
migrations.AddField(
model_name='material',
name='bin_number_main',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='主库位号'),
),
]

View File

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

View File

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

View File

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

View File

@ -4,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):
"""
@ -42,6 +44,12 @@ class Process(CommonBModel):
"""获取可产出的materialIds
"""
return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct())
def get_canin_mat_ids(self):
"""获取可输入的materialIds
"""
return list(RouteMat.objects.filter(route__process=self).values_list("material__id", flat=True).distinct()) + \
list(Route.objects.filter(process=self).values_list("material_in__id", flat=True).distinct())
# Create your models here.
class Material(CommonAModel):
@ -101,6 +109,8 @@ class Material(CommonAModel):
brothers = models.JSONField('兄弟件', default=list, null=False, blank=True)
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
into_wm = models.BooleanField('是否进入车间库存', default=True)
bin_number_main = models.CharField('主库位号', max_length=50, null=True, blank=True)
img = models.TextField('图片', null=True, blank=True)
class Meta:
verbose_name = '物料表'
@ -169,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):
@ -324,7 +360,8 @@ class RoutePack(CommonADModel):
route_dict[r.id] = {
"label": r.process.name if r.process else "",
"source": r.material_in.id,
"target": r.material_out.id
"target": r.material_out.id,
"id": r.id
}
# 获取所有物料信息
@ -380,7 +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 = ""
@ -510,6 +549,7 @@ class Route(CommonADModel):
'source': source,
'target': target,
'label': rq.process.name,
'id': rq.id
})
# 将批次号排序
nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time")

View File

@ -24,7 +24,7 @@ class MaterialSimpleSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ['id', 'name', 'number', 'model',
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking"]
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking", "bin_number_main"]
def get_full_name(self, obj):
return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}'
@ -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)
@ -329,4 +337,16 @@ class RouteMatSerializer(CustomModelSerializer):
class Meta:
model = RouteMat
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE
read_only_fields = EXCLUDE_FIELDS_BASE
def validate(self, attrs):
route:Route = attrs["route"]
if route.from_route is not None:
raise ParseError("该工艺步骤引用其他步骤,无法修改")
return attrs
class MaterialExportSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"]

View File

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

View File

@ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer,
MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer,
ShiftSerializer, TeamSerializer, ProcessSerializer,
RouteSerializer, TeamMemberSerializer, RoutePackSerializer,
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer)
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer, MaterialExportSerializer)
from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin
@ -21,6 +21,8 @@ from django.db.models import Q
from apps.wf.models import Ticket
from django.utils import timezone
from rest_framework.permissions import IsAdminUser
from apps.utils.export import export_excel
from operator import itemgetter
# Create your views here.
class MaterialViewSet(CustomModelViewSet):
@ -32,14 +34,13 @@ class MaterialViewSet(CustomModelViewSet):
queryset = Material.objects.all()
serializer_class = MaterialSerializer
filterset_class = MaterialFilter
search_fields = ['name', 'code', 'number', 'specification', 'model']
search_fields = ['name', 'code', 'number', 'specification', 'model', 'bin_number_main']
select_related_fields = ['process']
ordering = ['name', 'model', 'specification',
'type', 'process', 'process__sort', 'sort', 'id', 'number']
ordering_fields = ['name', 'model', 'specification',
'type', 'process', 'process__sort', 'sort', 'id', 'number']
'type', 'process', 'process__sort', 'sort', 'id', 'number', 'create_time']
@transaction.atomic
def perform_destroy(self, instance):
from apps.inm.models import MaterialBatch
if MaterialBatch.objects.filter(material=instance).exists():
@ -88,6 +89,23 @@ class MaterialViewSet(CustomModelViewSet):
def cates(self, request, *args, **kwargs):
res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct()
return Response(set(res))
@action(methods=['get'], detail=False, perms_map={'get': '*'})
def export_excel(self, request, pk=None):
"""导出excel
导出excel
"""
field_data = ['大类', '物料编号', '名称', '规格', '型号', '计量单位', '仓库位号', "安全库存", "单价"]
queryset = self.filter_queryset(self.get_queryset())
if queryset.count() > 1000:
raise ParseError('数据量超过1000,请筛选后导出')
odata = MaterialExportSerializer(queryset, many=True).data
# 处理数据
field_keys = ['cate', 'number', 'name', 'specification', 'model', 'unit',
'bin_number_main', 'count_safe', 'unit_price']
getter = itemgetter(*field_keys)
data = [list(getter(item)) for item in odata]
return Response({'path': export_excel(field_data, data, '物料清单')})
class ShiftViewSet(ListModelMixin, CustomGenericViewSet):
"""
@ -293,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):
"""变更工艺路线状态
@ -349,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):

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

335
apps/ofm/models.py Normal file
View File

@ -0,0 +1,335 @@
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(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):
"""TN: 会议室时段"""
mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m")
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 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)

193
apps/ofm/serializers.py Normal file
View File

@ -0,0 +1,193 @@
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):
class Meta:
model = Mroom
fields = '__all__'
class MroomBookingSerializer(CustomModelSerializer):
mroom = serializers.PrimaryKeyRelatedField(queryset=Mroom.objects.all(), write_only=True, label="会议室")
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}}
def create(self, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = super().create(validated_data)
MroomSlot.objects.filter(booking=booking).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
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
def update(self, instance, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = super().update(instance, validated_data)
MroomSlot.objects.filter(booking=instance).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
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()

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

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

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

@ -0,0 +1,32 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
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/'
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)),
]

259
apps/ofm/views.py Normal file
View File

@ -0,0 +1,259 @@
from django.shortcuts import render
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
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, SealFilter, BorrowRecordFilter
class MroomViewSet(CustomModelViewSet):
"""list: 会议室
会议室
"""
queryset = Mroom.objects.all()
serializer_class = MroomSerializer
class MroomBookingViewSet(CustomModelViewSet):
"""list: 会议室预订
会议室预订
"""
queryset = MroomBooking.objects.all()
serializer_class = MroomBookingSerializer
select_related_fields = ["create_by", "ticket", "belong_dept"]
filterset_class = MroomBookingFilterset
def add_info_for_list(self, data):
booking_ids = [d["id"] for d in data]
slots = MroomSlot.objects.filter(booking__in=booking_ids).order_by("booking", "mroom", "mdate", "slot")
booking_info = {}
for slot in slots:
booking_id = slot.booking.id
if booking_id not in booking_info:
booking_info[booking_id] = {
"mdate": slot.mdate.strftime("%Y-%m-%d"), # 格式化日期
"mroom": slot.mroom.id,
"mroom_name": slot.mroom.name, # 会议室名称
"time_ranges": [], # 存储时间段(如 ["8:00-9:00", "10:00-11:30"]
"current_slots": [], # 临时存储连续的slot用于合并
}
# 检查是否连续当前slot是否紧接上一个slot
current_slots = booking_info[booking_id]["current_slots"]
if not current_slots or slot.slot == current_slots[-1] + 1:
current_slots.append(slot.slot)
else:
# 如果不连续先把当前连续的slot转换成时间段
if current_slots:
start_time = self._slot_to_time(current_slots[0])
end_time = self._slot_to_time(current_slots[-1] + 1)
booking_info[booking_id]["time_ranges"].append(f"{start_time}-{end_time}")
current_slots.clear()
current_slots.append(slot.slot)
# 处理最后剩余的连续slot
for info in booking_info.values():
if info["current_slots"]:
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["slots"] = info.pop("current_slots") # 清理临时数据
for item in data:
item.update(booking_info.get(item["id"], {}))
return data
@staticmethod
def _slot_to_time(slot):
"""将slot (0-47) 转换为 HH:MM 格式的时间字符串"""
hours = slot // 2
minutes = (slot % 2) * 30
return f"{hours:02d}:{minutes:02d}"
def perform_update(self, serializer):
ins:MroomBooking = self.get_object()
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 and instance.create_by != self.request.user:
raise ParseError("只允许创建者删除")
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:
会议室预订时段
"""
queryset = MroomSlot.objects.all()
serializer_class = MroomSlotSerializer
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

@ -2,20 +2,24 @@ from rest_framework.exceptions import ValidationError
from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder
from django.db.models import F, Sum
from apps.inm.models import MIO, MIOItem
from rest_framework.exceptions import ParseError
class PumService:
@staticmethod
def cal_pu_order_total_price(puorder: PuOrder):
total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0
puorder.total_price = total_price
puorder.save()
@staticmethod
def cal_pu_plan_total_price(puplan: PuPlan):
total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0
puplan.total_price = total_price
puplan.save()
@staticmethod
def change_puplan_state_when_puorder_sumbit(puorder: PuOrder):
puplanIds = PuPlanItem.objects.filter(
pu_order=puorder).values_list('pu_plan', flat=True)
@ -29,24 +33,45 @@ class PumService:
puplan.state = state
puplan.save()
def mio_purin(mio: MIO, is_reverse: bool = False):
@staticmethod
def mio_pur(mio: MIO, is_reverse: bool = False, mioitem: MIOItem = None):
"""
采购入库成功后的操作
"""
pu_order = mio.pu_order
if pu_order is None:
return
for i in MIOItem.objects.filter(mio=mio):
pu_orderitem = PuOrderItem.objects.get(
material=i.material, pu_order=pu_order)
if mioitem is None:
qs = MIOItem.objects.filter(mio=mio)
else:
qs = MIOItem.objects.filter(id=mioitem.id)
if mio.type == MIO.MIO_TYPE_PUR_IN:
if is_reverse:
xtype = "out"
else:
xtype = "in"
elif mio.type == MIO.MIO_TYPE_PUR_OUT:
if is_reverse:
xtype = "in"
else:
xtype = "out"
for i in qs:
try:
pu_orderitem = PuOrderItem.objects.get(
material=i.material, pu_order=pu_order)
except PuOrderItem.DoesNotExist:
raise ParseError(f'{str(i.material)}-采购订单中不存在该物料')
if xtype == "out":
delivered_count = pu_orderitem.delivered_count - i.count
else:
delivered_count = pu_orderitem.delivered_count + i.count
if delivered_count > pu_orderitem.count:
raise ValidationError(f'{i.material.name}-超出采购订单所需数量')
raise ValidationError(f'{str(i.material)}-超出采购订单所需数量')
elif delivered_count < 0:
raise ValidationError(f'{i.material.name}-数量小于0')
raise ValidationError(f'{str(i.material)}-数量小于0')
pu_orderitem.delivered_count = delivered_count
pu_orderitem.save()
pu_order_state = PuOrder.PUORDER_SHIP
@ -69,3 +94,4 @@ class PumService:
if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE:
puplan.state = PuPlan.PUPLAN_DONE
puplan.save()

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

@ -25,6 +25,8 @@ class QctFilter(filters.FilterSet):
"testitems": ["exact"],
"defects": ["exact"],
"qctmat__material": ["exact"],
"qctmat__use_for_in": ["exact"],
"qctmat__use_for_out": ["exact"],
"qctmat__tracing": ["exact"],
}
@ -38,18 +40,24 @@ class TestItemFilter(filters.FilterSet):
class FtestWorkFilter(filters.FilterSet):
cbatch = filters.CharFilter(label='批次号', method='filter_cbatch')
class Meta:
model = FtestWork
fields = {
"material__process__name": ["exact", "contains"],
"material": ["exact"],
"wm": ["exact"],
"mb": ["exact"],
"wm": ["exact", "isnull"],
"mb": ["exact", "isnull"],
"batch": ["exact"],
"type": ["exact"],
"type2": ["exact"],
"shift": ["exact"]
}
def filter_cbatch(self, queryset, name, value):
qs1 = queryset.filter(wm__batch=value)
qs2 = queryset.filter(mb__batch=value)
return qs1.union(qs2)
class FtestFilter(filters.FilterSet):
wpr = filters.CharFilter(label="wprId", method="filter_wpr")

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2025-07-18 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qm', '0051_alter_ftestwork_batch'),
]
operations = [
migrations.AddField(
model_name='qctmat',
name='use_for_in',
field=models.BooleanField(default=True, verbose_name='可用于消耗'),
),
migrations.AddField(
model_name='qctmat',
name='use_for_out',
field=models.BooleanField(default=True, verbose_name='可用于产出'),
),
]

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

@ -172,9 +172,14 @@ class Qct(CommonAModel):
return QctMat.objects.filter(qct=self)
@classmethod
def get(cls, material:Material, tag:str):
def get(cls, material:Material, tag:str, type:str=None):
try:
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag)
if type == "in":
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_in=True)
elif type == "out":
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_out=True)
else:
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag)
except Qct.DoesNotExist:
try:
qct = Qct.objects.get(name="默认检验表")
@ -195,8 +200,12 @@ class Qct(CommonAModel):
return None
@classmethod
def get_qs(cls, materialId:str, tag:str):
def get_qs(cls, materialId:str, tag:str, type:str):
qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag)
if type == "in":
qct_qs = qct_qs.filter(qctmat__use_for_in=True)
elif type == "out":
qct_qs = qct_qs.filter(qctmat__use_for_out=True)
if not qct_qs.exists():
qct_qs = Qct.objects.filter(name="默认检验表")
return qct_qs
@ -224,6 +233,8 @@ class QctMat(BaseModel):
material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE)
tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES,
max_length=20, help_text=str(QC_TRACE_CHOICES))
use_for_in = models.BooleanField("可用于消耗", default=True)
use_for_out = models.BooleanField("可用于产出", default=True)
max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True)
@ -326,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)
@ -342,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):
@ -435,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

@ -37,6 +37,7 @@ class DefectSerializer(CustomModelSerializer):
class QctGetSerializer(serializers.Serializer):
material = serializers.CharField(label="物料ID")
tag = serializers.CharField(label="标签")
type = serializers.CharField(label="类型(in/out)", required=False)
class TestItemSerializer(CustomModelSerializer):
process_name = serializers.CharField(source="process.name", read_only=True)
@ -53,6 +54,7 @@ class QctSerializer(CustomModelSerializer):
class QctTestItemSerializer(CustomModelSerializer):
testitem_name = serializers.CharField(source='testitem.name', read_only=True)
testitem_type = serializers.CharField(source='testitem.type', read_only=True)
testitem_description = serializers.CharField(source='testitem.description', read_only=True)
testitem_field_type = serializers.CharField(source='testitem.field_type', read_only=True)
testitem_choices = serializers.CharField(source='testitem.choices', read_only=True)
@ -61,11 +63,11 @@ class QctTestItemSerializer(CustomModelSerializer):
model = QctTestItem
fields = '__all__'
def validate(self, attrs):
testitem:TestItem = attrs.get("testitem")
if testitem.type != TestItem.T_TEST:
raise ParseError("只可选择检测项")
return attrs
# def validate(self, attrs):
# testitem:TestItem = attrs.get("testitem")
# if testitem.type != TestItem.T_TEST:
# raise ParseError("只可选择检测项")
# return attrs
class QctDefectSerializer(CustomModelSerializer):
defect_name = serializers.CharField(source='defect.name', read_only=True)
@ -213,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):
@ -290,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
@ -362,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:
raise ParseError("新的检测项!")
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:
raise ParseError("新的缺陷项!")
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,8 +24,8 @@ 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 ins.qct is None or not fwd_qs.exists():
if wm and ins.need_update_wm:
if ins.qct is None and not fwd_qs.exists():
if wm.state == WMaterial.WM_TEST:
# 更新对应的车间库存
wm.count = wm.count - ins.count
@ -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,9 +125,37 @@ 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()
# 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch])
def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
ins = FtestWork.objects.get(id=new_ticket_data['t_id'])
@ -147,5 +176,4 @@ def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
def ftestwork_audit_end(ticket: Ticket):
ins = FtestWork.objects.get(id=ticket.ticket_data['t_id'])
ftestwork_submit(ins, ticket.create_by)
ana_batch_thread(xbatchs=[ins.batch])
ftestwork_submit(ins, ticket.create_by)

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()
@ -63,8 +62,10 @@ class QctViewSet(CustomModelViewSet):
sr = QctGetSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
qct = Qct.get(vdata["material"], vdata["tag"])
return Response(QctDetailSerializer(instance=qct).data)
qct = Qct.get(vdata["material"], vdata["tag"], vdata.get("type", None))
if qct:
return Response(QctDetailSerializer(instance=qct).data)
return Response()
class QctTestItemViewSet(CustomModelViewSet):
@ -91,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:
@ -149,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()
@ -236,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()
@ -282,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()
@ -320,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 # 不可少

51
apps/utils/lock.py Normal file
View File

@ -0,0 +1,51 @@
from contextlib import contextmanager
from rest_framework.exceptions import ParseError
from functools import wraps
from django.db import transaction
@contextmanager
def lock_model_record(model_class, pk):
"""
Locks a model instance and returns it.
"""
try:
instance = model_class.objects.select_for_update().get(pk=pk)
yield instance
except model_class.DoesNotExist:
raise ParseError("该记录不存在或已被删除")
def lock_model_record_d_func(model_class, pk_attr='id'):
"""
通用模型锁装饰器内置事务用于装饰函数
"""
def decorator(func):
@wraps(func)
@transaction.atomic
def wrapper(old_instance, *args, **kwargs):
try:
# 获取新鲜记录
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
# 调用原函数,但传入新鲜记录
return func(fresh_record, *args, **kwargs)
except model_class.DoesNotExist:
raise ParseError('记录不存在或已被删除')
return wrapper
return decorator
def lock_model_record_d_method(model_class, pk_attr='id'):
"""
通用模型锁装饰器内置事务, 用于装饰类方法
"""
def decorator(func):
@wraps(func)
@transaction.atomic
def wrapper(self, old_instance, *args, **kwargs):
try:
# 获取新鲜记录
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
# 调用原函数,但传入新鲜记录
return func(self, fresh_record, *args, **kwargs)
except model_class.DoesNotExist:
raise ParseError('记录不存在或已被删除')
return wrapper
return decorator

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
@ -18,6 +17,8 @@ from drf_yasg import openapi
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')
@ -80,7 +81,8 @@ class BulkCreateModelMixin(CreateModelMixin):
def after_bulk_create(self, objs):
pass
@transaction.atomic
def create(self, request, *args, **kwargs):
"""创建(支持批量)
@ -90,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)
@ -104,6 +105,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
def after_bulk_update(self, objs):
pass
@transaction.atomic
def partial_update(self, request, *args, **kwargs):
"""部分更新(支持批量)
@ -112,6 +114,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
@transaction.atomic
def update(self, request, *args, **kwargs):
"""更新(支持批量)
@ -123,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)
@ -147,6 +149,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
class BulkDestroyModelMixin(DestroyModelMixin):
@swagger_auto_schema(request_body=PkSerializer)
@transaction.atomic
def destroy(self, request, *args, **kwargs):
"""删除(支持批量)
@ -192,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):
@ -240,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:
@ -261,13 +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:
new_qs = new_qs.order_by(*ordering)
for item in ordering:
if item.startswith('-'):
# JSONField 排序只能用字符串,不要 F
order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item)
else:
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)
@ -320,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
@ -348,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),
@ -442,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

@ -75,8 +75,8 @@ class CustomModelSerializer(DynamicFieldsMixin, TreeSerializerMixin, serializers
class QuerySerializer(serializers.Serializer):
field = serializers.CharField(label='字段名')
compare = serializers.ChoiceField(
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains"])
value = serializers.CharField(label='')
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains", "isnull"])
value = serializers.JSONField(label='', allow_null=True)
class ComplexSerializer(serializers.Serializer):
@ -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:
@ -46,13 +58,17 @@ def query_one_dict(sql, params=None):
:return:
"""
with connection.cursor() as cursor:
if params:
cursor.execute(sql, params=params)
else:
cursor.execute(sql)
cursor.execute(sql, params or ()) # 更简洁的参数处理
columns = [desc[0] for desc in cursor.description]
row = cursor.fetchone()
return dict(zip(columns, row))
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
import psycopg2

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 = '工作流'

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