Compare commits
849 Commits
Author | SHA1 | Date |
---|---|---|
|
2ae7dc2635 | |
|
7cd95bd1b5 | |
|
97026d86c6 | |
|
54999cce73 | |
|
303252e7a5 | |
|
388e225108 | |
|
25ee92602b | |
|
241df0beca | |
|
5ea7980a1b | |
|
a3416cfc0d | |
|
2ca47b8949 | |
|
10792d090c | |
|
bfc8454ac7 | |
|
02b14ec2c6 | |
|
acb4c802e4 | |
|
260c9893eb | |
|
666a9c169c | |
|
b869521221 | |
|
99d8144bdf | |
|
8a87ba356e | |
|
830bf18132 | |
|
b52e90a11f | |
|
ee67e6896a | |
|
54f8b82c98 | |
|
6125139fbf | |
|
2169bbea68 | |
|
6dfab46b4d | |
|
477976f86c | |
|
c37ff77eda | |
|
1aa7d51769 | |
|
e06cc8c38e | |
|
b93024ca44 | |
|
7d87c79dd1 | |
|
9f030ece6d | |
|
67f9cbb700 | |
|
8fe2b8ca48 | |
|
7732ddc88e | |
|
6713693c6c | |
|
20604ef7cb | |
|
eb2deb02c2 | |
|
f5f6c136d9 | |
|
8eee09678a | |
|
efd40d1d32 | |
|
3ab9682b07 | |
|
1983f7b121 | |
|
cec6837d00 | |
|
e214c6115a | |
|
6fb415a9f0 | |
|
727d190610 | |
|
a6e0fb4f0d | |
|
9cf900d2ef | |
|
69e8e7b025 | |
|
647603f986 | |
|
d21c1dc55d | |
|
7f206bd0a7 | |
|
bd763be83a | |
|
fe499ffac5 | |
|
0c1e93bf0b | |
|
5d3c4137fe | |
|
dbaf121685 | |
|
fe524e389c | |
|
bf8b886a2d | |
|
7b8ec7f9d6 | |
|
7620122c2d | |
|
de99f85259 | |
|
e7c121de15 | |
|
bdd686f50b | |
|
54140ba742 | |
|
c186b7d296 | |
|
3a03cd76ff | |
|
f9c9a592e3 | |
|
419b52f9be | |
|
fb71f0697a | |
|
34e217e468 | |
|
f34356057d | |
|
b35015b58d | |
|
c434c76605 | |
|
9909596cdb | |
|
6f4a1c4c88 | |
|
2445ae53f1 | |
|
0fc57a454f | |
|
d26e066769 | |
|
f347fa0d23 | |
|
362a5ef725 | |
|
ad98280458 | |
|
3c0fa9f244 | |
|
b520b69f95 | |
|
899a314f5d | |
|
dbb0d6ae75 | |
|
bc4953893b | |
|
a8722724e1 | |
|
1c447a86ef | |
|
820f814766 | |
|
1689683aa3 | |
|
cdb201a0ce | |
|
d8ad57fa7e | |
|
e32d4b816b | |
|
47ca272cee | |
|
4390992a14 | |
|
085978a6c4 | |
|
f6668b5d38 | |
|
f6720ad7ce | |
|
e5a9f77f3d | |
|
c2deb0ee45 | |
|
94f87df707 | |
|
694dca27cc | |
|
0284809933 | |
|
f60250bb21 | |
|
0b4524aa85 | |
|
f7e27c290f | |
|
2c9c74131c | |
|
70cdf0d1c6 | |
|
bac046530e | |
|
7a3988d6bd | |
|
f151f4f2ec | |
|
7a82844842 | |
|
9bf44c4211 | |
|
ed2804c098 | |
|
00d0b1ea00 | |
|
aa80c1b00a | |
|
67b92f0dd4 | |
|
06e86330bd | |
|
34ca36aced | |
|
649022eb57 | |
|
5709bc3a47 | |
|
cdcb02d4d5 | |
|
e7ea16ece6 | |
|
6b08200016 | |
|
8ef9852e27 | |
|
e9246cc47f | |
|
5ece330457 | |
|
643c45882f | |
|
8bcd9b7d03 | |
|
aff39f3e31 | |
|
53117c838b | |
|
57a61daa66 | |
|
e5008c8412 | |
|
5e8e72cee9 | |
|
674f62a05a | |
|
527e6c0fc2 | |
|
90b7e2087b | |
|
412398d461 | |
|
ebd125ca1d | |
|
6d36f3fa7d | |
|
e8b914d556 | |
|
c9a2daaa48 | |
|
c4c61ff737 | |
|
aa72b0780a | |
|
9d692b4d5d | |
|
42a4332b87 | |
|
51fb42d597 | |
|
19c7e7aad1 | |
|
b4cfdd693a | |
|
448bdb9ee6 | |
|
839fc2af82 | |
|
cecffbdb78 | |
|
a0c3443d9e | |
|
b380c3805b | |
|
80fa245b58 | |
|
d828bb76d5 | |
|
e4a2e7c4f5 | |
|
53858cac94 | |
|
a5f32dbeda | |
|
e491d7b4fe | |
|
1ea9ef48a6 | |
|
9db173cdb9 | |
|
2369c1469e | |
|
27416dfeaa | |
|
ec8881fb02 | |
|
f6f842c17f | |
|
172e2397be | |
|
71bc4e76f0 | |
|
ba8b258ee7 | |
|
8b7a87abb6 | |
|
d42bc29d2c | |
|
6b423c80eb | |
|
df4b062209 | |
|
d84b8e94fd | |
|
6d09e5e4f3 | |
|
bc555b7bea | |
|
3906b0f744 | |
|
d19fee8e6f | |
|
81a16fd37e | |
|
ddb4a6930d | |
|
cbc5d558d9 | |
|
0183234497 | |
|
068e391845 | |
|
6b6c4b7e57 | |
|
0a12927518 | |
|
3be13dd920 | |
|
8cc9c46a95 | |
|
2e2ac78bad | |
|
0b1f71e652 | |
|
94a218c09a | |
|
de1e5b4d41 | |
|
0e783a92e0 | |
|
3e90dd6820 | |
|
10d4a64c3a | |
|
f97f51e72c | |
|
6b246f147d | |
|
fdb49e1147 | |
|
ca18fece21 | |
|
6a6d46583b | |
|
41c1653d13 | |
|
c20580e263 | |
|
a6fcdab836 | |
|
026ebccef5 | |
|
f1aa922946 | |
|
a7fd4b9448 | |
|
65d47008a4 | |
|
65726e967f | |
|
2524feca72 | |
|
01b30e21d3 | |
|
4cffb8a563 | |
|
0c08543e9c | |
|
e6a1363b43 | |
|
3d188a72f3 | |
|
6104a2e6be | |
|
abd1ac9d56 | |
|
cc45163775 | |
|
f82db7bb6f | |
|
c4539260a8 | |
|
872e59ffe1 | |
|
7d2f11f194 | |
|
bf3803a0e8 | |
|
a6b320bad6 | |
|
ba0e86d834 | |
|
28671451b0 | |
|
45af751350 | |
|
c997cba6a5 | |
|
9af23216b6 | |
|
81561b5238 | |
|
f7cc3b438f | |
|
5c163cdcbb | |
|
67f7afd3fc | |
|
1929fd30a6 | |
|
c38457e947 | |
|
8f6e401fdd | |
|
6582a4dae1 | |
|
30f8d484d1 | |
|
31d0dc4829 | |
|
9d1c5415e1 | |
|
a249258ba6 | |
|
4fa0b33254 | |
|
22a310906a | |
|
5e9c15c4a5 | |
|
786e885071 | |
|
41485d7143 | |
|
73b225fa19 | |
|
c47b66af6c | |
|
1d0861e0e7 | |
|
fa243faf34 | |
|
ebe0e4cc6a | |
|
894142712f | |
|
cd4b686f11 | |
|
9ce5d29fa6 | |
|
3e8bce688a | |
|
fa7d3095de | |
|
c7514836cb | |
|
316931fb40 | |
|
8a039094cf | |
|
f0b24da22f | |
|
5496058226 | |
|
3810860f13 | |
|
0a6959a36c | |
|
4823acb004 | |
|
8b6d6fd668 | |
|
c58c79a18a | |
|
41a3418110 | |
|
05955abd9c | |
|
0cf04c50b3 | |
|
01e7d73bee | |
|
58024cc33d | |
|
4921383de1 | |
|
bbfa512bb1 | |
|
d019e708cd | |
|
d9326bc264 | |
|
a8ed1c3083 | |
|
2bb1cf2844 | |
|
0dab1714f3 | |
|
fbce5ec64f | |
|
69e606bccc | |
|
efd25b040b | |
|
ee9fce675d | |
|
7f619a36b0 | |
|
84c701bdeb | |
|
ca3e811b1b | |
|
1d778dbdf0 | |
|
c69c33e85a | |
|
8ecd7d25b0 | |
|
e03faeffc4 | |
|
c7b1c8a8b8 | |
|
ddefbd49b2 | |
|
10745394e3 | |
|
82f6f4ee5e | |
|
46c9c5abab | |
|
09e390b37b | |
|
afb27246d5 | |
|
98a9806c4b | |
|
d3f83605aa | |
|
783f288926 | |
|
40eb07eb12 | |
|
835c5f78dd | |
|
e30241dc22 | |
|
b9376cc2d6 | |
|
05fcc4a00a | |
|
cf25d0d280 | |
|
c7f6abf6a6 | |
|
a42d48775c | |
|
aa72953d17 | |
|
925666d3ef | |
|
b7823d56a8 | |
|
0a6de249ee | |
|
d130f75076 | |
|
dbde5a1327 | |
|
9b4a44be51 | |
|
05ea28f334 | |
|
c14229f6f5 | |
|
94b0719f38 | |
|
24a389d566 | |
|
8a2244a0cc | |
|
1c24e9948b | |
|
763c64d5d0 | |
|
3251c87eb3 | |
|
c790b3bd4a | |
|
884b10d6bc | |
|
7d2fee8409 | |
|
6a93ca31af | |
|
7f89e8a72d | |
|
011577f9a5 | |
|
9b5e1bd9a7 | |
|
28900d3218 | |
|
393e528434 | |
|
2f0ce5c171 | |
|
04d4c560ef | |
|
5514e8b561 | |
|
264083ebd5 | |
|
808b8ac229 | |
|
70069a5a07 | |
|
33709bbb60 | |
|
bbef553bd5 | |
|
1fccdd042c | |
|
de953085cd | |
|
54345d2bce | |
|
ad9ef9efa5 | |
|
06de49f335 | |
|
eebc5238ae | |
|
5755c508d7 | |
|
84198f4d91 | |
|
b5889374eb | |
|
c98ce36c16 | |
|
5c21d6042b | |
|
7f31ec8add | |
|
739c4b0d65 | |
|
b45eadc53c | |
|
5d335fdd6a | |
|
382a5a827c | |
|
b3b77db0c1 | |
|
fa38cfdbdd | |
|
45c224c10e | |
|
b1f6798c65 | |
|
0c7c1e4f17 | |
|
2fbe1620fb | |
|
66a1d6df7f | |
|
3cacea1b06 | |
|
a91b75ae7a | |
|
c35cdb6d0a | |
|
d732652c9f | |
|
3985401361 | |
|
ec83674c6a | |
|
f955c8bd8c | |
|
357b1fdfa0 | |
|
9c391add48 | |
|
4e1c33317c | |
|
80b265b243 | |
|
1bc9cd2c60 | |
|
c0d504ecfe | |
|
7fd47c2a97 | |
|
e2a8da9eb4 | |
|
47cfa7ce17 | |
|
17996a0b86 | |
|
69c7da883f | |
|
811e971b4b | |
|
e8eeb14766 | |
|
bdf69a6635 | |
|
dbfe308154 | |
|
18abcc5c9f | |
|
5eac4240fb | |
|
e6850c92cb | |
|
c3337cfa2a | |
|
fc572d30e3 | |
|
528604d24f | |
|
5285bf0252 | |
|
8ba233f2fd | |
|
9e62bf8d41 | |
|
3fe723b397 | |
|
b8c799b938 | |
|
5a98d1bea7 | |
|
4b52c28b5e | |
|
e2c7847c74 | |
|
024cae6fa3 | |
|
523b4b2a42 | |
|
d76d77fa49 | |
|
3858b3b9f9 | |
|
05ebb25dc7 | |
|
45647d2149 | |
|
28acc48768 | |
|
30bb96cf6c | |
|
e57e087006 | |
|
33f79b5cc5 | |
|
14f3b215c4 | |
|
e9059c93bd | |
|
662bef04ab | |
|
403477afd0 | |
|
1e128041cd | |
|
433218feed | |
|
e3067e115a | |
|
79f1322c27 | |
|
17284dcf4f | |
|
89aca50f44 | |
|
0284117297 | |
|
dd5ef95fa4 | |
|
1a621aca34 | |
|
4b2bcc6d29 | |
|
1d840e7c9d | |
|
e2f3c95748 | |
|
864fb783dc | |
|
8a4ddd0307 | |
|
620e547da5 | |
|
a3b0455fb4 | |
|
fdf717aecb | |
|
a5e8fe3dde | |
|
5ae9030152 | |
|
e86f4e79d4 | |
|
76cdd08e9a | |
|
a0ed107491 | |
|
e6d8509971 | |
|
c054095f94 | |
|
10b63cdad5 | |
|
30379bdab5 | |
|
1bb449c0db | |
|
e25f392175 | |
|
c4cde1c5ea | |
|
a6d53471fc | |
|
d03f106055 | |
|
b500ad6615 | |
|
f3263caed0 | |
|
ab22415e0f | |
|
31f4e2869d | |
|
43e9642917 | |
|
c338509854 | |
|
5dddbfef81 | |
|
037008d7c9 | |
|
9ff72b2da1 | |
|
53972fd22a | |
|
cb09cee753 | |
|
b0df18f01a | |
|
c5e4cc48a4 | |
|
8e33e084b4 | |
|
ce54f326ee | |
|
1242b3d43a | |
|
108fe0ae9f | |
|
f0cacf6777 | |
|
50bf3e78d4 | |
|
307935cb16 | |
|
3683ddbdeb | |
|
fec244765a | |
|
6a5ec34ef8 | |
|
7942a5168b | |
|
db2e8934ba | |
|
4f08374ca6 | |
|
dce573f892 | |
|
3b04bdf067 | |
|
741b64142b | |
|
25c1cce41a | |
|
8805237a44 | |
|
a819566c23 | |
|
6af708d4f0 | |
|
39c86a14d5 | |
|
8d6f2bfd20 | |
|
2b039dd4f7 | |
|
1366f432ee | |
|
31421ca0aa | |
|
95b10371b9 | |
|
a820d6803e | |
|
e774992d3a | |
|
4955ad1f11 | |
|
ab0c962053 | |
|
4bd6eab9f4 | |
|
b9a7138e72 | |
|
cf7673cd98 | |
|
680608e16f | |
|
6d87a69d8d | |
|
c02bb6d3db | |
|
dbad152218 | |
|
bb56becb5d | |
|
b3000b013f | |
|
0719ef8f42 | |
|
fea3c320a9 | |
|
bfc37f69af | |
|
8efd28633c | |
|
5e0c515929 | |
|
438d06af2f | |
|
84690e7b54 | |
|
2e3e8f997c | |
|
7d29b9c4eb | |
|
016ef53517 | |
|
77c4c54eb1 | |
|
11f06c36f7 | |
|
b9cad6bbd8 | |
|
27b225face | |
|
b102b78b02 | |
|
be5dc161de | |
|
53c9afbca9 | |
|
519672ef3f | |
|
6e93d7cd68 | |
|
c2770eed57 | |
|
4cc28b3583 | |
|
ff2b098210 | |
|
fa221218f9 | |
|
bb9e2f337a | |
|
7d0491ac24 | |
|
dff602abd1 | |
|
60c484ee7f | |
|
facee9342c | |
|
370c905781 | |
|
bd9d5a5dc2 | |
|
f06ad961f7 | |
|
9d1af24994 | |
|
014b48e135 | |
|
265a15c9fc | |
|
9ede95a5d6 | |
|
a34514160d | |
|
e1eaa78b11 | |
|
1f9800db16 | |
|
9e9501a065 | |
|
ec42e70118 | |
|
7429b7d5fb | |
|
8994b39917 | |
|
f689b7609c | |
|
45e02eae5f | |
|
5d33301389 | |
|
ad5d8db283 | |
|
4cd4ba3c9d | |
|
725a471fd8 | |
|
49efafcaff | |
|
82958e047b | |
|
70d80dafd2 | |
|
37dc922c34 | |
|
f4fd214110 | |
|
42ba894875 | |
|
a4f2b6eb55 | |
|
e47464805e | |
|
d385fefc4f | |
|
ed6b6c2776 | |
|
f41068e923 | |
|
6539bc2e61 | |
|
3a69075b9f | |
|
eefd3691e6 | |
|
74e176ce97 | |
|
9c1ba20d36 | |
|
62f261fd75 | |
|
359dd8f898 | |
|
1445a14a9a | |
|
0735b5d2b7 | |
|
6b7088997f | |
|
8a7c3830c9 | |
|
68d065a2e3 | |
|
fdb454d6e5 | |
|
36790a72b4 | |
|
a2e45d6954 | |
|
c254d99c3c | |
|
446f705eb3 | |
|
dbea38f82b | |
|
a672a76eee | |
|
8fb204a93d | |
|
5011ef8076 | |
|
126ef8a1b8 | |
|
3500cd25c8 | |
|
4f9157e19a | |
|
d0027679e6 | |
|
5fe2b4db48 | |
|
b1487957b5 | |
|
1a1f64f9fe | |
|
0f0b055101 | |
|
2e0727a2c0 | |
|
b9ca9b802a | |
|
05bf1def23 | |
|
88a5d55425 | |
|
9f3b3a52a7 | |
|
518379ea35 | |
|
1d7c2ebb35 | |
|
d1ccc9043f | |
|
1f96162de9 | |
|
767cfb18e2 | |
|
5a08b5d22c | |
|
3075d8ab8e | |
|
156f6b4844 | |
|
0057e4c181 | |
|
cbeecea2dc | |
|
ca9d46c8d5 | |
|
bd7bbbef0e | |
|
9c69711f38 | |
|
8a61ca9eb1 | |
|
eb8087fdf9 | |
|
c50e4bf3a7 | |
|
01539a32da | |
|
8834d66066 | |
|
356310db3f | |
|
f32adf3702 | |
|
57dfab3bb9 | |
|
d9f66d2970 | |
|
6fcda8c0a7 | |
|
a4a9dd257f | |
|
f6a99f542f | |
|
be6d51af7d | |
|
98bf81f5e0 | |
|
56afccf0a4 | |
|
88c029f66d | |
|
f3ab4476a4 | |
|
c744c07326 | |
|
09072f7998 | |
|
ad1bbaa32e | |
|
1c8489a0b4 | |
|
06e04858df | |
|
0d3d4160ef | |
|
954f5d262f | |
|
8a501c2681 | |
|
c29d1e99d8 | |
|
20384ca33c | |
|
5e2fd57b89 | |
|
8a75fcbc42 | |
|
cdb4e65159 | |
|
dae8d50d2c | |
|
a9f55eb61c | |
|
7aafa5d9f7 | |
|
cc12d80c92 | |
|
2128b4e847 | |
|
adf21e716d | |
|
c05432ce56 | |
|
a4ba33550e | |
|
a5a862f7fb | |
|
8e35443433 | |
|
725dd4d554 | |
|
da69e654bc | |
|
a2cba2128f | |
|
dd6dcad74a | |
|
f35d2d7d9e | |
|
ac610a53ce | |
|
7c752ec39b | |
|
325fad0a6e | |
|
e59c83aa3f | |
|
ced3e7a16c | |
|
6b8fa4e58c | |
|
7131697802 | |
|
7338d09f5d | |
|
0ff9aa1e5a | |
|
c5a7a19f74 | |
|
25a111bc6b | |
|
849e63da3e | |
|
76a48d46ee | |
|
76adab1571 | |
|
9d634940d3 | |
|
d7dfb8c48f | |
|
a0346f00bf | |
|
5d94f9615c | |
|
cd0045c561 | |
|
8a83f8b7d8 | |
|
2ca9b3ce02 | |
|
8697288d69 | |
|
89c8cac7c1 | |
|
c0277ad9a6 | |
|
139aa6de27 | |
|
cdb9083def | |
|
c8d05bed68 | |
|
be57fec29f | |
|
d6fe5af39c | |
|
6033216869 | |
|
000ea6fc27 | |
|
295edf65d2 | |
|
439bfeb7d6 | |
|
8f6a6eb973 | |
|
84380931fd | |
|
ca7d56a462 | |
|
9df6294617 | |
|
1cb4128be2 | |
|
0f728d8b29 | |
|
960787d7d8 | |
|
7e6ca8cf70 | |
|
8183073026 | |
|
b66157edb8 | |
|
aa0add371a | |
|
e2d63ed056 | |
|
dd4d958f86 | |
|
2987853afb | |
|
74581dc906 | |
|
cbc667d50c | |
|
ba9d7d041d | |
|
75503fc7e8 | |
|
433d85456e | |
|
32185abffd | |
|
c2539185e2 | |
|
3811c055f9 | |
|
6e12ca3137 | |
|
f22ba2752f | |
|
ea00a38406 | |
|
3d0536745a | |
|
e329c8cbc8 | |
|
8ac3ad3477 | |
|
42c249330e | |
|
f12d8ce03c | |
|
311e9e9e04 | |
|
5cab0d018b | |
|
ced7515d80 | |
|
2ea3dc79cb | |
|
82587b1736 | |
|
786d178edc | |
|
181d477425 | |
|
0489a545e8 | |
|
e24d92fe9b | |
|
adcde59bd1 | |
|
101a51050d | |
|
9e4e5a28c6 | |
|
00e352d8ef | |
|
1e379d412c | |
|
2b9e3322ac | |
|
6678e39518 | |
|
b44fec0695 | |
|
6d904341a0 | |
|
369c452dd5 | |
|
fafe39c1bd | |
|
044b489388 | |
|
1aad0bc481 | |
|
5f2385ccd1 | |
|
896be043f5 | |
|
9179803fda | |
|
b5c21b7472 | |
|
4bc126ca97 | |
|
300023ff81 | |
|
e0217e86c4 | |
|
4c7f2b24f8 | |
|
7a3c5be4eb | |
|
d3263236f9 | |
|
50b264929b | |
|
4874f6356e | |
|
d864405c0d | |
|
8c27b1a03c | |
|
afc884eb7f | |
|
c670bd69e1 | |
|
cd294bc53c | |
|
88bf75baaf | |
|
1cbbade280 | |
|
c791bde5be | |
|
22b6b1305f | |
|
882b0f2cbd | |
|
f228884b27 | |
|
fda9ecae94 | |
|
1ff822161a | |
|
4b87f6fe8e | |
|
648aaa465c | |
|
cdc30f5163 | |
|
060c07abd7 | |
|
c0eacd7fa8 | |
|
8068cb1ca3 | |
|
15ccfbe6f9 | |
|
948bbcb805 | |
|
2a0c97dfb2 | |
|
b34e477593 | |
|
ef0414b053 | |
|
1efd1c71da | |
|
b852e9b740 | |
|
61488bb4bd | |
|
166b83c04d | |
|
de5f231154 | |
|
9f0a9fc699 | |
|
885d932fc8 | |
|
b400b13ded | |
|
a8f9676ed0 | |
|
a79717abd8 | |
|
18546d48f5 | |
|
10e0f4aaef | |
|
2923fad2a8 | |
|
5fe24c381b | |
|
20943494ad | |
|
783a56dfea | |
|
cde15991a1 | |
|
6dfb5c489a | |
|
c05c59b004 | |
|
cb28f1d9ef | |
|
146fe6641f | |
|
0b76602c84 | |
|
1936fe183f | |
|
3c0e416735 | |
|
88da422f54 | |
|
853c28976d | |
|
cb1b2dfae6 | |
|
98d76424c7 | |
|
c541e58006 | |
|
6b3a34879e | |
|
4b9e65ba40 | |
|
18e5ca2598 | |
|
73f0e504c9 | |
|
40370b876f | |
|
ce38115255 | |
|
afe53f3687 | |
|
c002017a84 | |
|
a31faa0cda | |
|
2d7183f8dd | |
|
eaa5d75b99 | |
|
bac30a2925 | |
|
4e7757f64d | |
|
0ddc6692ac | |
|
3e074b51e3 | |
|
a2ed0eb1d3 | |
|
5c77eb2742 | |
|
0c9c38890e | |
|
8bf3ad06d6 | |
|
546b020dae | |
|
7f2aaf3f86 | |
|
22e64e5b84 | |
|
0e8cce0669 | |
|
11d7e1a772 | |
|
52b96dc740 | |
|
6f28056ed4 | |
|
b9a3427162 | |
|
d4df340c1c | |
|
b070b312a8 | |
|
e07f9c877c | |
|
c1a4edf9e5 | |
|
468cb7b658 | |
|
5ee10e7cc5 | |
|
f57604492b | |
|
9780cd8021 | |
|
d8c421969c | |
|
43ddafa6d7 | |
|
0842806f83 | |
|
108116342e | |
|
23ad849e36 | |
|
8867e76701 | |
|
0474d0a943 | |
|
1289c19aaa | |
|
a773f4884b | |
|
5e3069b147 | |
|
fa66a9a4d5 | |
|
3ad7acafc9 | |
|
cde3d68d7f | |
|
d136f96bcc | |
|
77f411887a | |
|
48988d3237 |
|
@ -20,6 +20,8 @@ db.sqlite3
|
|||
server/conf*.py
|
||||
server/conf.ini
|
||||
server/conf*.json
|
||||
config/conf*.py
|
||||
config/conf*.json
|
||||
sh/*
|
||||
temp/*
|
||||
nohup.out
|
||||
|
|
|
@ -7,7 +7,7 @@ from apps.utils.models import CommonADModel, CommonBModel
|
|||
|
||||
class Area(CommonBModel):
|
||||
"""
|
||||
地图区域
|
||||
TN: 地图区域
|
||||
"""
|
||||
AREA_1 = 10
|
||||
AREA_2 = 20
|
||||
|
@ -47,7 +47,7 @@ class Area(CommonBModel):
|
|||
|
||||
class Access(CommonADModel):
|
||||
"""
|
||||
准入禁入权限(动态变化)
|
||||
TN: 准入禁入权限(动态变化)
|
||||
"""
|
||||
ACCESS_IN_YES = 10
|
||||
ACCESS_IN_NO = 20
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.urls import path, include
|
|||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/am/'
|
||||
HTML_BASE_URL = 'am/'
|
||||
HTML_BASE_URL = 'dhtml/am/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('area', AreaViewSet, basename='area')
|
||||
|
|
|
@ -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="验证码")
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ from apps.auth1.errors import USERNAME_OR_PASSWORD_WRONG
|
|||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.core.cache import cache
|
||||
from apps.auth1.services import check_phone_code
|
||||
from apps.utils.sms import send_sms
|
||||
|
||||
from apps.utils.tools import rannum
|
||||
from apps.utils.wxmp import wxmpClient
|
||||
from apps.utils.wx import wxClient
|
||||
|
@ -23,7 +23,8 @@ from apps.auth1.serializers import FaceLoginSerializer
|
|||
|
||||
|
||||
from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer,
|
||||
PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer)
|
||||
PwResetSerializer, SecretLoginSerializer,
|
||||
SendCodeSerializer, WxCodeSerializer, UserIdSerializer)
|
||||
from apps.system.models import User
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from apps.auth1.authentication import get_user_by_username_or
|
||||
|
@ -182,6 +183,7 @@ class SendCode(CreateAPIView):
|
|||
|
||||
短信验证码发送
|
||||
"""
|
||||
from apps.utils.sms import send_sms
|
||||
phone = request.data['phone']
|
||||
code = rannum(6)
|
||||
is_ok, _ = send_sms(phone, 505, {'code': code})
|
||||
|
@ -233,6 +235,29 @@ class SecretLogin(CreateAPIView):
|
|||
return Response(ret)
|
||||
raise ParseError('登录失败')
|
||||
|
||||
class UserIdLogin(CreateAPIView):
|
||||
"""直接UserId登录(危险操作)
|
||||
|
||||
直接UserId登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = UserIdSerializer
|
||||
|
||||
def post(self, request):
|
||||
sr = UserIdSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
userid = vdata['user_id']
|
||||
try:
|
||||
user = User.objects.get(id=userid)
|
||||
except Exception as e:
|
||||
raise ParseError(f'用户不存在-{e}')
|
||||
if user:
|
||||
ret = get_tokens_for_user(user)
|
||||
return Response(ret)
|
||||
raise ParseError('登录失败')
|
||||
|
||||
|
||||
class PwResetView(CreateAPIView):
|
||||
"""重置密码
|
||||
|
|
|
@ -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='启用'),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -5,16 +5,19 @@ from apps.bi.models import Dataset
|
|||
import concurrent
|
||||
from apps.utils.sql import execute_raw_sql, format_sqldata
|
||||
|
||||
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE"]
|
||||
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE", "INSERT", "CREATE", "ALTER", "GRANT", "REVOKE", "EXEC", "EXECUTE"]
|
||||
|
||||
|
||||
def check_sql_safe(sql: str):
|
||||
"""检查sql安全性
|
||||
"""
|
||||
sql_upper = sql.upper()
|
||||
# 将SQL按空格和分号分割成单词
|
||||
words = [word for word in sql_upper.replace(';', ' ').split() if word]
|
||||
for kw in forbidden_keywords:
|
||||
if kw in sql_upper:
|
||||
raise ParseError('sql查询有风险')
|
||||
# 检查关键字是否作为独立单词出现
|
||||
if kw in words:
|
||||
raise ParseError(f'sql查询有风险-{kw}')
|
||||
return sql
|
||||
|
||||
def format_json_with_placeholders(json_str, **kwargs):
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.utils import timezone
|
|||
from apps.bi.models import Dataset, DatasetRecord
|
||||
from apps.bi.services import exec_dataset
|
||||
import json
|
||||
|
||||
from apps.utils.tools import MyJSONEncoder
|
||||
|
||||
@shared_task()
|
||||
def exec_dataset_and_store(code: str, query: str = ''):
|
||||
|
@ -16,4 +16,5 @@ def exec_dataset_and_store(code: str, query: str = ''):
|
|||
if query:
|
||||
squery = json.loads(query)
|
||||
dtr.full_sql, dtr.result = exec_dataset(dt, squery)
|
||||
dtr.result = json.loads(json.dumps(dtr.result, cls=MyJSONEncoder))
|
||||
dtr.save()
|
||||
|
|
|
@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
|
|||
from apps.bi.views import DatasetViewSet, DatasetRecordViewSet
|
||||
|
||||
API_BASE_URL = 'api/bi/'
|
||||
HTML_BASE_URL = 'bi/'
|
||||
HTML_BASE_URL = 'dhtml/bi/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('dataset', DatasetViewSet, basename='dataset')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from django.utils import timezone
|
||||
from .models import LabelTemplate
|
||||
from apps.utils.filters import MyJsonListFilter
|
||||
|
||||
class LabelTemplateFilter(filters.FilterSet):
|
||||
process = MyJsonListFilter(label='按工序查询', field_name="process_json")
|
||||
|
||||
class Meta:
|
||||
model = LabelTemplate
|
||||
fields = ['process', "name"]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.12 on 2025-04-30 05:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cm', '0003_alter_lablemat_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LabelTemplate',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.TextField(verbose_name='名称')),
|
||||
('commands', models.JSONField(blank=True, default=list, verbose_name='指令模板')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2025-05-06 02:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cm', '0004_labeltemplate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='labeltemplate',
|
||||
name='process_json',
|
||||
field=models.JSONField(blank=True, default=list, verbose_name='工序'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2025-05-23 01:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cm', '0005_labeltemplate_process_json'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='lablemat',
|
||||
name='batch',
|
||||
field=models.TextField(db_index=True, verbose_name='批次号'),
|
||||
),
|
||||
]
|
|
@ -3,13 +3,40 @@ from apps.utils.models import BaseModel
|
|||
from apps.mtm.models import Material
|
||||
from apps.pum.models import Supplier
|
||||
from apps.wpm.models import WmStateOption
|
||||
from rest_framework.exceptions import NotFound, ParseError
|
||||
# Create your models here.
|
||||
|
||||
class LableMat(BaseModel):
|
||||
"""TN: 标签物料"""
|
||||
state = models.PositiveSmallIntegerField('状态', default=10, choices=WmStateOption.choices)
|
||||
material = models.ForeignKey(Material, on_delete=models.CASCADE)
|
||||
batch = models.CharField('批次号', max_length=100)
|
||||
batch = models.TextField('批次号', db_index=True)
|
||||
supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
notok_sign = models.CharField('不合格标记', max_length=10, null=True, blank=True)
|
||||
defect = models.ForeignKey("qm.defect", verbose_name='缺陷', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='lm_mo')
|
||||
|
||||
|
||||
class LabelTemplate(BaseModel):
|
||||
"""TN: 标签模板"""
|
||||
name = models.TextField("名称")
|
||||
commands = models.JSONField("指令模板", default=list, blank=True)
|
||||
process_json = models.JSONField("工序", default=list, blank=True)
|
||||
|
||||
@classmethod
|
||||
def gen_commands(cls, label_template, label_template_name, tdata):
|
||||
if label_template:
|
||||
lt = LabelTemplate.objects.get(id=label_template)
|
||||
else:
|
||||
lt = LabelTemplate.objects.filter(name=label_template_name).first()
|
||||
if not lt:
|
||||
raise NotFound("标签模板不存在")
|
||||
commands:list = lt.commands
|
||||
try:
|
||||
n_commands = []
|
||||
for item in commands:
|
||||
item = item.format(**tdata)
|
||||
n_commands.append(item)
|
||||
except Exception as e:
|
||||
raise ParseError(f"标签解析错误-{str(e)}")
|
||||
return n_commands
|
|
@ -1,11 +1,20 @@
|
|||
from rest_framework import serializers
|
||||
from .models import LableMat
|
||||
from .models import LableMat, LabelTemplate
|
||||
from apps.qm.models import NotOkOption
|
||||
from apps.wpm.models import WmStateOption
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
|
||||
|
||||
class TidSerializer(serializers.Serializer):
|
||||
tid = serializers.CharField(label='表ID')
|
||||
label_template = serializers.CharField(label='标签模板ID', allow_null=True, required=False)
|
||||
label_template_name = serializers.CharField(label='标签模板名称', allow_null=True, required=False)
|
||||
extra_data = serializers.JSONField(label='额外数据', allow_null=True, required=False)
|
||||
|
||||
class Tid2Serializer(serializers.Serializer):
|
||||
label_template = serializers.CharField(label='标签模板ID', allow_null=True, required=False)
|
||||
label_template_name = serializers.CharField(label='标签模板名称', allow_null=True, required=False)
|
||||
data = serializers.JSONField(label='数据', allow_null=True, required=False)
|
||||
|
||||
|
||||
class LabelMatSerializer(serializers.ModelSerializer):
|
||||
|
@ -24,3 +33,9 @@ class LabelMatSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_state_name(self, obj):
|
||||
return getattr(WmStateOption, str(obj.state), WmStateOption.OK).label if obj.state else None
|
||||
|
||||
|
||||
class LabelTemplateSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = LabelTemplate
|
||||
fields = '__all__'
|
|
@ -1,12 +1,13 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.cm.views import LableMatViewSet
|
||||
from apps.cm.views import LableMatViewSet, LabelTemplateViewSet
|
||||
|
||||
API_BASE_URL = 'api/cm/'
|
||||
HTML_BASE_URL = 'cm/'
|
||||
HTML_BASE_URL = 'dhtml/cm/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('labelmat', LableMatViewSet, basename='labelmat')
|
||||
router.register('labeltemplate', LabelTemplateViewSet, basename='labeltemplate')
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
]
|
|
@ -1,11 +1,12 @@
|
|||
from apps.cm.models import LableMat
|
||||
from apps.cm.models import LableMat, LabelTemplate
|
||||
from rest_framework.decorators import action
|
||||
from apps.cm.serializers import TidSerializer, LabelMatSerializer
|
||||
from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer
|
||||
from apps.inm.models import MaterialBatch, MIOItem
|
||||
from apps.wpm.models import WMaterial
|
||||
from rest_framework.exceptions import ParseError, NotFound
|
||||
from rest_framework.response import Response
|
||||
from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin
|
||||
from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin, CustomModelViewSet
|
||||
from apps.cm.filters import LabelTemplateFilter
|
||||
# Create your views here.
|
||||
|
||||
SPLIT_FIELD = "#"
|
||||
|
@ -24,6 +25,9 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
|
|||
从仓库明细获取物料标签
|
||||
"""
|
||||
tid = request.data.get("tid")
|
||||
label_template = request.data.get("label_template", None)
|
||||
label_template_name = request.data.get("label_template_name", None)
|
||||
extra_data = request.data.get("extra_data", {})
|
||||
try:
|
||||
mb = MaterialBatch.objects.get(id=tid)
|
||||
except MaterialBatch.DoesNotExist:
|
||||
|
@ -31,6 +35,10 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
|
|||
obj, _ = LableMat.objects.get_or_create(state=mb.state, material=mb.material, batch=mb.batch, defect=mb.defect, defaults={"supplier": mb.supplier})
|
||||
rdata = LabelMatSerializer(obj).data
|
||||
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
|
||||
if label_template or label_template_name:
|
||||
tdata = {**rdata, **extra_data}
|
||||
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
|
||||
rdata["commands"] = commands
|
||||
return Response(rdata)
|
||||
|
||||
@action(methods=["post"], detail=False, serializer_class=TidSerializer)
|
||||
|
@ -41,6 +49,9 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
|
|||
从车间库存明细获取物料标签
|
||||
"""
|
||||
tid = request.data.get("tid")
|
||||
label_template = request.data.get("label_template", None)
|
||||
label_template_name = request.data.get("label_template_name", None)
|
||||
extra_data = request.data.get("extra_data", {})
|
||||
try:
|
||||
wm = WMaterial.objects.get(id=tid)
|
||||
except WMaterial.DoesNotExist:
|
||||
|
@ -50,6 +61,10 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
|
|||
material_origin=wm.material_origin)
|
||||
rdata = LabelMatSerializer(obj).data
|
||||
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
|
||||
if label_template or label_template_name:
|
||||
tdata = {**rdata, **extra_data}
|
||||
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
|
||||
rdata["commands"] = commands
|
||||
return Response(rdata)
|
||||
|
||||
@action(methods=["post"], detail=False, serializer_class=TidSerializer)
|
||||
|
@ -60,6 +75,9 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
|
|||
从出入库明细获取物料标签
|
||||
"""
|
||||
tid = request.data.get("tid")
|
||||
label_template = request.data.get("label_template", None)
|
||||
label_template_name = request.data.get("label_template_name", None)
|
||||
extra_data = request.data.get("extra_data", {})
|
||||
try:
|
||||
mioitem: MIOItem = MIOItem.objects.get(id=tid)
|
||||
except MIOItem.DoesNotExist:
|
||||
|
@ -67,4 +85,32 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
|
|||
obj, _ = LableMat.objects.get_or_create(state=10, material=mioitem.material, batch=mioitem.batch, defaults={"supplier": mioitem.mio.supplier})
|
||||
rdata = LabelMatSerializer(obj).data
|
||||
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
|
||||
if label_template or label_template_name:
|
||||
tdata = {**rdata, **extra_data}
|
||||
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
|
||||
rdata["commands"] = commands
|
||||
return Response(rdata)
|
||||
|
||||
|
||||
class LabelTemplateViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list: 标签模板
|
||||
|
||||
标签模板
|
||||
"""
|
||||
queryset = LabelTemplate.objects.all()
|
||||
serializer_class = LabelTemplateSerializer
|
||||
filterset_class = LabelTemplateFilter
|
||||
|
||||
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"})
|
||||
def commands(self, request, *args, **kwargs):
|
||||
"""
|
||||
获取标签指令
|
||||
|
||||
获取标签指令
|
||||
"""
|
||||
label_template = request.data.get("label_template", None)
|
||||
label_template_name = request.data.get("label_template_name", None)
|
||||
data = request.data.get("data", {})
|
||||
return Response({"commands": LabelTemplate.gen_commands(label_template, label_template_name, data)})
|
||||
|
|
@ -3,6 +3,7 @@ from apps.utils.models import CommonADModel
|
|||
|
||||
# Create your models here.
|
||||
class Article(CommonADModel):
|
||||
"""TN: 文章"""
|
||||
title = models.CharField(max_length=200, verbose_name="标题")
|
||||
poster = models.TextField(verbose_name="封面地址", null=True, blank=True)
|
||||
video = models.TextField(verbose_name="视频地址", null=True, blank=True)
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.urls import path, include
|
|||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/cms/'
|
||||
HTML_BASE_URL = 'cms/'
|
||||
HTML_BASE_URL = 'dhtml/cms/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('article', ArticleViewSet, basename='article')
|
||||
|
|
|
@ -5,6 +5,7 @@ from apps.utils.models import BaseModel
|
|||
|
||||
|
||||
class Project(BaseModel):
|
||||
"""TN: 项目"""
|
||||
name = models.CharField('项目名称', max_length=50)
|
||||
code = models.CharField('标识', max_length=20, unique=True)
|
||||
config_json = models.JSONField('配置信息', default=dict)
|
||||
|
|
|
@ -36,3 +36,8 @@ class SpeakerSerializer(serializers.Serializer):
|
|||
|
||||
class AreaManSerializer(serializers.Serializer):
|
||||
area = serializers.CharField()
|
||||
|
||||
|
||||
class ServerTimeSerializer(serializers.Serializer):
|
||||
server_time = serializers.DateTimeField()
|
||||
timezone = serializers.CharField(read_only=True)
|
|
@ -2,6 +2,8 @@ from __future__ import absolute_import, unicode_literals
|
|||
from celery import shared_task
|
||||
import subprocess
|
||||
from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD
|
||||
import logging
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
|
||||
@shared_task
|
||||
|
@ -23,6 +25,7 @@ def backup_database():
|
|||
@shared_task
|
||||
def reload_server_git():
|
||||
command = "bash {}/git_server.sh".format(SH_PATH)
|
||||
myLogger.info(f"reload_server_git: {command}")
|
||||
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
return completed.stderr
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from django.urls import path, include
|
||||
from apps.develop.views import BackupDatabase, BackupMedia, ReloadClientGit, ReloadServerGit, ReloadServerOnly, TestViewSet, CorrectViewSet
|
||||
from apps.develop.views import (BackupDatabase, BackupMedia, ReloadClientGit,
|
||||
ReloadServerGit, ReloadServerOnly, TestViewSet, CorrectViewSet, testScanHtml, ServerTime)
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/develop/'
|
||||
HTML_BASE_URL = 'develop/'
|
||||
HTML_BASE_URL = 'dhtml/develop/'
|
||||
router = DefaultRouter()
|
||||
router.register('test', TestViewSet, basename='api_test')
|
||||
router.register('correct', CorrectViewSet, basename='correct')
|
||||
|
@ -14,5 +15,7 @@ urlpatterns = [
|
|||
path(API_BASE_URL + 'reload_server_only/', ReloadServerOnly.as_view()),
|
||||
path(API_BASE_URL + 'backup_database/', BackupDatabase.as_view()),
|
||||
path(API_BASE_URL + 'backup_media/', BackupMedia.as_view()),
|
||||
path(API_BASE_URL + 'server_time/', ServerTime.as_view()),
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
path(HTML_BASE_URL + "testscan/", testScanHtml)
|
||||
]
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.permissions import IsAdminUser, AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.decorators import action
|
||||
from apps.am.models import Area
|
||||
from apps.am.tasks import cache_areas_info
|
||||
from apps.develop.serializers import AreaManSerializer, CleanDataSerializer, GenerateVoiceSerializer, SendSmsSerializer, SpeakerSerializer, \
|
||||
TestTaskSerializer, TestAlgoSerializer
|
||||
TestTaskSerializer, TestAlgoSerializer, ServerTimeSerializer
|
||||
from apps.develop.tasks import backup_database, backup_media, reload_web_git, reload_server_git, reload_server_only
|
||||
from rest_framework.exceptions import APIException
|
||||
from apps.ecm.service import check_not_in_place, create_remind, handle_xx_event, loc_change, notify_event, rail_in, snap_and_analyse
|
||||
|
@ -19,7 +19,6 @@ from apps.opm.models import Opl
|
|||
from apps.third.dahua import dhClient
|
||||
from apps.third.speaker import spClient
|
||||
from apps.third.models import TDevice
|
||||
from apps.utils.permission import RbacPermission
|
||||
from apps.utils.sms import send_sms
|
||||
from apps.utils.speech import generate_voice
|
||||
from apps.utils.tools import get_info_from_id
|
||||
|
@ -29,14 +28,50 @@ from rest_framework.authentication import BasicAuthentication, SessionAuthentica
|
|||
|
||||
from apps.utils.viewsets import CustomGenericViewSet
|
||||
from apps.utils.wx import wxClient
|
||||
from apps.wf.models import State, Transition, Workflow
|
||||
from apps.wf.models import Transition
|
||||
from django.db import transaction
|
||||
from apps.utils.snowflake import idWorker
|
||||
from django.core.cache import cache
|
||||
import json
|
||||
from apps.utils.decorators import auto_log, idempotent
|
||||
from apps.utils.decorators import auto_log
|
||||
from django.http import HttpResponse
|
||||
from server.settings import SD_PWD, TIME_ZONE
|
||||
import subprocess
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from datetime import datetime
|
||||
# Create your views here.
|
||||
|
||||
class ServerTime(APIView):
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method == 'GET':
|
||||
return [AllowAny()]
|
||||
return [IsAdminUser()]
|
||||
|
||||
@swagger_auto_schema(responses={200: ServerTimeSerializer})
|
||||
def get(self, request):
|
||||
"""
|
||||
获取服务器时间
|
||||
|
||||
获取服务器时间
|
||||
"""
|
||||
return Response({"server_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timezone": TIME_ZONE})
|
||||
|
||||
@swagger_auto_schema(request_body=ServerTimeSerializer)
|
||||
def post(self, request):
|
||||
"""
|
||||
修改服务器时间
|
||||
|
||||
修改服务器时间
|
||||
"""
|
||||
command = f'date -s "{request.data["server_time"]}"'
|
||||
completed = subprocess.run(
|
||||
["sudo", "-S", "sh", "-c", command], # 添加 -S 参数
|
||||
input=SD_PWD + "\n", # 注意要在密码后加换行符
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ParseError(completed.stderr)
|
||||
return Response()
|
||||
|
||||
class ReloadServerGit(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
@ -615,14 +650,14 @@ class TestViewSet(CustomGenericViewSet):
|
|||
'apps.rpm.tasks.close_rpj_by_leave_time']).delete()
|
||||
if 'mes' in datas:
|
||||
from apps.inm.models import MaterialBatch, MIO
|
||||
from apps.mtm.models import Material
|
||||
from apps.mtm.models import Material, RoutePack
|
||||
from apps.wpmw.models import Wpr
|
||||
from apps.wpm.models import WMaterial, Mlog
|
||||
from apps.wpm.models import WMaterial, Mlog, Handover, BatchSt
|
||||
from apps.pum.models import PuOrder
|
||||
from apps.sam.models import Order
|
||||
from apps.pm.models import Utask, Mtask
|
||||
from apps.wpm.models import Handover
|
||||
from apps.qm.models import Ftest, FtestWork
|
||||
from apps.wf.models import Ticket
|
||||
MaterialBatch.objects.all().delete()
|
||||
MIO.objects.all().delete()
|
||||
Wpr.objects.all().delete()
|
||||
|
@ -635,7 +670,9 @@ class TestViewSet(CustomGenericViewSet):
|
|||
Handover.objects.all().delete()
|
||||
Ftest.objects.all().delete()
|
||||
FtestWork.objects.all().delete()
|
||||
Material.objects.all().update(count=0, count_mb=0, count_wm=0)
|
||||
Material.objects.get_queryset(all=True).update(count=0, count_mb=0, count_wm=0)
|
||||
BatchSt.objects.all().delete()
|
||||
Ticket.objects.get_queryset(all=True).delete()
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=False, serializer_class=Serializer, permission_classes=[])
|
||||
|
@ -643,3 +680,35 @@ class TestViewSet(CustomGenericViewSet):
|
|||
from apps.wpm.tasks import cal_exp_duration_sec
|
||||
cal_exp_duration_sec('3397169058570170368')
|
||||
return Response()
|
||||
|
||||
|
||||
html_str = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebView 扫码</title>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="openScanner()">扫码</button>
|
||||
<p>扫描结果: <span id="result"></span></p>
|
||||
|
||||
<script>
|
||||
function openScanner() {
|
||||
if (window.Android) {
|
||||
window.Android.openScanner();
|
||||
} else {
|
||||
alert("当前环境不支持扫码");
|
||||
}
|
||||
}
|
||||
|
||||
function onScanResult(data) {
|
||||
document.getElementById("result").innerText = data;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
def testScanHtml(request):
|
||||
return HttpResponse(html_str)
|
|
@ -5,7 +5,7 @@ from django_celery_beat.models import PeriodicTask
|
|||
# Create your models here.
|
||||
class RiskPoint(CommonAModel):
|
||||
"""
|
||||
风险点表
|
||||
TN: 风险点表
|
||||
"""
|
||||
R_LEVEL = (
|
||||
(10, '低风险'),
|
||||
|
@ -34,7 +34,7 @@ class RiskPoint(CommonAModel):
|
|||
|
||||
class Risk(CommonAModel):
|
||||
"""
|
||||
风险表
|
||||
TN: 风险表
|
||||
"""
|
||||
name = models.TextField('项目/步骤')
|
||||
level = models.PositiveSmallIntegerField('风险等级', default=10, choices=RiskPoint.R_LEVEL)
|
||||
|
@ -56,7 +56,7 @@ class Risk(CommonAModel):
|
|||
|
||||
class CheckTaskSet(CommonADModel):
|
||||
"""
|
||||
检查任务派发设置
|
||||
TN:检查任务派发设置
|
||||
"""
|
||||
riskpoint = models.ForeignKey(RiskPoint, verbose_name='关联风险点', related_name='ctask_riskpoint', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
note = models.TextField('派发备注', null=True, blank=True)
|
||||
|
@ -68,7 +68,7 @@ class CheckTaskSet(CommonADModel):
|
|||
|
||||
class CheckWork(CommonAModel):
|
||||
"""
|
||||
检查工作
|
||||
TN: 检查工作
|
||||
"""
|
||||
CW_TYPE = (
|
||||
(10, '手动'),
|
||||
|
@ -90,7 +90,7 @@ class CheckWork(CommonAModel):
|
|||
|
||||
class Hazard(CommonADModel):
|
||||
"""
|
||||
事故隐患表
|
||||
TN: 事故隐患表
|
||||
"""
|
||||
H_HARM = (
|
||||
(10, '无伤害'),
|
||||
|
@ -142,7 +142,7 @@ class Hazard(CommonADModel):
|
|||
|
||||
class CheckItem(BaseModel):
|
||||
"""
|
||||
检查工作-隐患关联表
|
||||
TN:检查工作-隐患关联表
|
||||
"""
|
||||
CITEM_RESULT = (
|
||||
(10, '未检查'),
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.urls import path, include
|
|||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/dpm/'
|
||||
HTML_BASE_URL = 'dpm/'
|
||||
HTML_BASE_URL = 'dhtml/dpm/'
|
||||
# cache_areas_info()
|
||||
router = DefaultRouter()
|
||||
router.register('riskpoint', RiskPointViewSet, basename='riskpoint')
|
||||
|
|
|
@ -16,7 +16,7 @@ LEVEL_CHOICES = (
|
|||
|
||||
class EventCate(CommonAModel):
|
||||
"""
|
||||
事件种类
|
||||
TN: 事件种类
|
||||
"""
|
||||
EVENT_TRIGGER_CHOICES = (
|
||||
(10, '监控'),
|
||||
|
@ -47,7 +47,7 @@ class EventCate(CommonAModel):
|
|||
|
||||
|
||||
class AlgoChannel(BaseModel):
|
||||
|
||||
"""TN: 算法通道"""
|
||||
algo = models.ForeignKey(EventCate, verbose_name='关联算法', on_delete=models.CASCADE, related_name='ac_algo')
|
||||
vchannel = models.ForeignKey(TDevice, verbose_name='视频通道', on_delete=models.CASCADE, related_name='ac_vchannel')
|
||||
always_on = models.BooleanField("实时开启", default=False)
|
||||
|
@ -58,7 +58,7 @@ class AlgoChannel(BaseModel):
|
|||
|
||||
class NotifySetting(CommonADModel):
|
||||
"""
|
||||
提醒配置
|
||||
TN: 提醒配置
|
||||
"""
|
||||
PUSH_FILTER1_CHOICES = (
|
||||
(10, '当事人部门'),
|
||||
|
@ -84,7 +84,7 @@ class NotifySetting(CommonADModel):
|
|||
|
||||
class Event(CommonBDModel):
|
||||
"""
|
||||
事件
|
||||
TN: 事件
|
||||
"""
|
||||
EVENT_MARK_CHOICES = (
|
||||
(10, '正常'),
|
||||
|
@ -118,6 +118,7 @@ class Event(CommonBDModel):
|
|||
|
||||
|
||||
class Eventdo(BaseModel):
|
||||
"""TN: 事件处理记录"""
|
||||
cate = models.ForeignKey(EventCate, verbose_name='关联事件种类',
|
||||
on_delete=models.CASCADE, related_name='do_cate')
|
||||
event = models.ForeignKey(Event, verbose_name='关联事件',
|
||||
|
@ -130,7 +131,7 @@ class Eventdo(BaseModel):
|
|||
|
||||
class Remind(BaseModel):
|
||||
"""
|
||||
事件提醒表
|
||||
TN: 事件提醒表
|
||||
"""
|
||||
event = models.ForeignKey(Event, verbose_name='关联事件',
|
||||
on_delete=models.CASCADE)
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.urls import path, include
|
|||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/ecm/'
|
||||
HTML_BASE_URL = 'ecm/'
|
||||
HTML_BASE_URL = 'dhtml/ecm/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('event_cate', EventCateViewSet, basename='event_cate')
|
||||
|
|
|
@ -6,6 +6,7 @@ from apps.system.models import User, Dept, File
|
|||
|
||||
|
||||
class Train(CommonADModel):
|
||||
"""TN: 线下培训"""
|
||||
T_L_POST = 10
|
||||
T_L_TEAM = 20
|
||||
T_L_DEPT = 30
|
||||
|
@ -27,6 +28,7 @@ class Train(CommonADModel):
|
|||
|
||||
|
||||
class Questioncat(CommonAModel):
|
||||
"""TN: 题库分类"""
|
||||
name = models.CharField(max_length=200, verbose_name="名称")
|
||||
parent = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="父级", related_name="cate_parent")
|
||||
sort = models.PositiveIntegerField(default=0, verbose_name="排序")
|
||||
|
@ -40,6 +42,7 @@ class Questioncat(CommonAModel):
|
|||
|
||||
|
||||
class Question(CommonAModel):
|
||||
"""TN: 题目"""
|
||||
Q_DAN = 10
|
||||
Q_DUO = 20
|
||||
Q_PAN = 30
|
||||
|
@ -65,6 +68,7 @@ class Question(CommonAModel):
|
|||
|
||||
|
||||
class Paper(CommonAModel):
|
||||
"""TN: 押题卷"""
|
||||
PAPER_FIX = 10 # 固定试卷
|
||||
PAPER_RANDOM = 20 # 自动抽题
|
||||
|
||||
|
@ -94,6 +98,7 @@ class Paper(CommonAModel):
|
|||
|
||||
|
||||
class PaperQuestion(BaseModel):
|
||||
"""TN: 试卷-试题关系表"""
|
||||
paper = models.ForeignKey(Paper, on_delete=models.CASCADE, verbose_name="试卷")
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name="试题")
|
||||
total_score = models.FloatField(default=0, verbose_name="单题满分")
|
||||
|
@ -101,6 +106,7 @@ class PaperQuestion(BaseModel):
|
|||
|
||||
|
||||
class Exam(CommonADModel):
|
||||
"""TN: 在线考试"""
|
||||
name = models.CharField("名称", max_length=100)
|
||||
open_time = models.DateTimeField("开启时间")
|
||||
close_time = models.DateTimeField("关闭时间", null=True, blank=True)
|
||||
|
@ -120,7 +126,7 @@ class Exam(CommonADModel):
|
|||
|
||||
class ExamRecord(CommonADModel):
|
||||
"""
|
||||
考试记录表
|
||||
TN: 考试记录表
|
||||
"""
|
||||
|
||||
exam = models.ForeignKey(Exam, on_delete=models.CASCADE, verbose_name="关联考试", related_name="record_exam")
|
||||
|
@ -139,6 +145,7 @@ class ExamRecord(CommonADModel):
|
|||
|
||||
|
||||
class AnswerDetail(BaseModel):
|
||||
"""TN: 答题记录详情表"""
|
||||
examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE, related_name="detail_er")
|
||||
total_score = models.FloatField(default=0, verbose_name="该题满分")
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE)
|
||||
|
|
|
@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
|
|||
from apps.edu.views import QuestioncatViewSet, QuestionViewSet, PaperViewSet, ExamViewSet, ExamRecordViewSet, TrainRecordViewSet
|
||||
|
||||
API_BASE_URL = 'api/edu/'
|
||||
HTML_BASE_URL = 'edu/'
|
||||
HTML_BASE_URL = 'dhtml/edu/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('questioncat', QuestioncatViewSet, basename='questioncat')
|
||||
|
|
266
apps/em/cd.py
266
apps/em/cd.py
|
@ -1,26 +1,252 @@
|
|||
import socket
|
||||
from rest_framework.exceptions import ParseError
|
||||
import json
|
||||
import time
|
||||
from django.core.cache import cache
|
||||
from apps.utils.thread import MyThread
|
||||
import uuid
|
||||
import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
def get_checksum(body_msg):
|
||||
return sum(body_msg) & 0xFF
|
||||
|
||||
def handle_bytes(arr):
|
||||
if len(arr) < 8:
|
||||
return f"返回数据长度错误-{arr}"
|
||||
|
||||
if arr[0] != 0xEB or arr[1] != 0x90:
|
||||
return "数据头不正确"
|
||||
|
||||
|
||||
# 读取长度信息
|
||||
length_arr = arr[2:4][::-1] # 反转字节
|
||||
length = int.from_bytes(length_arr, byteorder='little', signed=True) # 小端格式
|
||||
|
||||
# 提取内容
|
||||
body = arr[4:4 + length - 3]
|
||||
|
||||
# 校验和检查
|
||||
check_sum = get_checksum(body)
|
||||
if check_sum != arr[length + 1]:
|
||||
return "校验错误"
|
||||
|
||||
|
||||
# 尾部标识检查
|
||||
if arr[length + 2] != 0xFF or arr[length + 3] != 0xFE:
|
||||
return "尾错误"
|
||||
|
||||
content = body.decode('utf-8')
|
||||
|
||||
res = json.loads(content)
|
||||
|
||||
return res[0]
|
||||
|
||||
def get_tyy_data_t(host, port, tid):
|
||||
cd_thread_key_id = f"cd_thread_{host}_{port}_id"
|
||||
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
|
||||
sc = None
|
||||
def connect_and_send(retry=1):
|
||||
nonlocal sc
|
||||
try:
|
||||
if sc is None:
|
||||
sc = socket.socket()
|
||||
sc.connect((host, int(port)))
|
||||
sc.sendall(b"R")
|
||||
except BrokenPipeError:
|
||||
if retry > 0:
|
||||
connect_and_send(retry-1)
|
||||
else:
|
||||
if sc:
|
||||
try:
|
||||
sc.close()
|
||||
except Exception:
|
||||
pass
|
||||
sc = None
|
||||
except OSError as e:
|
||||
sc = None
|
||||
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"})
|
||||
except ConnectionResetError:
|
||||
sc = None
|
||||
cache.set(cd_thread_key_val, {"err_msg": "采集器重置了连接"})
|
||||
except socket.timeout:
|
||||
sc = None
|
||||
cache.set(cd_thread_key_val, {"err_msg": "采集器连接超时"})
|
||||
except Exception as e:
|
||||
sc = None
|
||||
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"})
|
||||
|
||||
while cache.get(cd_thread_key_id) == tid:
|
||||
if cache.get(cd_thread_key_val) == "get":
|
||||
cache.set(cd_thread_key_val, "working")
|
||||
connect_and_send()
|
||||
if sc is None:
|
||||
continue
|
||||
resp = sc.recv(1024)
|
||||
res = handle_bytes(resp)
|
||||
if isinstance(res, str):
|
||||
cache.set(cd_thread_key_val, {"err_msg": f'采集器返回数据错误-{res}'})
|
||||
elif not res:
|
||||
cache.set(cd_thread_key_val, {"err_msg": f"采集器返回数据为空-{str(res)}"})
|
||||
else:
|
||||
myLogger.info(f"采集器返回数据-{res}")
|
||||
cache.set(cd_thread_key_val, res)
|
||||
time.sleep(0.3)
|
||||
|
||||
if sc:
|
||||
try:
|
||||
sc.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_tyy_data_2(*args, retry=1):
|
||||
host, port = args[0], int(args[1])
|
||||
cd_thread_key_id = f"cd_thread_{host}_{port}_id"
|
||||
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
|
||||
cd_thread_val_id = cache.get(cd_thread_key_id, default=None)
|
||||
if cd_thread_val_id is None:
|
||||
tid = uuid.uuid4()
|
||||
cache.set(cd_thread_key_id, tid, timeout=10800)
|
||||
cd_thread = MyThread(target=get_tyy_data_t, args=(host, port, tid), daemon=True)
|
||||
cd_thread.start()
|
||||
cache.set(cd_thread_key_val, "get")
|
||||
num = 0
|
||||
get_val = False
|
||||
|
||||
while True:
|
||||
num += 1
|
||||
if num > 8:
|
||||
break
|
||||
val = cache.get(cd_thread_key_val)
|
||||
if isinstance(val, dict):
|
||||
get_val = True
|
||||
if "err_msg" in val:
|
||||
raise ParseError(val["err_msg"])
|
||||
return val
|
||||
time.sleep(0.3)
|
||||
|
||||
if not get_val and retry > 0:
|
||||
cache.set(cd_thread_key_id, None)
|
||||
get_tyy_data_2(*args, retry=retry-1)
|
||||
|
||||
|
||||
sc_all = {}
|
||||
sc_lock = threading.Lock()
|
||||
|
||||
def get_tyy_data_1(*args, retry=1):
|
||||
host, port = args[0], int(args[1])
|
||||
global sc_all
|
||||
sc = None
|
||||
|
||||
def connect_and_send(retry=1):
|
||||
nonlocal sc
|
||||
sc = sc_all.get(f"{host}_{port}", None)
|
||||
try:
|
||||
if sc is None:
|
||||
sc = socket.socket()
|
||||
sc.settimeout(5) # 设置超时
|
||||
sc.connect((host, port))
|
||||
sc_all[f"{host}_{port}"] = sc
|
||||
else:
|
||||
# 清空接收缓冲区
|
||||
sc.settimeout(0.1) # 设置短暂超时
|
||||
for _ in range(5):
|
||||
try:
|
||||
data = sc.recv(65536)
|
||||
if not data:
|
||||
break
|
||||
except (socket.timeout, BlockingIOError):
|
||||
break
|
||||
sc.settimeout(5) # 恢复原超时设置
|
||||
sc.sendall(b"R")
|
||||
except BrokenPipeError:
|
||||
if retry > 0:
|
||||
if sc:
|
||||
try:
|
||||
sc.close()
|
||||
except Exception:
|
||||
pass
|
||||
sc_all.pop(f"{host}_{port}", None)
|
||||
return connect_and_send(retry-1)
|
||||
else:
|
||||
if sc:
|
||||
try:
|
||||
sc.close()
|
||||
except Exception:
|
||||
pass
|
||||
sc_all.pop(f"{host}_{port}", None)
|
||||
sc = None
|
||||
raise ParseError("采集器连接失败-管道重置")
|
||||
except OSError as e:
|
||||
if sc:
|
||||
try:
|
||||
sc.close()
|
||||
except Exception:
|
||||
pass
|
||||
sc_all.pop(f"{host}_{port}", None)
|
||||
sc = None
|
||||
raise ParseError(f"采集器连接失败-{str(e)}")
|
||||
except TimeoutError as e:
|
||||
if sc:
|
||||
try:
|
||||
sc.close()
|
||||
except Exception:
|
||||
pass
|
||||
sc_all.pop(f"{host}_{port}", None)
|
||||
sc = None
|
||||
raise ParseError(f"采集器连接超时-{str(e)}")
|
||||
|
||||
with sc_lock:
|
||||
connect_and_send()
|
||||
resp = sc.recv(1024)
|
||||
res = handle_bytes(resp)
|
||||
# myLogger.error(res)
|
||||
if isinstance(res, str):
|
||||
raise ParseError(f'采集器返回数据错误-{res}')
|
||||
else:
|
||||
return res
|
||||
|
||||
|
||||
def get_tyy_data_3(*args, retry=2):
|
||||
host, port = args[0], int(args[1])
|
||||
for attempt in range(retry):
|
||||
try:
|
||||
# 每次请求都新建连接(确保无共享状态)
|
||||
with socket.create_connection((host, port), timeout=10) as sc:
|
||||
sc.sendall(b"R")
|
||||
|
||||
# 接收完整响应(避免数据不完整)
|
||||
# resp = b""
|
||||
# while True:
|
||||
# chunk = sc.recv(4096)
|
||||
# if not chunk:
|
||||
# break
|
||||
# resp += chunk
|
||||
resp = sc.recv(4096)
|
||||
if not resp:
|
||||
raise ParseError("设备未启动")
|
||||
|
||||
res = handle_bytes(resp)
|
||||
if isinstance(res, str):
|
||||
raise ParseError(f"采集器返回数据错误: {res}")
|
||||
return res
|
||||
except (socket.timeout, ConnectionError) as e:
|
||||
if attempt == retry - 1: # 最后一次尝试失败才报错
|
||||
raise ParseError(f"采集器连接失败: {str(e)}")
|
||||
time.sleep(0.5) # 失败后等待 1s 再重试
|
||||
except ParseError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ParseError(f"未知错误: {str(e)}")
|
||||
|
||||
|
||||
def get_tyy_data(*args):
|
||||
sc = socket.socket()
|
||||
try:
|
||||
sc.connect((args[0], int(args[1])))
|
||||
except Exception:
|
||||
raise ParseError("无法连接到采集器")
|
||||
sc.send(b"R")
|
||||
resp = sc.recv(1024)
|
||||
if len(resp) < 8:
|
||||
raise ParseError("设备未启动")
|
||||
try:
|
||||
json_data = resp[5:-4]
|
||||
json_str = json_data.decode('utf-8')
|
||||
res = json.loads(json_str)
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
sc.close()
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(get_tyy_data())
|
|
@ -9,6 +9,7 @@ etype_choices = ((10, "生产设备"), (20, "计量设备"), (30, "治理设备"
|
|||
|
||||
|
||||
class Ecate(CommonADModel):
|
||||
"""TN:设备类别"""
|
||||
name = models.CharField("名称", max_length=50, unique=True)
|
||||
code = models.CharField("编码", max_length=50, unique=True, null=True, blank=True)
|
||||
type = models.PositiveSmallIntegerField("类型", choices=etype_choices, help_text=str(etype_choices))
|
||||
|
@ -22,8 +23,7 @@ class Ecate(CommonADModel):
|
|||
|
||||
class Equipment(CommonBModel):
|
||||
"""
|
||||
设备台账信息
|
||||
其中belong_dept是责任部门
|
||||
TN:设备台账信息,其中belong_dept是责任部门
|
||||
"""
|
||||
RUNING = 10
|
||||
STANDBY = 20
|
||||
|
@ -126,7 +126,7 @@ class Equipment(CommonBModel):
|
|||
|
||||
class EcheckRecord(CommonADModel):
|
||||
"""
|
||||
校准检定记录
|
||||
TN:校准检定记录
|
||||
"""
|
||||
|
||||
CHECK_CHOICES = ((10, "正常"), (20, "异常"))
|
||||
|
@ -139,7 +139,7 @@ class EcheckRecord(CommonADModel):
|
|||
|
||||
class EInspect(CommonADModel):
|
||||
"""
|
||||
巡检记录
|
||||
TN:巡检记录
|
||||
"""
|
||||
|
||||
INSPECT_RESULTS = (
|
||||
|
|
|
@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
|
|||
from apps.em.views import EquipmentViewSet, EcheckRecordViewSet, EInspectViewSet, EcateViewSet, CdView
|
||||
|
||||
API_BASE_URL = 'api/em/'
|
||||
HTML_BASE_URL = 'em/'
|
||||
HTML_BASE_URL = 'dhtml/em/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('equipment', EquipmentViewSet, basename='equipment')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,6 +5,7 @@ from apps.mtm.models import Material, Mgroup, Team
|
|||
from django_celery_beat.models import PeriodicTask
|
||||
|
||||
class Xscript(BaseModel):
|
||||
"""TN:脚本"""
|
||||
name = models.CharField("脚本名称", max_length=50)
|
||||
code = models.TextField("脚本内容", null=True, blank=True)
|
||||
base_data = models.JSONField("基础数据", default=dict, null=True, blank=True)
|
||||
|
@ -13,7 +14,7 @@ class Xscript(BaseModel):
|
|||
periodictask = models.ForeignKey(PeriodicTask, verbose_name='关联定时任务', on_delete=models.CASCADE, related_name='xscript_periodictask', null=True, blank=True)
|
||||
|
||||
class Mpoint(CommonBModel):
|
||||
"""测点"""
|
||||
"""TN:测点"""
|
||||
MT_AUTO = 10
|
||||
MT_COMPUTE = 20
|
||||
MT_MANUAL = 30
|
||||
|
@ -61,7 +62,7 @@ class Mpoint(CommonBModel):
|
|||
cal_coefficient = models.FloatField("计算系数", null=True, blank=True)
|
||||
|
||||
mpoint_from = models.ForeignKey("self", verbose_name="来源自采测点", related_name="mp_mpoint_from", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "运行时统计")], null=True, blank=True)
|
||||
cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "不涉及"), (20, "运行时统计")], null=True, blank=True)
|
||||
|
||||
@classmethod
|
||||
def cache_key(cls, code: str):
|
||||
|
@ -69,7 +70,7 @@ class Mpoint(CommonBModel):
|
|||
|
||||
class MpLogx(models.Model):
|
||||
"""
|
||||
测点记录超表
|
||||
TN:测点记录超表
|
||||
"""
|
||||
|
||||
timex = models.DateTimeField("采集时间", primary_key=True)
|
||||
|
@ -87,7 +88,7 @@ class MpLogx(models.Model):
|
|||
|
||||
|
||||
class MpLog(BaseModel):
|
||||
"""旧表(已废弃)"""
|
||||
"""TN:旧表(已废弃)"""
|
||||
|
||||
mpoint = models.ForeignKey(Mpoint, verbose_name="关联测点", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
tag_id = models.BigIntegerField("记录ID", db_index=True)
|
||||
|
@ -97,7 +98,7 @@ class MpLog(BaseModel):
|
|||
|
||||
|
||||
class MpointStat(CommonADModel):
|
||||
"""测点统计表"""
|
||||
"""TN:测点统计表"""
|
||||
|
||||
type = models.CharField("统计维度", max_length=50, default="hour", help_text="year/month/day/year_s/month_s/month_st/day_s/sflog/hour_s/hour")
|
||||
year = models.PositiveSmallIntegerField("年", null=True, blank=True)
|
||||
|
@ -123,7 +124,7 @@ class MpointStat(CommonADModel):
|
|||
|
||||
class EnStat(BaseModel):
|
||||
"""
|
||||
能源数据统计表
|
||||
TN:能源数据统计表
|
||||
"""
|
||||
|
||||
type = models.CharField("统计维度", max_length=50, default="hour", help_text="year_s/month_s/month_st/day_s/sflog/hour_s")
|
||||
|
@ -174,7 +175,7 @@ class EnStat(BaseModel):
|
|||
|
||||
class EnStat2(BaseModel):
|
||||
"""
|
||||
能源数据统计表2
|
||||
TN:能源数据统计表2
|
||||
"""
|
||||
|
||||
type = models.CharField("统计维度", max_length=50, default="month_s", help_text="month_s/day_s")
|
||||
|
|
|
@ -64,7 +64,7 @@ def get_can_save_from_save_expr(expr_str: str, self_val) -> bool:
|
|||
if current_val is None:
|
||||
return True
|
||||
else:
|
||||
expr_str = expr_str.replace(f"{{{match}}}", current_val)
|
||||
expr_str = expr_str.replace(f"{{{match}}}", str(current_val))
|
||||
try:
|
||||
rval = eval(expr_str)
|
||||
except Exception as e:
|
||||
|
@ -207,6 +207,8 @@ class MpointCache:
|
|||
set_eq_rs(ep_belong_id, timex, Equipment.OFFLINE)
|
||||
|
||||
def set(self, last_timex: datetime, last_val):
|
||||
# last_timex保存到秒
|
||||
last_timex = last_timex.replace(microsecond=0)
|
||||
current_cache_val = self.data
|
||||
cache_key = self.cache_key
|
||||
last_data = current_cache_val["last_data"]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 = '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'),
|
||||
]
|
|
@ -12,6 +12,7 @@ from apps.enm.tasks import cal_mpointstat_manual
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.views import APIView
|
||||
from apps.enm.tasks import cal_mpointstats_duration
|
||||
from apps.enm.services import king_sync, MpointCache
|
||||
from django.db import transaction
|
||||
|
@ -21,7 +22,13 @@ from apps.enm.services import get_analyse_data_mgroups_duration
|
|||
from django.db.models import Sum
|
||||
import logging
|
||||
from django.core.cache import cache
|
||||
from apps.utils.sql import query_one_dict, query_all_dict
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from django.utils import timezone
|
||||
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
class MpointViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list:测点
|
||||
|
@ -84,6 +91,34 @@ class MpointViewSet(CustomModelViewSet):
|
|||
king_sync(getattr(settings, "KING_PROJECTNAME", ""))
|
||||
return Response()
|
||||
|
||||
@action(methods=["post"], detail=False, perms_map={"post": "mpoint.create"}, serializer_class=Serializer)
|
||||
def show_picture(self, request, *args, **kwargs):
|
||||
import requests
|
||||
import os
|
||||
headers = {
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
}
|
||||
url = "http://localhost:8093/boxplot"
|
||||
payload = {
|
||||
"startTime1": request.data.get("startTime1"),
|
||||
"endTime1": request.data.get("endTime1"),
|
||||
"startTime2": request.data.get("startTime2"),
|
||||
"endTime2": request.data.get("endTime2")
|
||||
}
|
||||
try:
|
||||
response = requests.request("POST", url, json=payload, headers=headers)
|
||||
except Exception as e:
|
||||
myLogger.error(e)
|
||||
pic_dir = os.path.join(settings.MEDIA_ROOT, "box_pic")
|
||||
os.makedirs(pic_dir, exist_ok=True)
|
||||
file_name= datetime.now().strftime('%Y%m%d_%H%M%S')+'.png'
|
||||
pic_path = os.path.join(pic_dir, file_name)
|
||||
with open(pic_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
rel_path = os.path.join('media/box_pic', file_name)
|
||||
rel_path = rel_path.replace('\\', '/')
|
||||
return Response({"rel_path": rel_path})
|
||||
|
||||
|
||||
class XscriptViewSet(CustomModelViewSet):
|
||||
"""
|
||||
|
@ -138,6 +173,97 @@ class XscriptViewSet(CustomModelViewSet):
|
|||
# select_related_fields = ['mpoint']
|
||||
# filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept']
|
||||
|
||||
class MpLogxAPIView(APIView):
|
||||
"""
|
||||
list:测点采集数据
|
||||
|
||||
测点采集数据
|
||||
"""
|
||||
perms_map = {"get": "*"}
|
||||
|
||||
@swagger_auto_schema(manual_parameters=[
|
||||
openapi.Parameter('mpoint', openapi.IN_QUERY, description='测点ID', type=openapi.TYPE_STRING),
|
||||
openapi.Parameter('timex__gte', openapi.IN_QUERY, description='开始时间', type=openapi.TYPE_STRING),
|
||||
openapi.Parameter('timex__lte', openapi.IN_QUERY, description='结束时间', type=openapi.TYPE_STRING),
|
||||
openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER),
|
||||
openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量', type=openapi.TYPE_INTEGER),
|
||||
openapi.Parameter('ordering', openapi.IN_QUERY, description='排序字段,如 -timex', type=openapi.TYPE_STRING),
|
||||
openapi.Parameter('fields', openapi.IN_QUERY, description='返回字段,如 timex,val_float,val_int', type=openapi.TYPE_STRING),
|
||||
])
|
||||
def get(self, request, *args, **kwargs):
|
||||
mpoint = request.query_params.get("mpoint", None)
|
||||
timex__gte_str = request.query_params.get("timex__gte", None)
|
||||
timex__lte_str = request.query_params.get("timex__lte", None)
|
||||
page = int(request.query_params.get("page", 1))
|
||||
page_size = int(request.query_params.get("page_size", 20))
|
||||
fields = request.query_params.get("fields", None)
|
||||
if page < 0 and page_size < 0:
|
||||
raise ParseError("page, page_size must be positive")
|
||||
ordering = request.query_params.get("ordering", "-timex") # 默认倒序
|
||||
|
||||
if mpoint is None or timex__gte_str is None:
|
||||
raise ParseError("mpoint, timex__gte are required")
|
||||
|
||||
# 处理时间
|
||||
timex__gte = timezone.make_aware(datetime.strptime(timex__gte_str, "%Y-%m-%d %H:%M:%S"))
|
||||
timex__lte = timezone.make_aware(datetime.strptime(timex__lte_str, "%Y-%m-%d %H:%M:%S")) if timex__lte_str else timezone.now()
|
||||
|
||||
# 统计总数
|
||||
count_sql = """SELECT COUNT(*) AS total_count FROM enm_mplogx
|
||||
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s"""
|
||||
count_data = query_one_dict(count_sql, [mpoint, timex__gte, timex__lte], with_time_format=True)
|
||||
|
||||
# 排序白名单
|
||||
allowed_fields = {"timex", "val_mrs", "val_int", "val_float"} # 根据表字段修改
|
||||
order_fields = []
|
||||
for field in ordering.split(","):
|
||||
field = field.strip()
|
||||
if not field:
|
||||
continue
|
||||
desc = field.startswith("-")
|
||||
field_name = field[1:] if desc else field
|
||||
if field_name in allowed_fields:
|
||||
order_fields.append(f"{field_name} {'DESC' if desc else 'ASC'}")
|
||||
|
||||
# 如果没有合法字段,使用默认排序
|
||||
if not order_fields:
|
||||
order_fields = ["timex DESC"]
|
||||
|
||||
order_clause = "ORDER BY " + ", ".join(order_fields)
|
||||
|
||||
# 构造 SQL
|
||||
if page == 0:
|
||||
if fields:
|
||||
# 过滤白名单,避免非法列
|
||||
fields = [f for f in fields.split(",") if f in allowed_fields]
|
||||
if not fields:
|
||||
fields = ["timex", "val_float", "val_int"] # 默认列
|
||||
select_clause = ", ".join(fields)
|
||||
else:
|
||||
select_clause = "timex, val_float, val_int" # 默认列
|
||||
page_sql = f"""SELECT {select_clause} FROM enm_mplogx
|
||||
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
|
||||
{order_clause}"""
|
||||
page_params = [mpoint, timex__gte, timex__lte]
|
||||
else:
|
||||
page_sql = f"""SELECT * FROM enm_mplogx
|
||||
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
|
||||
{order_clause} LIMIT %s OFFSET %s"""
|
||||
page_params = [mpoint, timex__gte, timex__lte, page_size, (page-1)*page_size]
|
||||
|
||||
page_data = query_all_dict(page_sql, page_params, with_time_format=True)
|
||||
|
||||
if page == 0:
|
||||
return Response(page_data)
|
||||
|
||||
return Response({
|
||||
"count": count_data["total_count"],
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"results": page_data
|
||||
})
|
||||
|
||||
|
||||
|
||||
class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
|
@ -151,11 +277,20 @@ class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
|
|||
serializer_class = MpLogxSerializer
|
||||
filterset_fields = {
|
||||
"timex": ["exact", "gte", "lte", "year", "month", "day"],
|
||||
"mpoint": ["exact"],
|
||||
"mpoint": ["exact", "in"],
|
||||
"mpoint__ep_monitored": ["exact"]
|
||||
}
|
||||
ordering_fields = ["timex"]
|
||||
ordering = ["-timex"]
|
||||
|
||||
@action(methods=["get"], detail=False, perms_map={"get": "*"})
|
||||
def to_wide(self, request, *args, **kwargs):
|
||||
"""转换为宽表
|
||||
|
||||
转换为宽表
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
|
||||
class MpointStatViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
|
|
|
@ -6,6 +6,7 @@ from apps.em.models import Equipment
|
|||
|
||||
|
||||
class Drain(CommonBModel):
|
||||
"""TN:排口表"""
|
||||
DR_TYPE_CHOICES = (
|
||||
(10, "排放口"),
|
||||
(20, "污染源"),
|
||||
|
@ -37,7 +38,7 @@ class Drain(CommonBModel):
|
|||
|
||||
class DrainEquip(BaseModel):
|
||||
"""
|
||||
排口/设备关系表
|
||||
TN:排口/设备关系表
|
||||
"""
|
||||
|
||||
drain = models.ForeignKey(Drain, verbose_name="排口", related_name="drainequip_drain", on_delete=models.CASCADE)
|
||||
|
@ -50,7 +51,7 @@ class DrainEquip(BaseModel):
|
|||
|
||||
class EnvData(models.Model):
|
||||
"""
|
||||
环保监测数据
|
||||
TN:环保监测数据
|
||||
"""
|
||||
|
||||
enp_fields = [
|
||||
|
@ -105,6 +106,7 @@ class EnvData(models.Model):
|
|||
|
||||
|
||||
class VehicleAccess(BaseModel):
|
||||
"""TN:车辆出入厂记录"""
|
||||
type = models.PositiveSmallIntegerField("出入类型", default=1, help_text="1: 进厂, 2: 出厂")
|
||||
vehicle_number = models.CharField("车牌号", max_length=10)
|
||||
access_time = models.DateTimeField("出入时间", null=True, blank=True)
|
||||
|
@ -114,6 +116,7 @@ class VehicleAccess(BaseModel):
|
|||
|
||||
|
||||
class CarWash(BaseModel):
|
||||
"""TN:洗车记录"""
|
||||
station = models.ForeignKey(Equipment, verbose_name="洗车台", on_delete=models.CASCADE)
|
||||
vehicle_number = models.CharField("车牌号", max_length=10, default="")
|
||||
start_time = models.DateTimeField("洗车时间", null=True, blank=True)
|
||||
|
|
|
@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
|
|||
from .views import DrainViewSet, DrainEquipViewSet, VehicleAccessViewSet, EnvDataViewSet, CarWashViewSet
|
||||
|
||||
API_BASE_URL = 'api/enp/'
|
||||
HTML_BASE_URL = 'enp/'
|
||||
HTML_BASE_URL = 'dhtml/enp/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('drain', DrainViewSet, basename='drain')
|
||||
|
|
|
@ -6,6 +6,7 @@ from apps.wpm.models import SfLog
|
|||
# Create your models here.
|
||||
|
||||
class Fee(BaseModel):
|
||||
"""TN:费用"""
|
||||
name = models.CharField('名称', max_length=20)
|
||||
cate = models.CharField('父名称', max_length=20)
|
||||
element = models.CharField('要素', max_length=20, help_text='直接材料/直接人工/制造费用')
|
||||
|
@ -13,6 +14,7 @@ class Fee(BaseModel):
|
|||
|
||||
|
||||
class FeeSet(CommonADModel):
|
||||
"""TN:费用集合"""
|
||||
year = models.PositiveSmallIntegerField('年份')
|
||||
month = models.PositiveSmallIntegerField('月份')
|
||||
mgroup = models.ForeignKey(Mgroup, verbose_name='关联工段', on_delete=models.CASCADE)
|
||||
|
@ -21,6 +23,7 @@ class FeeSet(CommonADModel):
|
|||
|
||||
|
||||
class PriceSet(CommonADModel):
|
||||
"""TN:价格集合"""
|
||||
year = models.PositiveSmallIntegerField('年份')
|
||||
month = models.PositiveSmallIntegerField('月份')
|
||||
material = models.ForeignKey('mtm.material', on_delete=models.CASCADE, verbose_name='关联物料')
|
||||
|
|
|
@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
|
|||
from apps.fim.views import (PriceSetViewSet, FeeSetViewSet, FeeViewSet)
|
||||
|
||||
API_BASE_URL = 'api/fim/'
|
||||
HTML_BASE_URL = 'fim/'
|
||||
HTML_BASE_URL = 'dhtml/fim/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('priceset', PriceSetViewSet, basename='priceset')
|
||||
|
|
|
@ -8,7 +8,7 @@ from datetime import timedelta
|
|||
|
||||
class Employee(CommonBModel):
|
||||
"""
|
||||
员工信息
|
||||
TN:员工信息
|
||||
"""
|
||||
JOB_ON = 10
|
||||
JOB_OFF = 20
|
||||
|
@ -77,7 +77,7 @@ class Employee(CommonBModel):
|
|||
|
||||
class NotWorkRemark(CommonADModel):
|
||||
"""
|
||||
离岗说明
|
||||
TN:离岗说明
|
||||
"""
|
||||
not_work_date = models.DateField('未打卡日期')
|
||||
user = models.ForeignKey(User, verbose_name='用户', on_delete=models.CASCADE)
|
||||
|
@ -86,7 +86,7 @@ class NotWorkRemark(CommonADModel):
|
|||
|
||||
class Attendance(CommonADModel):
|
||||
"""
|
||||
到岗记录
|
||||
TN:到岗记录
|
||||
"""
|
||||
ATT_STATE_CHOICES = [
|
||||
('pending', '待定'),
|
||||
|
@ -119,7 +119,7 @@ class Attendance(CommonADModel):
|
|||
|
||||
class ClockRecord(BaseModel):
|
||||
"""
|
||||
打卡记录
|
||||
TN:打卡记录
|
||||
"""
|
||||
ClOCK_ON = 10
|
||||
CLOCK_OFF = 20
|
||||
|
@ -157,7 +157,7 @@ class ClockRecord(BaseModel):
|
|||
|
||||
class Certificate(CommonAModel):
|
||||
"""
|
||||
证书
|
||||
TN:证书
|
||||
"""
|
||||
CERT_OK = 10
|
||||
CERT_EXPIRING = 20
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.urls import path, include
|
|||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
API_BASE_URL = 'api/hrm/'
|
||||
HTML_BASE_URL = 'hrm/'
|
||||
HTML_BASE_URL = 'dhtml/hrm/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('employee', EmployeeViewSet, basename='employee')
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.ichat'
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
from django.db import models
|
||||
from apps.system.models import CommonADModel, BaseModel
|
||||
|
||||
# Create your models here.
|
||||
class Conversation(CommonADModel):
|
||||
"""
|
||||
TN: 对话
|
||||
"""
|
||||
title = models.CharField(max_length=200, default='新对话',verbose_name='对话标题')
|
||||
|
||||
class Message(BaseModel):
|
||||
"""
|
||||
TN: 消息
|
||||
"""
|
||||
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages', verbose_name='对话')
|
||||
content = models.TextField(verbose_name='消息内容')
|
||||
role = models.CharField("角色", max_length=10, default='user', help_text="system/user")
|
|
@ -0,0 +1,14 @@
|
|||
# 角色
|
||||
你是一位数据分析专家和前端程序员,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述, 并形成报告。
|
||||
# 技能
|
||||
1. 仔细分析用户提供的JSON格式数据,分析用户需求。
|
||||
2. 依据得到的需求, 分别获取JSON数据中的关键信息。
|
||||
3. 根据2中的关键信息最优化选择表格/饼图/柱状图/折线图等格式绘制报告。
|
||||
# 回答要求
|
||||
1. 仅生成完整的HTML代码,所有功能都需要实现,支持响应式,不要输出任何解释或说明。
|
||||
2. 代码中如需要Echarts等js库,请直接使用中国大陆的CDN链接例如bootcdn的链接。
|
||||
3. 标题为 数据分析报告。
|
||||
3. 在开始部分,请以表格形式简略展示获取的JSON数据。
|
||||
4. 之后选择最合适的图表方式生成相应的图。
|
||||
5. 在最后提供可下载该报告的完整PDF的按钮和功能。
|
||||
6. 在最后提供可下载含有JSON数据的EXCEL文件的按钮和功能。
|
|
@ -0,0 +1,53 @@
|
|||
# 角色
|
||||
你是一位资深的Postgresql数据库SQL专家,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述,并生成准确可执行的SQL语句。
|
||||
# 技能
|
||||
1. 仔细分析用户提供的文本描述,明确用户需求。
|
||||
2. 根据对用户需求的理解,生成符合Postgresql数据库语法的准确可执行的SQL语句。
|
||||
# 回答要求
|
||||
1. 如果用户的询问未以 查询 开头,请直接回复 "请以 查询 开头,重新描述你的需求"。
|
||||
2. 生成的SQL语句必须符合Postgresql数据库的语法规范。
|
||||
3. 不要使用 Markerdown 和 SQL 语法格式输出,禁止添加语法标准、备注、说明等信息。
|
||||
4. 直接输出符合Postgresql标准的SQL语句,用txt纯文本格式展示即可。
|
||||
5. 如果无法生成符合要求的SQL语句,请直接回复 "无法生成"。
|
||||
# 示例
|
||||
1. 问:查询 外协白片抛 工段在2025年6月1日到2025年6月15日之间的生产合格数以及合格率等
|
||||
答:select
|
||||
sum(mlog.count_use) as 领用数,
|
||||
sum(mlog.count_real) as 生产数,
|
||||
sum(mlog.count_ok) as 合格数,
|
||||
sum(mlog.count_notok) as 不合格数,
|
||||
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
|
||||
from wpm_mlog mlog
|
||||
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
|
||||
where mlog.submit_time is not null
|
||||
and mgroup.name = '外协白片抛'
|
||||
and mlog.handle_date >= '2025-06-01'
|
||||
and mlog.handle_date <= '2025-06-15'
|
||||
2. 问:查询 黑化 工段在2025年6月的生产合格数以及合格率等
|
||||
答: select
|
||||
sum(mlog.count_use) as 领用数,
|
||||
sum(mlog.count_real) as 生产数,
|
||||
sum(mlog.count_ok) as 合格数,
|
||||
sum(mlog.count_notok) as 不合格数,
|
||||
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
|
||||
from wpm_mlog mlog
|
||||
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
|
||||
where mlog.submit_time is not null
|
||||
and mgroup.name = '黑化'
|
||||
and mlog.handle_date >= '2025-06-01'
|
||||
and mlog.handle_date <= '2025-06-30'
|
||||
3. 问:查询 各工段 在2025年6月的生产合格数以及合格率等
|
||||
答: select
|
||||
mgroup.name as 工段,
|
||||
sum(mlog.count_use) as 领用数,
|
||||
sum(mlog.count_real) as 生产数,
|
||||
sum(mlog.count_ok) as 合格数,
|
||||
sum(mlog.count_notok) as 不合格数,
|
||||
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
|
||||
from wpm_mlog mlog
|
||||
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
|
||||
where mlog.submit_time is not null
|
||||
and mlog.handle_date >= '2025-06-01'
|
||||
and mlog.handle_date <= '2025-06-30'
|
||||
group by mgroup.id
|
||||
order by mgroup.sort
|
|
@ -0,0 +1,22 @@
|
|||
import json
|
||||
from .models import Message
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
def stream_generator(stream_response: bytes, conversation_id: str):
|
||||
full_content = ''
|
||||
for chunk in stream_response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
full_content += chunk.decode('utf-8')
|
||||
try:
|
||||
data = json.loads(full_content)
|
||||
content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
||||
Message.objects.create(
|
||||
conversation_id=conversation_id,
|
||||
content=content
|
||||
)
|
||||
yield f" data:{content}\n\n"
|
||||
full_content = ''
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return StreamingHttpResponse(stream_generator(stream_response, conversation_id), content_type='text/event-stream')
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Conversation, Message
|
||||
from apps.utils.constants import EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ['id', 'conversation', 'content', 'role']
|
||||
read_only_fields = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class ConversationSerializer(serializers.ModelSerializer):
|
||||
messages = MessageSerializer(many=True, read_only=True)
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = ['id', 'title', 'messages']
|
||||
read_only_fields = EXCLUDE_FIELDS
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.ichat.views import QueryLLMviewSet, ConversationViewSet
|
||||
from apps.ichat.views2 import WorkChain
|
||||
|
||||
API_BASE_URL = 'api/ichat/'
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
router.register('conversation', ConversationViewSet, basename='conversation')
|
||||
router.register('message', QueryLLMviewSet, basename='message')
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
path(API_BASE_URL + 'workchain/ask/', WorkChain.as_view(), name='workchain')
|
||||
]
|
|
@ -0,0 +1,88 @@
|
|||
import re
|
||||
import psycopg2
|
||||
import threading
|
||||
from django.db import transaction
|
||||
from .models import Message
|
||||
|
||||
# 数据库连接
|
||||
def connect_db():
|
||||
from server.conf import DATABASES
|
||||
db_conf = DATABASES['default']
|
||||
conn = psycopg2.connect(
|
||||
host=db_conf['HOST'],
|
||||
port=db_conf['PORT'],
|
||||
user=db_conf['USER'],
|
||||
password=db_conf['PASSWORD'],
|
||||
database=db_conf['NAME']
|
||||
)
|
||||
return conn
|
||||
|
||||
def extract_sql_code(text):
|
||||
# 优先尝试 ```sql 包裹的语句
|
||||
match = re.search(r"```sql\s*(.+?)```", text, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
|
||||
# fallback: 寻找首个 select 语句
|
||||
match = re.search(r"(SELECT\s.+?;)", text, re.IGNORECASE | re.DOTALL)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_schema_text(conn, table_names:list):
|
||||
cur = conn.cursor()
|
||||
query = """
|
||||
SELECT
|
||||
table_name, column_name, data_type
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_schema = 'public'
|
||||
and table_name in %s;
|
||||
"""
|
||||
cur.execute(query, (tuple(table_names), ))
|
||||
|
||||
schema = {}
|
||||
for table_name, column_name, data_type in cur.fetchall():
|
||||
if table_name not in schema:
|
||||
schema[table_name] = []
|
||||
schema[table_name].append(f"{column_name} ({data_type})")
|
||||
cur.close()
|
||||
schema_text = ""
|
||||
for table_name, columns in schema.items():
|
||||
schema_text += f"表{table_name} 包含列:{', '.join(columns)}\n"
|
||||
return schema_text
|
||||
|
||||
|
||||
def is_safe_sql(sql:str) -> bool:
|
||||
sql = sql.strip().lower()
|
||||
return sql.startswith("select") or sql.startswith("show") and not re.search(r"delete|update|insert|drop|create|alter", sql)
|
||||
|
||||
def execute_sql(conn, sql_query):
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql_query)
|
||||
try:
|
||||
rows = cur.fetchall()
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
result = [dict(zip(columns, row)) for row in rows]
|
||||
except psycopg2.ProgrammingError:
|
||||
result = cur.statusmessage
|
||||
cur.close()
|
||||
return result
|
||||
|
||||
def strip_sql_markdown(content: str) -> str:
|
||||
# 去掉包裹在 ```sql 或 ``` 中的内容
|
||||
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
return None
|
||||
|
||||
# ORM 写入包装函数
|
||||
def save_message_thread_safe(**kwargs):
|
||||
def _save():
|
||||
with transaction.atomic():
|
||||
Message.objects.create(**kwargs)
|
||||
threading.Thread(target=_save).start()
|
|
@ -0,0 +1,87 @@
|
|||
import requests
|
||||
from langchain_core.language_models import LLM
|
||||
from langchain_core.outputs import LLMResult, Generation
|
||||
from langchain_experimental.sql import SQLDatabaseChain
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
from server.conf import DATABASES
|
||||
from apps.ichat.serializers import CustomLLMrequestSerializer
|
||||
from rest_framework.views import APIView
|
||||
from urllib.parse import quote_plus
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
db_conf = DATABASES['default']
|
||||
# 密码需要 URL 编码(因为有特殊字符如 @)
|
||||
password_encodeed = quote_plus(db_conf['PASSWORD'])
|
||||
|
||||
db = SQLDatabase.from_uri(f"postgresql+psycopg2://{db_conf['USER']}:{password_encodeed}@{db_conf['HOST']}/{db_conf['NAME']}", include_tables=["enm_mpoint", "enm_mpointstat"])
|
||||
# model_url = "http://14.22.88.72:11025/v1/chat/completions"
|
||||
model_url = "http://139.159.180.64:11434/v1/chat/completions"
|
||||
|
||||
class CustomLLM(LLM):
|
||||
model_url: str
|
||||
mode: str = 'chat'
|
||||
def _call(self, prompt: str, stop: list = None) -> str:
|
||||
data = {
|
||||
"model":"glm4",
|
||||
"messages": self.build_message(prompt),
|
||||
"stream": False,
|
||||
}
|
||||
response = requests.post(self.model_url, json=data, timeout=600)
|
||||
response.raise_for_status()
|
||||
content = response.json()["choices"][0]["message"]["content"]
|
||||
print('content---', content)
|
||||
clean_sql = self.strip_sql_markdown(content) if self.mode == 'sql' else content.strip()
|
||||
return clean_sql
|
||||
|
||||
def _generate(self, prompts: list, stop: list = None) -> LLMResult:
|
||||
generations = []
|
||||
for prompt in prompts:
|
||||
text = self._call(prompt, stop)
|
||||
generations.append([Generation(text=text)])
|
||||
return LLMResult(generations=generations)
|
||||
|
||||
def strip_sql_markdown(self, content: str) -> str:
|
||||
import re
|
||||
# 去掉包裹在 ```sql 或 ``` 中的内容
|
||||
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
return content.strip()
|
||||
|
||||
def build_message(self, prompt: str) -> list:
|
||||
if self.mode == 'sql':
|
||||
system_prompt = (
|
||||
"你是一个 SQL 助手,严格遵循以下规则:\n"
|
||||
"1. 只返回 PostgreSQL 语法 SQL 语句。\n"
|
||||
"2. 严格禁止添加任何解释、注释、Markdown 代码块标记(包括 ```sql 和 ```)。\n"
|
||||
"3. 输出必须是纯 SQL,且可直接执行,无需任何额外处理。\n"
|
||||
"4. 在 SQL 中如有多个表,请始终使用表名前缀引用字段,避免字段歧义。"
|
||||
)
|
||||
else:
|
||||
system_prompt = "你是一个聊天助手,请根据用户的问题,提供简洁明了的答案。"
|
||||
return [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
return "custom_llm"
|
||||
|
||||
|
||||
class QueryLLMview(APIView):
|
||||
def post(self, request):
|
||||
serializer = CustomLLMrequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
prompt = serializer.validated_data['prompt']
|
||||
mode = serializer.validated_data.get('mode', 'chat')
|
||||
llm = CustomLLM(model_url=model_url, mode=mode)
|
||||
print('prompt---', prompt, mode)
|
||||
if mode == 'sql':
|
||||
chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)
|
||||
result = chain.invoke(prompt)
|
||||
else:
|
||||
result = llm._call(prompt)
|
||||
return Response({"result": result})
|
|
@ -0,0 +1,155 @@
|
|||
import requests
|
||||
import json
|
||||
from rest_framework.views import APIView
|
||||
from apps.ichat.serializers import MessageSerializer, ConversationSerializer
|
||||
from rest_framework.response import Response
|
||||
from apps.ichat.models import Conversation, Message
|
||||
from apps.ichat.utils import connect_db, extract_sql_code, execute_sql, get_schema_text, is_safe_sql, save_message_thread_safe
|
||||
from django.http import StreamingHttpResponse, JsonResponse
|
||||
from rest_framework.decorators import action
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
|
||||
# API_KEY = "sk-5644e2d6077b46b9a04a8a2b12d6b693"
|
||||
# API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
# MODEL = "qwen-plus"
|
||||
|
||||
# #本地部署的模式
|
||||
API_KEY = "JJVAide0hw3eaugGmxecyYYFw45FX2LfhnYJtC+W2rw"
|
||||
API_BASE = "http://106.0.4.200:9000/v1"
|
||||
MODEL = "qwen14b"
|
||||
|
||||
# google gemini
|
||||
# API_KEY = "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"
|
||||
# API_BASE = "https://openrouter.ai/api/v1"
|
||||
# MODEL="google/gemini-2.0-flash-exp:free"
|
||||
|
||||
# deepseek v3
|
||||
# API_KEY = "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"
|
||||
# API_BASE = "https://openrouter.ai/api/v1"
|
||||
# MODEL="deepseek/deepseek-chat-v3-0324:free"
|
||||
|
||||
TABLES = ["enm_mpoint", "enm_mpointstat", "enm_mplogx"] # 如果整个数据库全都给模型,准确率下降,所以只给模型部分表
|
||||
|
||||
|
||||
class QueryLLMviewSet(CustomModelViewSet):
|
||||
queryset = Message.objects.all()
|
||||
serializer_class = MessageSerializer
|
||||
ordering = ['create_time']
|
||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
||||
|
||||
@action(methods=['post'], detail=False, perms_map={'post':'*'} ,serializer_class=MessageSerializer)
|
||||
def completion(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
prompt = serializer.validated_data['content']
|
||||
conversation = serializer.validated_data['conversation']
|
||||
if not prompt or not conversation:
|
||||
return JsonResponse({"error": "缺少 prompt 或 conversation"}, status=400)
|
||||
save_message_thread_safe(content=prompt, conversation=conversation, role="user")
|
||||
url = f"{API_BASE}/chat/completions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY}"
|
||||
}
|
||||
|
||||
user_prompt = f"""
|
||||
我提问的问题是:{prompt}请判断我的问题是否与数据库查询或操作相关。如果是,回答"database";如果不是,回答"general"。
|
||||
|
||||
注意:
|
||||
只需回答"database"或"general"即可,不要有其他内容。
|
||||
"""
|
||||
_payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": user_prompt}, {"role":"system" , "content": "只返回一个结果'database'或'general'"}],
|
||||
"temperature": 0,
|
||||
"max_tokens": 10
|
||||
}
|
||||
try:
|
||||
class_response = requests.post(url, headers=headers, json=_payload)
|
||||
class_response.raise_for_status()
|
||||
class_result = class_response.json()
|
||||
question_type = class_result.get('choices', [{}])[0].get('message', {}).get('content', '').strip().lower()
|
||||
print("question_type", question_type)
|
||||
if question_type == "database":
|
||||
conn = connect_db()
|
||||
schema_text = get_schema_text(conn, TABLES)
|
||||
print("schema_text----------------------", schema_text)
|
||||
user_prompt = f"""你是一个专业的数据库工程师,根据以下数据库结构:
|
||||
{schema_text}
|
||||
请根据我的需求生成一条标准的PostgreSQL SQL语句,直接返回SQL,不要额外解释。
|
||||
需求是:{prompt}
|
||||
"""
|
||||
else:
|
||||
user_prompt = f"""
|
||||
回答以下问题,不需要涉及数据库查询:
|
||||
|
||||
问题: {prompt}
|
||||
|
||||
请直接回答问题,不要提及数据库或SQL。
|
||||
"""
|
||||
# TODO 是否应该拿到conservastion的id,然后根据id去数据库查询所以的messages, 然后赋值给messages
|
||||
history = Message.objects.filter(conversation=conversation).order_by('create_time')
|
||||
# chat_history = [{"role": msg.role, "content": msg.content} for msg in history]
|
||||
# chat_history.append({"role": "user", "content": prompt})
|
||||
chat_history = [{"role":"user", "content":prompt}]
|
||||
print("chat_history", chat_history)
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": chat_history,
|
||||
"temperature": 0,
|
||||
"stream": True
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
return JsonResponse({"error":f"LLM API调用失败: {e}"}, status=500)
|
||||
def stream_generator():
|
||||
accumulated_content = ""
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
decoded_line = line.decode('utf-8')
|
||||
if decoded_line.startswith('data:'):
|
||||
if decoded_line.strip() == "data: [DONE]":
|
||||
break # OpenAI-style标志结束
|
||||
try:
|
||||
data = json.loads(decoded_line[6:])
|
||||
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
|
||||
if content:
|
||||
accumulated_content += content
|
||||
yield f"data: {content}\n\n"
|
||||
|
||||
except Exception as e:
|
||||
yield f"data: [解析失败]: {str(e)}\n\n"
|
||||
print("accumulated_content", accumulated_content)
|
||||
save_message_thread_safe(content=accumulated_content, conversation=conversation, role="system")
|
||||
|
||||
if question_type == "database":
|
||||
sql = extract_sql_code(accumulated_content)
|
||||
if sql:
|
||||
try:
|
||||
conn = connect_db()
|
||||
if is_safe_sql(sql):
|
||||
result = execute_sql(conn, sql)
|
||||
save_message_thread_safe(content=f"SQL结果: {result}", conversation=conversation, role="system")
|
||||
yield f"data: SQL执行结果: {result}\n\n"
|
||||
else:
|
||||
yield f"data: 拒绝执行非查询类 SQL:{sql}\n\n"
|
||||
except Exception as e:
|
||||
yield f"data: SQL执行失败: {str(e)}\n\n"
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
else:
|
||||
yield "data: \\n[文本结束]\n\n"
|
||||
return StreamingHttpResponse(stream_generator(), content_type='text/event-stream')
|
||||
|
||||
|
||||
# 先新建对话 生成对话session_id
|
||||
class ConversationViewSet(CustomModelViewSet):
|
||||
queryset = Conversation.objects.all()
|
||||
serializer_class = ConversationSerializer
|
||||
ordering = ['create_time']
|
||||
perms_map = {'get':'*', 'post':'*', 'put':'*'}
|
||||
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import requests
|
||||
import os
|
||||
from apps.utils.sql import execute_raw_sql
|
||||
import json
|
||||
from apps.utils.tools import MyJSONEncoder
|
||||
from .utils import is_safe_sql
|
||||
from rest_framework.views import APIView
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.response import Response
|
||||
from django.conf import settings
|
||||
from apps.utils.mixins import MyLoggingMixin
|
||||
from django.core.cache import cache
|
||||
import uuid
|
||||
from apps.utils.thread import MyThread
|
||||
|
||||
LLM_URL = getattr(settings, "LLM_URL", "")
|
||||
API_KEY = getattr(settings, "LLM_API_KEY", "")
|
||||
MODEL = "qwen14b"
|
||||
HEADERS = {
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def load_promot(name):
|
||||
with open(os.path.join(CUR_DIR, f'promot/{name}.md'), 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def ask(input:str, p_name:str, stream=False):
|
||||
his = [{"role":"system", "content": load_promot(p_name)}]
|
||||
his.append({"role":"user", "content": input})
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": his,
|
||||
"temperature": 0,
|
||||
"stream": stream
|
||||
}
|
||||
response = requests.post(LLM_URL, headers=HEADERS, json=payload, stream=stream)
|
||||
if not stream:
|
||||
return response.json()["choices"][0]["message"]["content"]
|
||||
else:
|
||||
# 处理流式响应
|
||||
full_content = ""
|
||||
for chunk in response.iter_lines():
|
||||
if chunk:
|
||||
# 通常流式响应是SSE格式(data: {...})
|
||||
decoded_chunk = chunk.decode('utf-8')
|
||||
if decoded_chunk.startswith("data:"):
|
||||
json_str = decoded_chunk[5:].strip()
|
||||
if json_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk_data = json.loads(json_str)
|
||||
if "choices" in chunk_data and chunk_data["choices"]:
|
||||
delta = chunk_data["choices"][0].get("delta", {})
|
||||
if "content" in delta:
|
||||
print(delta["content"])
|
||||
full_content += delta["content"]
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return full_content
|
||||
|
||||
def work_chain(input:str, t_key:str):
|
||||
pdict = {"state": "progress", "steps": [{"state":"ok", "msg":"正在生成查询语句"}]}
|
||||
cache.set(t_key, pdict)
|
||||
res_text = ask(input, 'w_sql')
|
||||
if res_text == '请以 查询 开头,重新描述你的需求':
|
||||
pdict["state"] = "error"
|
||||
pdict["steps"].append({"state":"error", "msg":res_text})
|
||||
cache.set(t_key, pdict)
|
||||
return
|
||||
else:
|
||||
pdict["steps"].append({"state":"ok", "msg":"查询语句生成成功", "content":res_text})
|
||||
cache.set(t_key, pdict)
|
||||
if not is_safe_sql(res_text):
|
||||
pdict["state"] = "error"
|
||||
pdict["steps"].append({"state":"error", "msg":"当前查询存在风险,请重新描述你的需求"})
|
||||
cache.set(t_key, pdict)
|
||||
return
|
||||
pdict["steps"].append({"state":"ok", "msg":"正在执行查询语句"})
|
||||
cache.set(t_key, pdict)
|
||||
res = execute_raw_sql(res_text)
|
||||
pdict["steps"].append({"state":"ok", "msg":"查询语句执行成功", "content":res})
|
||||
cache.set(t_key, pdict)
|
||||
pdict["steps"].append({"state":"ok", "msg":"正在生成报告"})
|
||||
cache.set(t_key, pdict)
|
||||
res2 = ask(json.dumps(res, cls=MyJSONEncoder, ensure_ascii=False), 'w_ana')
|
||||
content = res2.lstrip('```html ').rstrip('```')
|
||||
pdict["state"] = "done"
|
||||
pdict["content"] = content
|
||||
pdict["steps"].append({"state":"ok", "msg":"报告生成成功", "content": content})
|
||||
cache.set(t_key, pdict)
|
||||
return
|
||||
|
||||
class InputSerializer(serializers.Serializer):
|
||||
input = serializers.CharField(label="查询需求")
|
||||
|
||||
class WorkChain(MyLoggingMixin, APIView):
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_summary="提交查询需求",
|
||||
request_body=InputSerializer)
|
||||
def post(self, request):
|
||||
llm_enabled = getattr(settings, "LLM_ENABLED", False)
|
||||
if not llm_enabled:
|
||||
raise ParseError('LLM功能未启用')
|
||||
input = request.data.get('input')
|
||||
t_key = f'ichat_{uuid.uuid4()}'
|
||||
MyThread(target=work_chain, args=(input, t_key)).start()
|
||||
return Response({'ichat_tid': t_key})
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_summary="获取查询进度")
|
||||
def get(self, request):
|
||||
llm_enabled = getattr(settings, "LLM_ENABLED", False)
|
||||
if not llm_enabled:
|
||||
raise ParseError('LLM功能未启用')
|
||||
ichat_tid = request.GET.get('ichat_tid')
|
||||
if ichat_tid:
|
||||
return Response(cache.get(ichat_tid))
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(work_chain("查询 一次超洗 工段在2025年6月的生产合格数等并形成报告"))
|
||||
|
||||
from apps.ichat.views2 import work_chain
|
||||
print(work_chain('查询外观检验工段在2025年6月的生产合格数等并形成报告'))
|
|
@ -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)
|
||||
|
|
|
@ -13,7 +13,8 @@ class MaterialBatchFilter(filters.FilterSet):
|
|||
"material__process": ["exact", "in"],
|
||||
"count": ["exact", "gte", "lte"],
|
||||
"state": ["exact", "in"],
|
||||
"defect": ["exact"]
|
||||
"defect": ["exact"],
|
||||
"batch": ["exact"]
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,7 +35,11 @@ class MioFilter(filters.FilterSet):
|
|||
"order": ["exact"],
|
||||
"item_mio__test_date": ["isnull"],
|
||||
"item_mio__test_user": ["isnull"],
|
||||
"item_mio__w_mioitem__number": ["exact"],
|
||||
"mgroup": ["exact"],
|
||||
"item_mio__batch": ["exact"],
|
||||
"inout_date": ["gte", "lte", "exact"],
|
||||
"belong_dept": ["exact"]
|
||||
}
|
||||
|
||||
def filter_materials__type(self, queryset, name, value):
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 3.2.12 on 2025-05-23 01:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inm', '0029_alter_mioitem_batch'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='materialbatch',
|
||||
name='batch',
|
||||
field=models.TextField(db_index=True, verbose_name='批次号'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='materialbatcha',
|
||||
name='batch',
|
||||
field=models.TextField(db_index=True, verbose_name='批次号'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mioitem',
|
||||
name='batch',
|
||||
field=models.TextField(db_index=True, verbose_name='批次号'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mioitema',
|
||||
name='batch',
|
||||
field=models.TextField(db_index=True, verbose_name='批次号'),
|
||||
),
|
||||
]
|
|
@ -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='单价'),
|
||||
),
|
||||
]
|
|
@ -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='出入库类型'),
|
||||
),
|
||||
]
|
|
@ -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='出入库类型'),
|
||||
),
|
||||
]
|
|
@ -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='对外编号'),
|
||||
),
|
||||
]
|
|
@ -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='关联装箱单'),
|
||||
),
|
||||
]
|
|
@ -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='装箱序号'),
|
||||
),
|
||||
]
|
|
@ -5,13 +5,13 @@ from apps.sam.models import Customer, Order
|
|||
from apps.mtm.models import Material, Mgroup
|
||||
from apps.system.models import User
|
||||
from datetime import datetime
|
||||
from django.db.models import Max
|
||||
from django.db.models import Max, Sum
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class WareHouse(CommonBModel):
|
||||
"""
|
||||
仓库信息
|
||||
TN:仓库信息
|
||||
"""
|
||||
number = models.CharField('仓库编号', max_length=20)
|
||||
name = models.CharField('仓库名称', max_length=20)
|
||||
|
@ -20,9 +20,9 @@ class WareHouse(CommonBModel):
|
|||
|
||||
class MaterialBatch(BaseModel):
|
||||
"""
|
||||
物料批次
|
||||
TN:物料批次
|
||||
"""
|
||||
batch = models.TextField('批次号')
|
||||
batch = models.TextField('批次号', db_index=True)
|
||||
state = models.PositiveSmallIntegerField('状态', default=10, choices=((10, '合格'), (20, '不合格'), (30, '返修'), (40, '检验'), (50, '报废')))
|
||||
material = models.ForeignKey(
|
||||
Material, on_delete=models.CASCADE, verbose_name='物料')
|
||||
|
@ -39,11 +39,15 @@ class MaterialBatch(BaseModel):
|
|||
defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
|
||||
@property
|
||||
def count_mioing(self):
|
||||
return MIOItem.objects.filter(mb=self, mio__submit_time__isnull=True).aggregate(count=Sum('count'))['count'] or 0
|
||||
|
||||
class MaterialBatchA(BaseModel):
|
||||
"""
|
||||
组合件物料批次
|
||||
TN:组合件物料批次
|
||||
"""
|
||||
batch = models.CharField('批次号', max_length=100)
|
||||
batch = models.TextField('批次号', db_index=True)
|
||||
material = models.ForeignKey(
|
||||
Material, on_delete=models.CASCADE, verbose_name='物料')
|
||||
rate = models.PositiveIntegerField('比例', default=1)
|
||||
|
@ -52,30 +56,39 @@ class MaterialBatchA(BaseModel):
|
|||
|
||||
|
||||
MIO_TYPE_PREFIX = {
|
||||
'do_out': 'SCLL', # 生产领料 (Shēngchǎn Lǐngliào)
|
||||
'sale_out': 'XSFH', # 销售发货 (Xiāoshòu Fāhuò)
|
||||
'pur_in': 'CGRK', # 采购入库 (Cǎigòu Rùkù)
|
||||
'do_in': 'SCRK', # 生产入库 (Shēngchǎn Rùkù)
|
||||
'other_in': 'QTRK', # 其他入库 (Qítā Rùkù)
|
||||
'other_out': 'QTCK' # 其他出库 (Qítā Chūkù)
|
||||
'do_in': 'SCRK', # 生产入库
|
||||
'do_out': 'SCLL', # 生产领料
|
||||
'sale_out': 'XSFH', # 销售发货
|
||||
'pur_in': 'CGRK', # 采购入库
|
||||
'pur_out': 'CGTH', # 采购退货
|
||||
'borrow_out': 'LYCK', # 领用出库
|
||||
'return_in': 'THRK', # 退还入库
|
||||
'other_in': 'QTRK', # 其他入库
|
||||
'other_out': 'QTCK' # 其他出库
|
||||
}
|
||||
|
||||
class MIO(CommonBDModel):
|
||||
"""
|
||||
出入库记录
|
||||
TN:出入库记录
|
||||
"""
|
||||
MIO_TYPE_DO_OUT = 'do_out'
|
||||
MIO_TYPE_SALE_OUT = 'sale_out'
|
||||
MIO_TYPE_PUR_IN = 'pur_in'
|
||||
MIO_TYPE_PUR_OUT = 'pur_out'
|
||||
MIO_TYPE_DO_IN = 'do_in'
|
||||
MIO_TYPE_OTHER_IN = 'other_in'
|
||||
MIO_TYPE_OTHER_OUT = 'other_out'
|
||||
MIO_TYPE_BORROW_OUT = 'borrow_out'
|
||||
MIO_TYPE_RETURN_IN = 'return_in'
|
||||
|
||||
MIO_TYPES = (
|
||||
(MIO_TYPE_DO_OUT, '生产领料'),
|
||||
(MIO_TYPE_SALE_OUT, '销售发货'),
|
||||
(MIO_TYPE_PUR_IN, '采购入库'),
|
||||
(MIO_TYPE_PUR_OUT, '采购退货'),
|
||||
(MIO_TYPE_DO_IN, '生产入库'),
|
||||
(MIO_TYPE_BORROW_OUT, '领用出库'),
|
||||
(MIO_TYPE_RETURN_IN, '退还入库'),
|
||||
(MIO_TYPE_OTHER_IN, '其他入库'),
|
||||
(MIO_TYPE_OTHER_OUT, '其他出库')
|
||||
)
|
||||
|
@ -124,9 +137,16 @@ class MIO(CommonBDModel):
|
|||
last_number = 1
|
||||
return f"{prefix}-{today_str}-{last_number:04d}"
|
||||
|
||||
class Pack(BaseModel):
|
||||
"""
|
||||
TN:装箱单
|
||||
"""
|
||||
index = models.PositiveSmallIntegerField('序号', default=1)
|
||||
mio = models.ForeignKey(MIO, verbose_name='关联出入库记录', on_delete=models.CASCADE, related_name='pack_mio')
|
||||
|
||||
class MIOItem(BaseModel):
|
||||
"""
|
||||
出入库明细
|
||||
TN:出入库明细
|
||||
"""
|
||||
mio = models.ForeignKey(MIO, verbose_name='关联出入库',
|
||||
on_delete=models.CASCADE, related_name='item_mio')
|
||||
|
@ -138,7 +158,8 @@ class MIOItem(BaseModel):
|
|||
WareHouse, on_delete=models.CASCADE, verbose_name='仓库')
|
||||
material = models.ForeignKey(
|
||||
Material, verbose_name='物料', on_delete=models.CASCADE)
|
||||
batch = models.TextField('批次号')
|
||||
batch = models.TextField('批次号', db_index=True)
|
||||
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
|
||||
count = models.DecimalField('出入数量', max_digits=12, decimal_places=3)
|
||||
count_tested = models.PositiveIntegerField('已检数', null=True, blank=True)
|
||||
test_date = models.DateField('检验日期', null=True, blank=True)
|
||||
|
@ -167,6 +188,11 @@ class MIOItem(BaseModel):
|
|||
count_n_qt = models.PositiveIntegerField('其他', default=0)
|
||||
|
||||
is_testok = models.BooleanField('检验是否合格', null=True, blank=True)
|
||||
note = models.TextField('备注', null=True, blank=True)
|
||||
pack_index = models.PositiveSmallIntegerField('装箱序号', null=True, blank=True)
|
||||
|
||||
# 以下字段暂时不用
|
||||
pack = models.ForeignKey(Pack, verbose_name='关联装箱单', on_delete=models.SET_NULL, related_name='mioitem_pack', null=True, blank=True)
|
||||
|
||||
@classmethod
|
||||
def count_fields(cls):
|
||||
|
@ -182,11 +208,11 @@ class MIOItem(BaseModel):
|
|||
|
||||
class MIOItemA(BaseModel):
|
||||
"""
|
||||
组合件出入库明细
|
||||
TN:组合件出入库明细
|
||||
"""
|
||||
material = models.ForeignKey(
|
||||
Material, verbose_name='物料', on_delete=models.CASCADE)
|
||||
batch = models.CharField('批次号', max_length=50)
|
||||
batch = models.TextField('批次号', db_index=True)
|
||||
rate = models.PositiveIntegerField('比例', default=1)
|
||||
mioitem = models.ForeignKey(
|
||||
MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='a_mioitem')
|
||||
|
@ -198,9 +224,10 @@ class MIOItemA(BaseModel):
|
|||
|
||||
class MIOItemw(BaseModel):
|
||||
"""
|
||||
单件记录
|
||||
TN:单件记录
|
||||
"""
|
||||
number = models.TextField('编号')
|
||||
number_out = models.TextField('对外编号', null=True, blank=True)
|
||||
wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw'
|
||||
, null=True, blank=True)
|
||||
mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem')
|
||||
|
|
|
@ -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,12 +48,19 @@ 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):
|
||||
warehouse_name = serializers.CharField(
|
||||
|
@ -109,17 +126,19 @@ 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)
|
||||
if mio.type == MIO.MIO_TYPE_DO_IN:
|
||||
assemb = validated_data.pop('assemb', [])
|
||||
if mio.type == MIO.MIO_TYPE_DO_IN and not assemb:
|
||||
if not wm:
|
||||
raise ParseError('生产入库必须指定车间库存')
|
||||
elif mio.type == MIO.MIO_TYPE_DO_OUT:
|
||||
|
@ -134,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('出入库记录非创建中不可新增')
|
||||
|
@ -147,51 +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"]
|
||||
assemb = validated_data.pop('assemb', [])
|
||||
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":
|
||||
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,11 +252,29 @@ 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):
|
||||
belong_dept = serializers.PrimaryKeyRelatedField(
|
||||
label="车间", queryset=Dept.objects.all(), required=True)
|
||||
label="车间", queryset=Dept.objects.all(), required=False)
|
||||
do_user = serializers.PrimaryKeyRelatedField(
|
||||
label="执行人", queryset=User.objects.all(), required=True)
|
||||
note = serializers.CharField(
|
||||
|
@ -235,20 +283,27 @@ class MIODoSerializer(CustomModelSerializer):
|
|||
class Meta:
|
||||
model = MIO
|
||||
fields = ['id', 'number', 'note', 'do_user',
|
||||
'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user']
|
||||
'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user', 'type']
|
||||
extra_kwargs = {'inout_date': {'required': True},
|
||||
'do_user': {'required': True}, 'belong_dept': {'required': False}}
|
||||
read_only_fields = ["number"]
|
||||
'do_user': {'required': True},
|
||||
'number': {"required": False, "allow_blank": True},
|
||||
'type': {'required': True}}
|
||||
|
||||
def validate(self, attrs):
|
||||
if 'mgroup' in attrs and attrs['mgroup']:
|
||||
attrs['belong_dept'] = attrs['mgroup'].belong_dept
|
||||
if not attrs.get("belong_dept", None):
|
||||
raise ParseError('请选择车间或工段')
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
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]:
|
||||
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"])
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
@ -265,11 +320,13 @@ class MIOSaleSerializer(CustomModelSerializer):
|
|||
class Meta:
|
||||
model = MIO
|
||||
fields = ['id', 'number', 'note', 'order', 'inout_date', 'customer', 'mio_user']
|
||||
extra_kwargs = {'inout_date': {'required': True}}
|
||||
extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}}
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['type'] = MIO.MIO_TYPE_SALE_OUT
|
||||
order: Order = validated_data.get('order', None)
|
||||
if not validated_data.get("number", None):
|
||||
validated_data["number"] = MIO.get_a_number(validated_data["type"])
|
||||
if order:
|
||||
if order.state in [Order.ORDER_CREATE, Order.ORDER_DELIVERED]:
|
||||
raise ValidationError('销售订单状态错误')
|
||||
|
@ -291,13 +348,19 @@ class MIOPurSerializer(CustomModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = MIO
|
||||
fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user']
|
||||
extra_kwargs = {'inout_date': {'required': True}}
|
||||
read_only_fields = ["number"]
|
||||
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
|
||||
validated_data['number'] = MIO.get_a_number(validated_data["type"])
|
||||
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)
|
||||
if pu_order:
|
||||
if pu_order.state in [PuOrder.PUORDER_CREATE, PuOrder.PUORDER_DONE]:
|
||||
|
@ -320,11 +383,11 @@ class MIOOtherSerializer(CustomModelSerializer):
|
|||
model = MIO
|
||||
fields = ['id', 'number', 'note', 'supplier',
|
||||
'customer', 'type', 'inout_date', 'mio_user']
|
||||
extra_kwargs = {'inout_date': {'required': True}}
|
||||
read_only_fields = ["number"]
|
||||
extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}}
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['number'] = MIO.get_a_number(validated_data["type"])
|
||||
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_OTHER_OUT, MIO.MIO_TYPE_OTHER_IN]:
|
||||
raise ValidationError('出入库类型错误')
|
||||
return super().create(validated_data)
|
||||
|
@ -380,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")
|
|
@ -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)
|
||||
|
|
|
@ -1,31 +1,38 @@
|
|||
from apps.inm.models import (MIO, MIOItem,
|
||||
MaterialBatch, MaterialBatchA,
|
||||
MIOItemA, WareHouse, MIOItemw)
|
||||
MIOItemA, MIOItemw)
|
||||
from rest_framework.exceptions import ParseError
|
||||
from apps.mtm.models import Material, Process
|
||||
from apps.utils.tools import ranstr
|
||||
from apps.utils.thread import MyThread
|
||||
from apps.mtm.models import Material
|
||||
from apps.mtm.services_2 import cal_material_count
|
||||
from apps.wpm.models import WMaterial
|
||||
from apps.wpm.services_2 import get_alldata_with_batch_and_store
|
||||
from apps.wpm.models import WMaterial, BatchSt, BatchLog
|
||||
from apps.wpm.services_2 import ana_batch_thread
|
||||
from apps.wpmw.models import Wpr
|
||||
from apps.qm.models import Ftest, Defect
|
||||
from django.db.models import Count, Q
|
||||
|
||||
def do_out(item: MIOItem):
|
||||
def do_out(item: MIOItem, is_reverse: bool = False):
|
||||
"""
|
||||
生产领料到车间
|
||||
"""
|
||||
if item.mb and item.mb.defect is not None:
|
||||
raise ParseError("生产领料不支持不合格品")
|
||||
|
||||
from apps.inm.models import MaterialBatch
|
||||
mio:MIO = item.mio
|
||||
belong_dept = mio.belong_dept
|
||||
mgroup = mio.mgroup
|
||||
do_user = mio.do_user
|
||||
material:Material = item.material
|
||||
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
|
||||
return
|
||||
|
||||
# 获取defect
|
||||
defect:Defect = None
|
||||
if item.wm and item.mb:
|
||||
raise ParseError("车间和仓库库存不能同时存在")
|
||||
if item.wm:
|
||||
defect = item.wm.defect
|
||||
elif item.mb:
|
||||
defect = item.mb.defect
|
||||
|
||||
action_list = []
|
||||
mias = MIOItemA.objects.filter(mioitem=item)
|
||||
is_zhj = False # 是否组合件领料
|
||||
|
@ -35,16 +42,18 @@ def do_out(item: MIOItem):
|
|||
for i in range(len(mias_list)):
|
||||
material, batch, rate = mias_list[i]
|
||||
new_count = rate * item.count # 假设 item.count 存在
|
||||
action_list.append([material, batch, new_count])
|
||||
action_list.append([material, batch, new_count, None])
|
||||
else:
|
||||
action_list = [[item.material, item.batch, item.count]]
|
||||
action_list = [[item.material, item.batch, item.count, defect]]
|
||||
|
||||
if is_zhj:
|
||||
try:
|
||||
mb = MaterialBatch.objects.get(
|
||||
material=item.material,
|
||||
warehouse=item.warehouse,
|
||||
batch=item.batch
|
||||
batch=item.batch,
|
||||
state=WMaterial.WM_OK,
|
||||
defect=None
|
||||
)
|
||||
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
|
||||
raise ParseError(f"组合件批次错误!{e}")
|
||||
|
@ -58,11 +67,17 @@ def do_out(item: MIOItem):
|
|||
raise ParseError("组合件暂不支持追踪单件")
|
||||
|
||||
xbatches = []
|
||||
if is_zhj:
|
||||
xbatches = [item.batch]
|
||||
for al in action_list:
|
||||
xmaterial:Material = al[0]
|
||||
xbatch:str = al[1]
|
||||
xcount:str = al[2]
|
||||
defect:Defect = al[3]
|
||||
|
||||
xbatches.append(xbatch)
|
||||
if xcount <= 0:
|
||||
raise ParseError("存在非正数!")
|
||||
mb = None
|
||||
if not is_zhj:
|
||||
try:
|
||||
|
@ -70,43 +85,48 @@ def do_out(item: MIOItem):
|
|||
material=xmaterial,
|
||||
warehouse=item.warehouse,
|
||||
batch=xbatch,
|
||||
state=10,
|
||||
defect=None
|
||||
state=WMaterial.WM_OK,
|
||||
defect=defect
|
||||
)
|
||||
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
|
||||
raise ParseError(f"批次错误!{e}")
|
||||
mb.count = mb.count - xcount
|
||||
if mb.count < 0:
|
||||
raise ParseError("批次库存不足,操作失败")
|
||||
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
|
||||
else:
|
||||
mb.save()
|
||||
|
||||
|
||||
# 领到车间库存(或工段)
|
||||
wm, new_create = WMaterial.objects.get_or_create(batch=xbatch, material=xmaterial,
|
||||
belong_dept=belong_dept, mgroup=mgroup,
|
||||
state=WMaterial.WM_OK)
|
||||
if new_create:
|
||||
wm.create_by = do_user
|
||||
wm.batch_ofrom = mb.batch if mb else None
|
||||
wm.material_ofrom = mb.material if mb else None
|
||||
wm.count = wm.count + item.count
|
||||
wm.update_by = do_user
|
||||
wm.save()
|
||||
if material.into_wm:
|
||||
# 领到车间库存(或工段)
|
||||
wm, new_create = WMaterial.objects.get_or_create(
|
||||
batch=xbatch, material=xmaterial,
|
||||
belong_dept=belong_dept, mgroup=mgroup,
|
||||
state=WMaterial.WM_OK, defect=defect)
|
||||
if new_create:
|
||||
wm.create_by = do_user
|
||||
wm.batch_ofrom = mb.batch if mb else None
|
||||
wm.material_ofrom = mb.material if mb else None
|
||||
wm.count = wm.count + item.count
|
||||
wm.update_by = do_user
|
||||
wm.save()
|
||||
|
||||
# 开始变动wpr
|
||||
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
|
||||
if material.into_wm is False:
|
||||
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
|
||||
mioitemws = MIOItemw.objects.filter(mioitem=item)
|
||||
if mioitemws.count() != item.count:
|
||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
||||
mb_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("mb__id", flat=True).distinct())
|
||||
if len(mb_ids) == 1 and mb_ids[0] == mb.id:
|
||||
pass
|
||||
else:
|
||||
raise ParseError(f'{xbatch}物料明细中存在{len(mb_ids)}个不同物料批次')
|
||||
for mioitemw in mioitemws:
|
||||
Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)
|
||||
|
||||
# 触发批次统计分析
|
||||
xbatches = list(set(xbatches))
|
||||
if xbatches:
|
||||
for xbatch in xbatches:
|
||||
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
|
||||
ana_batch_thread(xbatches)
|
||||
|
||||
|
||||
def do_in(item: MIOItem):
|
||||
|
@ -114,70 +134,89 @@ def do_in(item: MIOItem):
|
|||
生产入库后更新车间物料
|
||||
"""
|
||||
mio = item.mio
|
||||
if item.wm and item.wm.defect is not None:
|
||||
raise ParseError("不合格物料无法入库")
|
||||
wmin:WMaterial = item.wm
|
||||
if wmin and wmin.state != WMaterial.WM_OK:
|
||||
raise ParseError("非合格物料无法入库")
|
||||
belong_dept = mio.belong_dept
|
||||
mgroup = mio.mgroup
|
||||
do_user = mio.do_user
|
||||
material = item.material
|
||||
if material.into_wm is False: # 根据配置不进行入车间库存的处理
|
||||
return
|
||||
|
||||
action_list = []
|
||||
mias = MIOItemA.objects.filter(mioitem=item)
|
||||
is_zhj = False # 是否组合件入仓库
|
||||
|
||||
# 获取defect
|
||||
defect:Defect = None
|
||||
if item.wm and item.mb:
|
||||
raise ParseError("车间和仓库库存不能同时存在")
|
||||
if item.wm:
|
||||
defect = item.wm.defect
|
||||
elif item.mb:
|
||||
defect = item.mb.defect
|
||||
|
||||
if mias.exists():
|
||||
is_zhj = True
|
||||
mias_list = mias.values_list('material', 'batch', 'rate')
|
||||
for i in mias_list:
|
||||
material, batch, rate = i
|
||||
new_count = rate * item.count # 假设 item.count 存在
|
||||
action_list.append([material, batch, new_count])
|
||||
action_list.append([material, batch, new_count, None])
|
||||
else:
|
||||
action_list = [[item.material, item.batch, item.count]]
|
||||
action_list = [[item.material, item.batch, item.count, defect]]
|
||||
|
||||
production_dept = None
|
||||
|
||||
xbatchs = []
|
||||
if is_zhj:
|
||||
xbatchs = [item.batch]
|
||||
for al in action_list:
|
||||
xmaterial, xbatch, xcount = al
|
||||
xbatchs.append(xbatch)
|
||||
# 扣减车间库存
|
||||
wm_qs = WMaterial.objects.filter(
|
||||
batch=xbatch,
|
||||
material=xmaterial,
|
||||
belong_dept=belong_dept,
|
||||
mgroup=mgroup,
|
||||
state=WMaterial.WM_OK)
|
||||
count_x = wm_qs.count()
|
||||
if count_x == 1:
|
||||
wm = wm_qs.first()
|
||||
elif count_x == 0:
|
||||
raise ParseError(
|
||||
f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
|
||||
else:
|
||||
raise ParseError(
|
||||
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
|
||||
new_count = wm.count - xcount
|
||||
if new_count >= 0:
|
||||
wm.count = new_count
|
||||
wm.update_by = do_user
|
||||
wm.save()
|
||||
else:
|
||||
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
|
||||
xmaterial, xbatch, xcount, defect = al
|
||||
if xcount <= 0:
|
||||
raise ParseError("存在非正数!")
|
||||
|
||||
xbatchs.append(xbatch)
|
||||
if material.into_wm:
|
||||
wm_qs = WMaterial.objects.filter(
|
||||
batch=xbatch,
|
||||
material=xmaterial,
|
||||
belong_dept=belong_dept,
|
||||
mgroup=mgroup,
|
||||
defect=defect,
|
||||
state=WMaterial.WM_OK)
|
||||
count_x = wm_qs.count()
|
||||
if count_x == 1:
|
||||
wm = wm_qs.first()
|
||||
elif count_x == 0:
|
||||
raise ParseError(
|
||||
f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
|
||||
else:
|
||||
raise ParseError(
|
||||
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
|
||||
|
||||
# 扣减车间库存
|
||||
new_count = wm.count - xcount
|
||||
if new_count >= 0:
|
||||
wm.count = new_count
|
||||
wm.update_by = do_user
|
||||
wm.save()
|
||||
else:
|
||||
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
|
||||
|
||||
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
|
||||
if production_dept is None:
|
||||
production_dept = wm_production_dept
|
||||
elif wm_production_dept and production_dept != wm_production_dept:
|
||||
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
|
||||
|
||||
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else None
|
||||
if production_dept is None:
|
||||
production_dept = wm_production_dept
|
||||
elif wm_production_dept and production_dept != wm_production_dept:
|
||||
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
|
||||
# 增加mb
|
||||
if not is_zhj:
|
||||
mb, _ = MaterialBatch.objects.get_or_create(
|
||||
material=xmaterial,
|
||||
warehouse=item.warehouse,
|
||||
batch=xbatch,
|
||||
state=10,
|
||||
defect=None,
|
||||
state=WMaterial.WM_OK,
|
||||
defect=defect,
|
||||
defaults={
|
||||
"count": 0,
|
||||
"batch_ofrom": wm.batch_ofrom,
|
||||
|
@ -185,14 +224,23 @@ def do_in(item: MIOItem):
|
|||
"production_dept": production_dept
|
||||
}
|
||||
)
|
||||
if mb.production_dept is None:
|
||||
mb.production_dept = production_dept
|
||||
mb.count = mb.count + xcount
|
||||
mb.save()
|
||||
|
||||
# 开始变动wpr
|
||||
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
|
||||
if material.into_wm is False:
|
||||
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
|
||||
mioitemws = MIOItemw.objects.filter(mioitem=item)
|
||||
if mioitemws.count() != item.count:
|
||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
||||
wm_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("wm__id", flat=True).distinct())
|
||||
if len(wm_ids) == 1 and wm_ids[0] == wm.id:
|
||||
pass
|
||||
else:
|
||||
raise ParseError(f'{xbatch}物料明细中存在{len(wm_ids)}个不同物料批次')
|
||||
for mioitemw in mioitemws:
|
||||
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)
|
||||
|
||||
|
@ -202,17 +250,21 @@ def do_in(item: MIOItem):
|
|||
material=item.material,
|
||||
warehouse=item.warehouse,
|
||||
batch=item.batch,
|
||||
defect=None,
|
||||
state=WMaterial.WM_OK,
|
||||
defaults={"count": 0, "production_dept": production_dept}
|
||||
)
|
||||
if not is_created:
|
||||
raise ParseError("该批次组合件已存在")
|
||||
if mb.production_dept is None:
|
||||
mb.production_dept = production_dept
|
||||
mb.count = mb.count + item.count
|
||||
mb.save()
|
||||
for mia in mias:
|
||||
MaterialBatchA.objects.create(mb=mb, material=mia.material, batch=mia.batch, rate=mia.rate)
|
||||
|
||||
# 批次统计分析
|
||||
xbatchs = list(set(xbatchs))
|
||||
for xbatch in xbatchs:
|
||||
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
|
||||
ana_batch_thread(xbatchs)
|
||||
|
||||
class InmService:
|
||||
|
||||
|
@ -222,26 +274,51 @@ class InmService:
|
|||
更新物料数量
|
||||
"""
|
||||
# 统计物料数量
|
||||
m_ids = MIOItem.objects.filter(mio=instance).values_list('material_id', flat=True)
|
||||
cal_material_count(m_ids)
|
||||
m_ids = list(MIOItem.objects.filter(mio=instance).values_list('material_id', flat=True))
|
||||
m_ids2 = list(MIOItemA.objects.filter(mioitem__mio=instance).values_list('material_id', flat=True))
|
||||
cal_material_count(m_ids+m_ids2)
|
||||
|
||||
@classmethod
|
||||
def update_inm(cls, instance: MIO, is_reverse: bool = False):
|
||||
"""
|
||||
更新库存, 支持反向操作
|
||||
"""
|
||||
in_or_out = 1
|
||||
if is_reverse:
|
||||
in_or_out = -1
|
||||
if not MIOItem.objects.filter(mio=instance).exists():
|
||||
raise ParseError("出入库记录缺失明细,无法操作")
|
||||
|
||||
if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单
|
||||
# 这里还需要对入厂检验进行处理
|
||||
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)
|
||||
from apps.pum.services import PumService
|
||||
cls.update_mb(instance, in_or_out)
|
||||
PumService.mio_purin(instance, is_reverse)
|
||||
if is_reverse:
|
||||
cls.update_mb(instance, -1)
|
||||
else:
|
||||
cls.update_mb(instance, 1)
|
||||
PumService.mio_pur(instance, is_reverse)
|
||||
elif instance.type == MIO.MIO_TYPE_PUR_OUT:
|
||||
from apps.pum.services import PumService
|
||||
if is_reverse:
|
||||
cls.update_mb(instance, 1)
|
||||
else:
|
||||
cls.update_mb(instance, -1)
|
||||
PumService.mio_pur(instance, is_reverse)
|
||||
elif instance.type == MIO.MIO_TYPE_OTHER_IN:
|
||||
cls.update_mb(instance, in_or_out)
|
||||
elif instance.type == MIO.MIO_TYPE_DO_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)
|
||||
if is_reverse:
|
||||
cls.update_mb(instance, -1)
|
||||
else:
|
||||
cls.update_mb(instance, 1)
|
||||
elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]:
|
||||
mioitems = MIOItem.objects.filter(mio=instance)
|
||||
if is_reverse:
|
||||
for item in mioitems:
|
||||
|
@ -249,20 +326,26 @@ class InmService:
|
|||
else:
|
||||
for item in mioitems:
|
||||
do_in(item)
|
||||
elif instance.type in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT]:
|
||||
mioitems = MIOItem.objects.filter(mio=instance)
|
||||
if is_reverse:
|
||||
for item in mioitems:
|
||||
do_in(item)
|
||||
else:
|
||||
for item in mioitems:
|
||||
do_out(item)
|
||||
elif instance.type == MIO.MIO_TYPE_SALE_OUT:
|
||||
from apps.sam.services import SamService
|
||||
cls.update_mb(instance, in_or_out)
|
||||
if is_reverse:
|
||||
cls.update_mb(instance, 1)
|
||||
else:
|
||||
cls.update_mb(instance, -1)
|
||||
SamService.mio_saleout(instance, is_reverse)
|
||||
elif instance.type == MIO.MIO_TYPE_OTHER_OUT:
|
||||
cls.update_mb(instance, in_or_out)
|
||||
elif instance.type == MIO.MIO_TYPE_DO_OUT:
|
||||
mioitems = MIOItem.objects.filter(mio=instance)
|
||||
if is_reverse:
|
||||
for item in mioitems:
|
||||
do_in(item)
|
||||
cls.update_mb(instance, 1)
|
||||
else:
|
||||
for item in mioitems:
|
||||
do_out(item)
|
||||
cls.update_mb(instance, -1)
|
||||
else:
|
||||
raise ParseError('不支持该出入库操作')
|
||||
|
||||
|
@ -288,6 +371,7 @@ class InmService:
|
|||
out = -1
|
||||
默认使用count字段做加减
|
||||
"""
|
||||
mio_type = i.mio.type
|
||||
material: Material = i.material
|
||||
warehouse = i.warehouse
|
||||
tracking = material.tracking
|
||||
|
@ -319,11 +403,14 @@ class InmService:
|
|||
if xcount > 0:
|
||||
defect = defects_map[defect_id]
|
||||
m_list.append((material, warehouse, i.batch, xcount, defect, i))
|
||||
|
||||
xbatchs = []
|
||||
for material, warehouse, batch, change_count, defect, mioitem in m_list:
|
||||
if change_count <= 0:
|
||||
continue
|
||||
xbatchs.append(batch)
|
||||
if change_count < 0:
|
||||
raise ParseError("存在负数!")
|
||||
state = WMaterial.WM_OK
|
||||
if defect:
|
||||
if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
|
||||
state = WMaterial.WM_NOTOK
|
||||
mb, _ = MaterialBatch.objects.get_or_create(
|
||||
material=material,
|
||||
|
@ -355,7 +442,7 @@ class InmService:
|
|||
elif in_or_out == -1:
|
||||
mb.count = mb.count - change_count
|
||||
if mb.count < 0:
|
||||
raise ParseError("批次库存不足,操作失败")
|
||||
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
|
||||
else:
|
||||
mb.save()
|
||||
if tracking == Material.MA_TRACKING_SINGLE:
|
||||
|
@ -366,65 +453,34 @@ class InmService:
|
|||
if mioitemws.count() != change_count:
|
||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
||||
for mioitemw in mioitemws:
|
||||
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb)
|
||||
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb, number_out=mioitemw.number_out)
|
||||
else:
|
||||
raise ParseError("不支持的操作")
|
||||
|
||||
# 批次统计分析
|
||||
ana_batch_thread(xbatchs)
|
||||
|
||||
def daoru_mb(path: str):
|
||||
"""
|
||||
导入物料批次(如没有物料自动创建)
|
||||
"""
|
||||
# 注释的是初次导入时做的数据矫正
|
||||
# objs1 = Material.objects.filter(specification__contains=' ')
|
||||
# for i in objs1:
|
||||
# i.specification = i.specification.replace(' ', '')
|
||||
# i.save()
|
||||
# objs2 = Material.objects.filter(specification__contains='×')
|
||||
# for i in objs2:
|
||||
# i.specification = i.specification.replace('×', '*')
|
||||
# i.save()
|
||||
# objs3 = (Material.objects.filter(
|
||||
# specification__contains='优级') | Material.objects.filter(specification__contains='一级')).exclude(specification__contains='|')
|
||||
# for i in objs3:
|
||||
# i.specification = i.specification.replace(
|
||||
# '优级', '|优级').replace('一级', '|一级')
|
||||
# i.save()
|
||||
type_dict = {"主要原料": 30, "半成品": 20, "成品": 10, "辅助材料": 40, "加工工具": 50, "辅助工装": 60, "办公用品": 70}
|
||||
from apps.utils.snowflake import idWorker
|
||||
from openpyxl import load_workbook
|
||||
|
||||
wb = load_workbook(path)
|
||||
process_l = Process.objects.all()
|
||||
process_d = {p.name: p for p in process_l}
|
||||
warehouse_l = WareHouse.objects.all()
|
||||
warehouse_d = {w.name: w for w in warehouse_l}
|
||||
for sheet in wb.worksheets:
|
||||
i = 3
|
||||
while sheet[f"a{i}"].value:
|
||||
try:
|
||||
type = type_dict[sheet[f"a{i}"].value.replace(" ", "")]
|
||||
name = sheet[f"b{i}"].value.replace(" ", "")
|
||||
specification = sheet[f"c{i}"].value.replace(" ", "")
|
||||
if sheet[f"d{i}"].value and sheet[f"d{i}"].value.replace(" ", ""):
|
||||
specification = specification + "|" + sheet[f"d{i}"].value.replace(" ", "")
|
||||
model = sheet[f"e{i}"].value.replace(" ", "")
|
||||
process = process_d[sheet[f"f{i}"].value.replace(" ", "")]
|
||||
batch = sheet[f"g{i}"].value.replace(" ", "")
|
||||
count = int(sheet[f"h{i}"].value)
|
||||
warehouse = warehouse_d[sheet[f"i{i}"].value.replace(" ", "")]
|
||||
except KeyError as e:
|
||||
raise ParseError(f"第{i}行数据有误:{str(e)}")
|
||||
material, _ = Material.objects.get_or_create(
|
||||
type=type,
|
||||
name=name,
|
||||
specification=specification,
|
||||
model=model,
|
||||
process=process,
|
||||
defaults={"type": type, "name": name, "specification": specification, "model": model, "process": process, "number": ranstr(6), "id": idWorker.get_id()},
|
||||
)
|
||||
MaterialBatch.objects.get_or_create(
|
||||
material=material, batch=batch, warehouse=warehouse, defaults={"material": material, "batch": batch, "warehouse": warehouse, "count": count, "id": idWorker.get_id()}
|
||||
)
|
||||
cal_material_count([material.id])
|
||||
i = i + 1
|
||||
@classmethod
|
||||
def revert_and_del(cls, mioitem: MIOItem):
|
||||
mio = mioitem.mio
|
||||
if mio.submit_time is None:
|
||||
raise ParseError("未提交的出入库明细不允许撤销")
|
||||
if mioitem.test_date is not None:
|
||||
raise ParseError("已检验的出入库明细不允许撤销")
|
||||
if mio.type == MIO.MIO_TYPE_PUR_IN:
|
||||
from apps.pum.services import PumService
|
||||
cls.update_mb_item(mioitem, -1)
|
||||
BatchLog.clear(mioitem=mioitem)
|
||||
PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem)
|
||||
mioitem.delete()
|
||||
elif mio.type == MIO.MIO_TYPE_OTHER_IN:
|
||||
cls.update_mb_item(mioitem, -1)
|
||||
BatchLog.clear(mioitem=mioitem)
|
||||
mioitem.delete()
|
||||
elif mio.type == MIO.MIO_TYPE_DO_OUT:
|
||||
do_in(mioitem)
|
||||
BatchLog.clear(mioitem=mioitem)
|
||||
mioitem.delete()
|
||||
else:
|
||||
raise ParseError("不支持该出入库单明细撤销")
|
|
@ -0,0 +1,161 @@
|
|||
from rest_framework.exceptions import ParseError
|
||||
from apps.mtm.models import Process, Material
|
||||
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw, MIO
|
||||
from apps.utils.tools import ranstr
|
||||
from apps.mtm.services_2 import cal_material_count
|
||||
|
||||
def daoru_mb(path: str):
|
||||
"""
|
||||
导入物料批次(如没有物料自动创建)
|
||||
"""
|
||||
# 注释的是初次导入时做的数据矫正
|
||||
# objs1 = Material.objects.filter(specification__contains=' ')
|
||||
# for i in objs1:
|
||||
# i.specification = i.specification.replace(' ', '')
|
||||
# i.save()
|
||||
# objs2 = Material.objects.filter(specification__contains='×')
|
||||
# for i in objs2:
|
||||
# i.specification = i.specification.replace('×', '*')
|
||||
# i.save()
|
||||
# objs3 = (Material.objects.filter(
|
||||
# specification__contains='优级') | Material.objects.filter(specification__contains='一级')).exclude(specification__contains='|')
|
||||
# for i in objs3:
|
||||
# i.specification = i.specification.replace(
|
||||
# '优级', '|优级').replace('一级', '|一级')
|
||||
# i.save()
|
||||
type_dict = {"主要原料": 30, "半成品": 20, "成品": 10, "辅助材料": 40, "加工工具": 50, "辅助工装": 60, "办公用品": 70}
|
||||
from apps.utils.snowflake import idWorker
|
||||
from openpyxl import load_workbook
|
||||
|
||||
wb = load_workbook(path)
|
||||
process_l = Process.objects.all()
|
||||
process_d = {p.name: p for p in process_l}
|
||||
warehouse_l = WareHouse.objects.all()
|
||||
warehouse_d = {w.name: w for w in warehouse_l}
|
||||
for sheet in wb.worksheets:
|
||||
i = 3
|
||||
while sheet[f"a{i}"].value:
|
||||
try:
|
||||
type = type_dict[sheet[f"a{i}"].value.replace(" ", "")]
|
||||
name = sheet[f"b{i}"].value.replace(" ", "")
|
||||
specification = sheet[f"c{i}"].value.replace(" ", "")
|
||||
if sheet[f"d{i}"].value and sheet[f"d{i}"].value.replace(" ", ""):
|
||||
specification = specification + "|" + sheet[f"d{i}"].value.replace(" ", "")
|
||||
model = sheet[f"e{i}"].value.replace(" ", "")
|
||||
process = process_d[sheet[f"f{i}"].value.replace(" ", "")]
|
||||
batch = sheet[f"g{i}"].value.replace(" ", "")
|
||||
count = int(sheet[f"h{i}"].value)
|
||||
warehouse = warehouse_d[sheet[f"i{i}"].value.replace(" ", "")]
|
||||
except KeyError as e:
|
||||
raise ParseError(f"第{i}行数据有误:{str(e)}")
|
||||
material, _ = Material.objects.get_or_create(
|
||||
type=type,
|
||||
name=name,
|
||||
specification=specification,
|
||||
model=model,
|
||||
process=process,
|
||||
defaults={"type": type, "name": name, "specification": specification, "model": model, "process": process, "number": ranstr(6), "id": idWorker.get_id()},
|
||||
)
|
||||
MaterialBatch.objects.get_or_create(
|
||||
material=material, batch=batch, warehouse=warehouse, defaults={"material": material, "batch": batch, "warehouse": warehouse, "count": count, "id": idWorker.get_id()}
|
||||
)
|
||||
cal_material_count([material.id])
|
||||
i = i + 1
|
||||
|
||||
def daoru_mioitem_test(path:str, mioitem:MIOItem):
|
||||
from apps.utils.snowflake import idWorker
|
||||
from openpyxl import load_workbook
|
||||
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
|
||||
|
||||
qct = Qct.get(mioitem.material, tag="inm", type="in")
|
||||
if qct is None:
|
||||
raise ParseError("未找到检验表")
|
||||
|
||||
t_name_list = ["配套序号", "棒编号", "棒最大外径/mm", "锥度/mm", "管编号", "管最大内径/mm", "配合间隙"]
|
||||
t_list = []
|
||||
for name in t_name_list:
|
||||
try:
|
||||
t_list.append(TestItem.objects.get(name=name))
|
||||
except TestItem.DoesNotExist:
|
||||
raise ParseError(f"未找到检验项:{name}")
|
||||
except TestItem.MultipleObjectsReturned:
|
||||
raise ParseError(f"检验项重复:{name}")
|
||||
|
||||
test_user = mioitem.mio.mio_user
|
||||
test_date = mioitem.mio.inout_date
|
||||
|
||||
wb = load_workbook(path, data_only=True)
|
||||
if "Sheet1" in wb.sheetnames: # 检查是否存在
|
||||
sheet = wb["Sheet1"] # 获取工作表
|
||||
else:
|
||||
raise ParseError("未找到Sheet1")
|
||||
|
||||
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
|
||||
|
||||
for ind, item in enumerate(mioitemws):
|
||||
ftest:Ftest = item.ftest
|
||||
if ftest is None:
|
||||
ftest = Ftest.objects.create(
|
||||
type="purin",
|
||||
test_numer=item.number,
|
||||
qct=qct,
|
||||
test_user=test_user,
|
||||
is_ok=True,
|
||||
test_date=test_date)
|
||||
item.ftest = ftest
|
||||
item.save()
|
||||
else:
|
||||
FtestItem.objects.filter(ftest=ftest).delete()
|
||||
FtestDefect.objects.filter(ftest=ftest).delete()
|
||||
ftest.is_ok = True
|
||||
ftest.defect_main = None
|
||||
ftest.save()
|
||||
|
||||
i = ind + 4
|
||||
if sheet[f"c{i}"].value:
|
||||
ftestitems = []
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[0], test_val_json=sheet[f"b{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[1], test_val_json=sheet[f"c{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[2], test_val_json=sheet[f"e{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[3], test_val_json=sheet[f"f{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[4], test_val_json=sheet[f"g{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[5], test_val_json=sheet[f"j{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
||||
FtestItem.objects.bulk_create(ftestitems)
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
def daoru_mioitems(path:str, mio:MIO):
|
||||
from apps.utils.snowflake import idWorker
|
||||
from openpyxl import load_workbook
|
||||
|
||||
wb = load_workbook(path, data_only=True)
|
||||
if "Sheet1" in wb.sheetnames: # 检查是否存在
|
||||
sheet = wb["Sheet1"] # 获取工作表
|
||||
else:
|
||||
raise ParseError("未找到Sheet1")
|
||||
|
||||
mioitems = []
|
||||
ind = 2
|
||||
while sheet[f"a{ind}"].value:
|
||||
batch = sheet[f"b{ind}"].value
|
||||
material_number = sheet[f"a{ind}"].value
|
||||
try:
|
||||
material = Material.objects.get(number=material_number)
|
||||
except Exception as e:
|
||||
raise ParseError(f"未找到物料:{material_number} {e}")
|
||||
if batch:
|
||||
pass
|
||||
else:
|
||||
batch = "无"
|
||||
count = sheet[f"c{ind}"].value
|
||||
warehouse_name = sheet[f"d{ind}"].value
|
||||
try:
|
||||
warehouse = WareHouse.objects.get(name=warehouse_name)
|
||||
except Exception as e:
|
||||
raise ParseError(f"未找到仓库:{warehouse_name} {e}")
|
||||
mioitems.append(MIOItem(mio=mio, warehouse=warehouse, material=material, batch=batch, count=count, id=idWorker.get_id()))
|
||||
ind = ind + 1
|
||||
|
||||
MIOItem.objects.bulk_create(mioitems)
|
|
@ -6,7 +6,7 @@ from apps.inm.views import (
|
|||
MioPurViewSet, MioOtherViewSet, MIOItemwViewSet)
|
||||
|
||||
API_BASE_URL = 'api/inm/'
|
||||
HTML_BASE_URL = 'inm/'
|
||||
HTML_BASE_URL = 'dhtml/inm/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('warehouse', WarehouseVIewSet, basename='warehouse')
|
||||
|
@ -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)),
|
||||
]
|
||||
|
|
|
@ -9,21 +9,25 @@ from django.utils import timezone
|
|||
from rest_framework.response import Response
|
||||
from django.db.models import Sum
|
||||
|
||||
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw
|
||||
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack
|
||||
from apps.inm.serializers import (
|
||||
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
|
||||
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
|
||||
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
|
||||
MIOItemwSerializer)
|
||||
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer)
|
||||
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
from apps.inm.services import InmService, daoru_mb
|
||||
from apps.inm.services import InmService
|
||||
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test, daoru_mioitems
|
||||
from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin,
|
||||
CustomListModelMixin)
|
||||
from apps.utils.permission import has_perm
|
||||
from .filters import MaterialBatchFilter, MioFilter
|
||||
from apps.qm.serializers import FtestProcessSerializer
|
||||
from apps.mtm.models import Material
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from django.db import connection
|
||||
|
||||
|
||||
# Create your views here.
|
||||
|
@ -144,9 +148,19 @@ class MIOViewSet(CustomModelViewSet):
|
|||
serializer_class = MIOListSerializer
|
||||
retrieve_serializer_class = MIODetailSerializer
|
||||
filterset_class = MioFilter
|
||||
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model']
|
||||
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model',
|
||||
'item_mio__a_mioitem__batch']
|
||||
data_filter = True
|
||||
|
||||
@classmethod
|
||||
def lock_and_check_can_update(cls, mio:MIO):
|
||||
if not connection.in_atomic_block:
|
||||
raise ParseError("请在事务中调用该方法")
|
||||
mio:MIO = MIO.objects.select_for_update().get(id=mio.id)
|
||||
if mio.submit_time is not None:
|
||||
raise ParseError("该记录已提交无法更改")
|
||||
return mio
|
||||
|
||||
def add_info_for_list(self, data):
|
||||
# 获取检验状态
|
||||
mio_dict = {}
|
||||
|
@ -167,7 +181,7 @@ class MIOViewSet(CustomModelViewSet):
|
|||
if self.action in ['create', 'update', 'partial_update']:
|
||||
type = self.request.data.get('type')
|
||||
user = self.request.user
|
||||
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT]:
|
||||
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]:
|
||||
if not has_perm(user, ['mio.do']):
|
||||
raise PermissionDenied
|
||||
return MIODoSerializer
|
||||
|
@ -179,7 +193,7 @@ class MIOViewSet(CustomModelViewSet):
|
|||
if not has_perm(user, ['mio.sale']):
|
||||
raise PermissionDenied
|
||||
return MIOSaleSerializer
|
||||
elif type == MIO.MIO_TYPE_PUR_IN:
|
||||
elif type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]:
|
||||
if not has_perm(user, ['mio.pur']):
|
||||
raise PermissionDenied
|
||||
return MIOPurSerializer
|
||||
|
@ -191,27 +205,29 @@ class MIOViewSet(CustomModelViewSet):
|
|||
return super().perform_destroy(instance)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
|
||||
@transaction.atomic
|
||||
def submit(self, request, *args, **kwargs):
|
||||
"""提交
|
||||
|
||||
提交
|
||||
"""
|
||||
ins = self.get_object()
|
||||
ins:MIO = self.get_object()
|
||||
if ins.inout_date is None:
|
||||
raise ParseError('出入库日期未填写')
|
||||
if ins.state != MIO.MIO_CREATE:
|
||||
raise ParseError('记录状态异常')
|
||||
with transaction.atomic():
|
||||
ins.submit_time = timezone.now()
|
||||
ins.state = MIO.MIO_SUBMITED
|
||||
ins.submit_user = request.user
|
||||
ins.update_by = request.user
|
||||
ins.save()
|
||||
InmService.update_inm(ins)
|
||||
now = timezone.now()
|
||||
ins.submit_user = request.user
|
||||
ins.submit_time = now
|
||||
ins.update_by = request.user
|
||||
ins.state = MIO.MIO_SUBMITED
|
||||
ins.save()
|
||||
InmService.update_inm(ins)
|
||||
InmService.update_material_count(ins)
|
||||
return Response(MIOListSerializer(instance=ins).data)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
|
||||
@transaction.atomic
|
||||
def revert(self, request, *args, **kwargs):
|
||||
"""撤回
|
||||
|
||||
|
@ -223,16 +239,84 @@ class MIOViewSet(CustomModelViewSet):
|
|||
raise ParseError('记录状态异常')
|
||||
if ins.submit_user != user:
|
||||
raise ParseError('非提交人不可撤回')
|
||||
with transaction.atomic():
|
||||
ins.submit_time = None
|
||||
ins.state = MIO.MIO_CREATE
|
||||
ins.update_by = user
|
||||
ins.save()
|
||||
InmService.update_inm(ins, is_reverse=True)
|
||||
ins.submit_user = None
|
||||
ins.update_by = user
|
||||
ins.state = MIO.MIO_CREATE
|
||||
ins.submit_time = None
|
||||
ins.save()
|
||||
InmService.update_inm(ins, is_reverse=True)
|
||||
InmService.update_material_count(ins)
|
||||
return Response()
|
||||
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
|
||||
@transaction.atomic
|
||||
def pack_mioitem(self, request, *args, **kwargs):
|
||||
"""装箱
|
||||
|
||||
装箱
|
||||
"""
|
||||
mio:MIO = self.get_object()
|
||||
if mio.submit_time is not None:
|
||||
raise ParseError('该出入库已提交不可装箱')
|
||||
sr = PackMioSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
pack_index = vdata["pack_index"]
|
||||
mioitems = vdata["mioitems"]
|
||||
if not mioitems:
|
||||
raise ParseError('未选择明细')
|
||||
for id in mioitems:
|
||||
mioitem = MIOItem.objects.get(id=id)
|
||||
if mioitem.mio != mio:
|
||||
raise ParseError('存在明细不属于该箱')
|
||||
mioitem.pack_index = pack_index
|
||||
mioitem.save(update_fields=['pack_index', 'update_time'])
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
|
||||
def daoru_mioitem(self, request, *args, **kwargs):
|
||||
"""导入明细
|
||||
|
||||
导入明细
|
||||
"""
|
||||
daoru_mioitems(settings.BASE_DIR + request.data.get('path', ''), mio=self.get_object())
|
||||
return Response()
|
||||
class PackViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
list: 装箱记录
|
||||
|
||||
装箱记录
|
||||
"""
|
||||
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
|
||||
queryset = Pack.objects.all()
|
||||
serializer_class = PackSerializer
|
||||
filterset_fields = ["mio"]
|
||||
ordering = ["mio", "index"]
|
||||
|
||||
@action(methods=['post'], detail=False, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
|
||||
@transaction.atomic
|
||||
def pack_mioitem(self, request, *args, **kwargs):
|
||||
"""装箱
|
||||
|
||||
装箱
|
||||
"""
|
||||
vdata = PackMioSerializer(data=request.data)
|
||||
packId = vdata["pack"]
|
||||
pack:Pack = Pack.objects.get(id=packId)
|
||||
mioitems = vdata["mioitems"]
|
||||
if not mioitems:
|
||||
raise ParseError('未选择明细')
|
||||
for id in mioitems:
|
||||
mioitem = MIOItem.objects.get(id=id)
|
||||
if mioitem.mio != pack.mio:
|
||||
raise ParseError('存在明细不属于该装箱记录')
|
||||
mioitem.pack = pack
|
||||
mioitem.save(update_fields=['pack', 'update_time'])
|
||||
return Response()
|
||||
|
||||
|
||||
|
||||
class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
list: 出入库明细
|
||||
|
@ -242,6 +326,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
|||
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
|
||||
queryset = MIOItem.objects.all()
|
||||
serializer_class = MIOItemSerializer
|
||||
retrieve_serializer_class = MioItemDetailSerializer
|
||||
create_serializer_class = MIOItemCreateSerializer
|
||||
select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
|
||||
filterset_fields = {
|
||||
|
@ -251,21 +336,58 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
|||
"mio__type": ["exact", "in"],
|
||||
"mio__inout_date": ["gte", "lte", "exact"],
|
||||
"material": ["exact"],
|
||||
"material__type": ["exact"],
|
||||
"test_date": ["isnull", "exact"]
|
||||
}
|
||||
ordering = ['create_time']
|
||||
ordering_fields = ['create_time', 'test_date']
|
||||
search_fields =['batch', 'a_mioitem__batch']
|
||||
|
||||
def add_info_for_list(self, data):
|
||||
|
||||
with_mio = self.request.query_params.get('with_mio', "no")
|
||||
if with_mio == "yes" and isinstance(data, list):
|
||||
mio_ids = [item['mio'] for item in data]
|
||||
mio_qs = MIO.objects.filter(id__in=mio_ids)
|
||||
mio_qs_= MIOListSerializer(mio_qs, many=True).data
|
||||
mio_dict = {mio['id']: mio for mio in mio_qs_}
|
||||
for item in data:
|
||||
mioId = item['mio']
|
||||
item['mio_'] = mio_dict[mioId]
|
||||
return data
|
||||
|
||||
@swagger_auto_schema(manual_parameters=[
|
||||
openapi.Parameter(name="with_mio", in_=openapi.IN_QUERY, description="是否返回出入库记录信息",
|
||||
type=openapi.TYPE_STRING, required=False),
|
||||
openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据",
|
||||
type=openapi.TYPE_STRING, required=False),
|
||||
openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)",
|
||||
type=openapi.TYPE_STRING, required=False)
|
||||
])
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio'])
|
||||
return super().perform_create(serializer)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if instance.mio.state != MIO.MIO_CREATE:
|
||||
raise ParseError('出入库记录非创建中不可删除')
|
||||
MIOViewSet.lock_and_check_can_update(instance.mio)
|
||||
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
|
||||
raise PermissionDenied('无权限删除')
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
|
||||
@transaction.atomic
|
||||
def revert_and_del(self, request, *args, **kwargs):
|
||||
"""撤回并删除
|
||||
|
||||
撤回并删除
|
||||
"""
|
||||
ins:MIOItem = self.get_object()
|
||||
InmService.revert_and_del(ins)
|
||||
return Response()
|
||||
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer)
|
||||
@transaction.atomic
|
||||
def test(self, request, *args, **kwargs):
|
||||
|
@ -286,6 +408,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
|||
sr.save()
|
||||
# 开始变动库存
|
||||
InmService.update_mb_item(ins, -1, 'count_notok')
|
||||
InmService.update_material_count(ins.mio)
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=serializers.Serializer)
|
||||
|
@ -303,6 +426,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
|||
pass
|
||||
ins.test_date = None
|
||||
ins.save()
|
||||
InmService.update_material_count(ins.mio)
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemPurInTestSerializer)
|
||||
|
@ -320,6 +444,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
|||
sr = MIOItemPurInTestSerializer(instance=ins, data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
sr.save()
|
||||
InmService.update_material_count(ins.mio)
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=MioItemAnaSerializer)
|
||||
|
@ -347,12 +472,22 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
|||
res[i] = 0
|
||||
return Response(res)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
|
||||
@transaction.atomic
|
||||
def test_daoru_bg(self, request, *args, **kwargs):
|
||||
"""导入棒管检验
|
||||
|
||||
导入棒管检验
|
||||
"""
|
||||
daoru_mioitem_test(path=settings.BASE_DIR + request.data.get('path', ''), mioitem=self.get_object())
|
||||
return Response()
|
||||
|
||||
|
||||
class MIOItemwViewSet(CustomModelViewSet):
|
||||
perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'}
|
||||
queryset = MIOItemw.objects.all()
|
||||
serializer_class = MIOItemwCreateUpdateSerializer
|
||||
filterset_fields = ['mioitem']
|
||||
filterset_fields = ['mioitem', 'wpr']
|
||||
ordering = ["number", "create_time"]
|
||||
ordering_fields = ["number", "create_time"]
|
||||
|
||||
|
@ -368,20 +503,20 @@ class MIOItemwViewSet(CustomModelViewSet):
|
|||
mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count()
|
||||
mioitem.save()
|
||||
|
||||
@transaction.atomic
|
||||
def perform_create(self, serializer):
|
||||
ins: MIOItemw = serializer.save()
|
||||
mioitem: MIOItem = ins.mioitem
|
||||
self.cal_mioitem_count(mioitem)
|
||||
MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio)
|
||||
ins:MIOItemw = serializer.save()
|
||||
self.cal_mioitem_count(ins.mioitem)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_update(self, serializer):
|
||||
mioitemw = serializer.save()
|
||||
self.cal_mioitem_count(mioitemw.mioitem)
|
||||
ins:MIOItemw = serializer.instance
|
||||
MIOViewSet.lock_and_check_can_update(ins.mioitem.mio)
|
||||
ins:MIOItemw = serializer.save()
|
||||
self.cal_mioitem_count(ins.mioitem)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_destroy(self, instance: MIOItemw):
|
||||
mioitem = instance.mioitem
|
||||
MIOViewSet.lock_and_check_can_update(mioitem.mio)
|
||||
ftest = instance.ftest
|
||||
instance.delete()
|
||||
if ftest:
|
||||
|
|
|
@ -5,6 +5,7 @@ from apps.utils.models import BaseModel
|
|||
|
||||
|
||||
class AuditLog(models.Model):
|
||||
"""TN:审计表"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
action = models.CharField('动作', max_length=20)
|
||||
model_name = models.CharField('模型名', max_length=20)
|
||||
|
@ -18,7 +19,7 @@ class AuditLog(models.Model):
|
|||
|
||||
|
||||
class DrfRequestLog(BaseModel):
|
||||
"""Logs Django rest framework API requests"""
|
||||
"""TN:Logs Django rest framework API requests"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
user = models.ForeignKey(
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.urls import path
|
|||
from .views import DrfRequestLogViewSet, ServerInfoView, LogView, LogDetailView, index, room, video, DbBackupView, AuditlogViewSet, CeleryInfoView, RedisInfoView
|
||||
|
||||
API_BASE_URL = 'api/monitor/'
|
||||
HTML_BASE_URL = 'monitor/'
|
||||
HTML_BASE_URL = 'dhtml/monitor/'
|
||||
urlpatterns = [
|
||||
path(HTML_BASE_URL, index),
|
||||
path(HTML_BASE_URL + 'index/', index),
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from apps.mtm.models import Goal, Material, Route
|
||||
from apps.mtm.models import Goal, Material, Route, RoutePack
|
||||
from django.db.models.expressions import F
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
class MaterialFilter(filters.FilterSet):
|
||||
tag = filters.CharFilter(method='filter_tag')
|
||||
tag = filters.CharFilter(method='filter_tag', label="low_inm:库存不足")
|
||||
|
||||
class Meta:
|
||||
model = Material
|
||||
|
@ -27,7 +28,7 @@ class MaterialFilter(filters.FilterSet):
|
|||
|
||||
def filter_tag(self, queryset, name, value):
|
||||
if value == 'low_inm':
|
||||
queryset = queryset.exclude(count_safe=None).filter(
|
||||
queryset = queryset.exclude(count_safe=None).exclude(count_safe__lte=0).filter(
|
||||
count__lte=F('count_safe'))
|
||||
return queryset
|
||||
|
||||
|
@ -45,6 +46,8 @@ class GoalFilter(filters.FilterSet):
|
|||
|
||||
|
||||
class RouteFilter(filters.FilterSet):
|
||||
nprocess_name = filters.CharFilter(method='filter_nprocess_name', label="nprocess_name")
|
||||
material_in_has = filters.CharFilter(method='filter_material_in_has', label="material_in_has ID")
|
||||
class Meta:
|
||||
model = Route
|
||||
fields = {
|
||||
|
@ -58,5 +61,18 @@ class RouteFilter(filters.FilterSet):
|
|||
"mgroup": ["exact", "in", "isnull"],
|
||||
"mgroup__name": ["exact", "contains"],
|
||||
"mgroup__belong_dept": ["exact"],
|
||||
"mgroup__belong_dept__name": ["exact", "contains"]
|
||||
"mgroup__belong_dept__name": ["exact", "contains"],
|
||||
"from_route": ["exact", "isnull"],
|
||||
}
|
||||
|
||||
def filter_nprocess_name(self, queryset, name, value):
|
||||
return queryset
|
||||
|
||||
|
||||
def filter_material_in_has(self, queryset, name, value):
|
||||
nprocess_name = self.data.get('nprocess_name', None)
|
||||
if nprocess_name:
|
||||
routepack_qs = queryset.filter(material_in__id=value, routepack__isnull=False, routepack__state=RoutePack.RP_S_CONFIRM).values_list('routepack', flat=True)
|
||||
qs = queryset.filter(routepack__in=routepack_qs, process__name=nprocess_name)
|
||||
return qs
|
||||
raise ParseError("nprocess_name is required")
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.12 on 2025-03-21 08:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mtm', '0053_mgroup_mtype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='process',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mtm.process', verbose_name='父工序'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.12 on 2025-03-27 04:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mtm', '0054_process_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='routepack',
|
||||
name='gjson',
|
||||
field=models.JSONField(blank=True, default=dict, verbose_name='子图结构'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='routepack',
|
||||
name='material',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='产品'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2025-04-21 02:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mtm', '0055_auto_20250327_1239'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='mgroup',
|
||||
name='batch_append_code',
|
||||
field=models.BooleanField(default=False, verbose_name='批号追加工段标识'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2025-04-28 06:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mtm', '0056_mgroup_batch_append_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='process',
|
||||
name='number_to_batch',
|
||||
field=models.BooleanField(default=False, verbose_name='个号转批号'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2025-05-16 07:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mtm', '0057_process_number_to_batch'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='process',
|
||||
name='wpr_number_rule',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='单个编号规则'),
|
||||
),
|
||||
]
|
|
@ -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='主库位号'),
|
||||
),
|
||||
]
|
|
@ -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='工艺参数'),
|
||||
),
|
||||
]
|
|
@ -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='图片'),
|
||||
),
|
||||
]
|
|
@ -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='上级路线'),
|
||||
),
|
||||
]
|
|
@ -2,10 +2,14 @@ from django.db import models
|
|||
from apps.system.models import CommonAModel, Dictionary, CommonBModel, CommonADModel, File, BaseModel
|
||||
from rest_framework.exceptions import ParseError
|
||||
from apps.utils.models import CommonBDModel
|
||||
from collections import defaultdict, deque
|
||||
from django.db.models import Q
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
class Process(CommonBModel):
|
||||
"""
|
||||
工序
|
||||
TN:工序
|
||||
"""
|
||||
PRO_PROD = 10
|
||||
PRO_TEST = 20
|
||||
|
@ -14,6 +18,7 @@ class Process(CommonBModel):
|
|||
PRO_NORMAL = 10
|
||||
PRO_DIV = 20
|
||||
PRO_MERGE = 30
|
||||
|
||||
name = models.CharField('工序名称', max_length=100)
|
||||
type = models.PositiveSmallIntegerField("工序类型", default=PRO_PROD, choices=((PRO_PROD, '生产工序'), (PRO_TEST, '检验工序')))
|
||||
mtype = models.PositiveSmallIntegerField("工序生产类型", default=PRO_NORMAL, choices=((PRO_NORMAL, '常规'), (PRO_DIV, '切分'), (PRO_MERGE, '合并')))
|
||||
|
@ -27,14 +32,28 @@ class Process(CommonBModel):
|
|||
batch_append_equip = models.BooleanField('批号追加设备', default=False)
|
||||
mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False)
|
||||
mstate_json = models.JSONField('中间状态', default=list, blank=True)
|
||||
parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True)
|
||||
number_to_batch = models.BooleanField('个号转批号', default=False)
|
||||
wpr_number_rule = models.TextField("单个编号规则", null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '工序'
|
||||
ordering = ['sort', 'create_time']
|
||||
|
||||
def get_canout_mat_ids(self):
|
||||
"""获取可产出的materialIds
|
||||
"""
|
||||
return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct())
|
||||
|
||||
def get_canin_mat_ids(self):
|
||||
"""获取可输入的materialIds
|
||||
"""
|
||||
return list(RouteMat.objects.filter(route__process=self).values_list("material__id", flat=True).distinct()) + \
|
||||
list(Route.objects.filter(process=self).values_list("material_in__id", flat=True).distinct())
|
||||
|
||||
# Create your models here.
|
||||
class Material(CommonAModel):
|
||||
"""TN:物料"""
|
||||
MA_TYPE_BASE = 0
|
||||
MA_TYPE_GOOD = 10
|
||||
MA_TYPE_HALFGOOD = 20
|
||||
|
@ -90,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 = '物料表'
|
||||
|
@ -100,7 +121,7 @@ class Material(CommonAModel):
|
|||
|
||||
|
||||
class Shift(CommonBModel):
|
||||
"""班次
|
||||
"""TN:班次
|
||||
"""
|
||||
name = models.CharField('名称', max_length=50)
|
||||
rule = models.CharField('所属规则', max_length=10, default='默认')
|
||||
|
@ -113,13 +134,13 @@ class Shift(CommonBModel):
|
|||
|
||||
class Srule(CommonBDModel):
|
||||
"""
|
||||
班组规则
|
||||
TN:班组规则
|
||||
"""
|
||||
rule = models.JSONField('排班规则', default=list, blank=True)
|
||||
|
||||
|
||||
class Team(CommonBModel):
|
||||
"""班组, belong_dept为所属车间
|
||||
"""TN:班组, belong_dept为所属车间
|
||||
"""
|
||||
name = models.CharField('名称', max_length=50)
|
||||
leader = models.ForeignKey(
|
||||
|
@ -127,7 +148,7 @@ class Team(CommonBModel):
|
|||
|
||||
|
||||
class Mgroup(CommonBModel):
|
||||
"""测点集
|
||||
"""TN:测点集
|
||||
"""
|
||||
M_SELF = 10
|
||||
M_OTHER = 20
|
||||
|
@ -147,6 +168,7 @@ class Mgroup(CommonBModel):
|
|||
'直接材料', default=list, blank=True, help_text='material的ID列表')
|
||||
test_materials = models.JSONField(
|
||||
'检测材料', default=list, blank=True, help_text='material的ID列表')
|
||||
batch_append_code = models.BooleanField('批号追加工段标识', default=False)
|
||||
sort = models.PositiveSmallIntegerField('排序', default=1)
|
||||
need_enm = models.BooleanField('是否进行能源监测', default=True)
|
||||
is_running = models.BooleanField('是否正常运行', default=False)
|
||||
|
@ -158,6 +180,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):
|
||||
team = models.ForeignKey(Team, verbose_name='关联班组',
|
||||
|
@ -171,7 +219,7 @@ class TeamMember(BaseModel):
|
|||
|
||||
|
||||
class Goal(CommonADModel):
|
||||
"""目标
|
||||
"""TN:目标
|
||||
"""
|
||||
mgroup = models.ForeignKey(
|
||||
Mgroup, verbose_name='关联工段', on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
@ -198,7 +246,7 @@ class Goal(CommonADModel):
|
|||
|
||||
class RoutePack(CommonADModel):
|
||||
"""
|
||||
加工工艺
|
||||
TN:加工工艺
|
||||
"""
|
||||
RP_S_CREATE = 10
|
||||
RP_S_AUDIT = 20
|
||||
|
@ -211,14 +259,143 @@ class RoutePack(CommonADModel):
|
|||
state = models.PositiveSmallIntegerField('状态', default=10, choices=RP_STATE)
|
||||
name = models.CharField('名称', max_length=100)
|
||||
material = models.ForeignKey(
|
||||
Material, verbose_name='产品', on_delete=models.CASCADE)
|
||||
Material, verbose_name='产品', on_delete=models.CASCADE, null=True, blank=True)
|
||||
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.SET_NULL, related_name='routepack_ticket', null=True, blank=True, db_constraint=False)
|
||||
document = models.ForeignKey("system.file", verbose_name='工艺文件', on_delete=models.SET_NULL, null=True, blank=True, db_constraint=False)
|
||||
gjson = models.JSONField('子图结构', default=dict, blank=True)
|
||||
|
||||
def validate_and_return_gjson(self, raise_exception=True):
|
||||
"""
|
||||
校验所有子图,返回结构化数据:
|
||||
{
|
||||
final_material_out_id1: {
|
||||
"routes": [1, 2, ...], # 子图关联的Route ID
|
||||
"meta": {
|
||||
"is_valid": True,
|
||||
"errors": [],
|
||||
"levels": {material_id: 层级深度}
|
||||
}
|
||||
},
|
||||
final_material_out_id2: { ... }
|
||||
}
|
||||
"""
|
||||
all_routes = Route.objects.filter(routepack=self).order_by('sort', 'process__sort', 'create_time')
|
||||
result = {}
|
||||
|
||||
# 1. 构建正向和反向图(material_id作为节点)
|
||||
forward_graph = defaultdict(list) # out_id -> [in_ids]
|
||||
reverse_graph = defaultdict(list) # in_id -> [out_ids]
|
||||
route_edges = defaultdict(list) # (in_id, out_id) -> [Route]
|
||||
|
||||
for route in all_routes:
|
||||
if not (route.material_in and route.material_out):
|
||||
raise ParseError(f"Route(ID:{route.id}) 缺失material_in或material_out")
|
||||
in_id, out_id = route.material_in.id, route.material_out.id
|
||||
forward_graph[out_id].append(in_id)
|
||||
reverse_graph[in_id].append(out_id)
|
||||
route_edges[(in_id, out_id)].append(route.id)
|
||||
|
||||
# 2. 识别所有最终节点(未被任何in_id引用的out_id)
|
||||
all_out_ids = {r.material_out.id for r in all_routes}
|
||||
all_in_ids = {r.material_in.id for r in all_routes}
|
||||
final_ids = all_out_ids - all_in_ids
|
||||
|
||||
if not final_ids:
|
||||
raise ParseError("未找到最终产品节点")
|
||||
|
||||
# 3. 为每个最终产品构建子图
|
||||
for final_id in final_ids:
|
||||
# BFS逆向遍历所有上游节点
|
||||
visited = set()
|
||||
queue = deque([final_id])
|
||||
|
||||
while queue:
|
||||
current_out_id = queue.popleft()
|
||||
if current_out_id in visited:
|
||||
continue
|
||||
visited.add(current_out_id)
|
||||
|
||||
# 将当前节点的所有上游in_id加入队列
|
||||
for in_id in forward_graph.get(current_out_id, []):
|
||||
if in_id not in visited:
|
||||
queue.append(in_id)
|
||||
|
||||
# 收集所有关联Route(只要out_id在子图内)
|
||||
subgraph_route_ids = []
|
||||
for route in all_routes:
|
||||
if route.material_out.id in visited:
|
||||
subgraph_route_ids.append(route.id)
|
||||
|
||||
result[final_id] = {"routes": subgraph_route_ids}
|
||||
return result
|
||||
|
||||
def get_gjson(self, need_update=False, final_material_id=None):
|
||||
if not self.gjson or need_update:
|
||||
self.gjson = self.validate_and_return_gjson()
|
||||
self.save()
|
||||
return self.gjson.get(final_material_id, {}) if final_material_id else self.gjson
|
||||
|
||||
def get_dags(self):
|
||||
gjson = self.get_gjson()
|
||||
materialIdList = []
|
||||
routeIdList = []
|
||||
for final_material_id, data in gjson.items():
|
||||
materialIdList.append(final_material_id)
|
||||
routeIdList.extend(data['routes'])
|
||||
|
||||
# 获取所有相关路由,并确保关联了material_in和material_out
|
||||
route_qs = Route.objects.filter(id__in=routeIdList).select_related("material_in", "material_out", "process")
|
||||
|
||||
# 收集所有相关物料ID,并过滤掉None值
|
||||
matids1 = [mid for mid in route_qs.values_list("material_in__id", flat=True) if mid is not None]
|
||||
matids2 = [mid for mid in route_qs.values_list("material_out__id", flat=True) if mid is not None]
|
||||
materialIdList.extend(matids1)
|
||||
materialIdList.extend(matids2)
|
||||
|
||||
# 构建路由字典,添加空值检查
|
||||
route_dict = {}
|
||||
for r in route_qs:
|
||||
if r.material_in and r.material_out: # 只有两个物料都存在时才添加
|
||||
route_dict[r.id] = {
|
||||
"label": r.process.name if r.process else "",
|
||||
"source": r.material_in.id,
|
||||
"target": r.material_out.id,
|
||||
"id": r.id
|
||||
}
|
||||
|
||||
# 获取所有物料信息
|
||||
mat_qs = Material.objects.filter(id__in=materialIdList).order_by("process__sort", "create_time")
|
||||
mat_dict = {mat.id: {"id": mat.id, "label": str(mat), "shape": "rect"} for mat in mat_qs}
|
||||
|
||||
res = {}
|
||||
for final_material_id, data in gjson.items():
|
||||
# 确保最终物料存在于mat_dict中
|
||||
if final_material_id not in mat_dict:
|
||||
continue
|
||||
|
||||
item = {"name": mat_dict[final_material_id]["label"]}
|
||||
edges = []
|
||||
nodes_set = set()
|
||||
|
||||
for route_id in data['routes']:
|
||||
# 只处理存在于route_dict中的路由
|
||||
if route_id in route_dict:
|
||||
edges.append(route_dict[route_id])
|
||||
nodes_set.update([route_dict[route_id]['source'], route_dict[route_id]['target']])
|
||||
|
||||
item['edges'] = edges
|
||||
# 只添加存在于mat_dict中的节点
|
||||
item['nodes'] = [mat_dict[node_id] for node_id in nodes_set if node_id in mat_dict]
|
||||
res[final_material_id] = item
|
||||
|
||||
return res
|
||||
|
||||
def get_final_material_ids(self):
|
||||
return list(self.get_gjson().keys())
|
||||
class Route(CommonADModel):
|
||||
"""
|
||||
加工路线
|
||||
TN:加工路线
|
||||
"""
|
||||
routepack = models.ForeignKey(RoutePack, verbose_name='关联路线包', on_delete=models.CASCADE, null=True, blank=True)
|
||||
material = models.ForeignKey(
|
||||
|
@ -240,25 +417,152 @@ class Route(CommonADModel):
|
|||
batch_bind = models.BooleanField('是否绑定批次', default=True)
|
||||
materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials",
|
||||
through="mtm.routemat", blank=True)
|
||||
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True)
|
||||
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True, related_name="route_parent")
|
||||
params_json = models.JSONField('工艺参数', default=dict, blank=True)
|
||||
from_route = models.ForeignKey('self', verbose_name='来源路线', on_delete=models.SET_NULL, null=True, blank=True, related_name="route_f")
|
||||
|
||||
def __str__(self):
|
||||
x = ""
|
||||
if self.material_in:
|
||||
x = x + str(self.material_in) + "->"
|
||||
if self.material_out:
|
||||
x = x + str(self.material_out)
|
||||
return x
|
||||
|
||||
@staticmethod
|
||||
def get_routes(material: Material):
|
||||
def get_routes(material: Material=None, routepack:RoutePack=None, routeIds=None):
|
||||
"""
|
||||
返回工艺路线带车间(不关联工艺包)
|
||||
TN:返回工艺路线带车间(不关联工艺包)
|
||||
"""
|
||||
kwargs = {'material': material, 'routepack__isnull': True}
|
||||
# 校验工艺路线是否正常
|
||||
rq = Route.objects.filter(
|
||||
**kwargs).order_by('sort', 'process__sort', 'create_time')
|
||||
if not rq.exists():
|
||||
raise ParseError('未配置工艺路线')
|
||||
if rq.first().material_in is None or rq.last().material_out is None or rq.last().material_out != rq.last().material:
|
||||
raise ParseError('首步缺少输入/最后一步缺少输出')
|
||||
if not rq.filter(is_count_utask=True).exists():
|
||||
raise ParseError('未指定统计步骤')
|
||||
return rq
|
||||
if material:
|
||||
kwargs = {'material': material, 'routepack__isnull': True}
|
||||
rqs = Route.objects.filter(
|
||||
**kwargs).order_by('sort', 'process__sort', 'create_time')
|
||||
# if not rq.exists():
|
||||
# raise ParseError('未配置工艺路线')
|
||||
# if rq.first().material_in is None or rq.last().material_out is None or rq.last().material_out != rq.last().material:
|
||||
# raise ParseError('首步缺少输入/最后一步缺少输出')
|
||||
# if not rq.filter(is_count_utask=True).exists():
|
||||
# raise ParseError('未指定统计步骤')
|
||||
elif routepack:
|
||||
rqs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
|
||||
elif routeIds:
|
||||
rqs = Route.objects.filter(id__in=routeIds).order_by('sort', 'process__sort', 'create_time')
|
||||
return rqs
|
||||
|
||||
@classmethod
|
||||
def validate_dag(cls, final_material_out:Material, rqs):
|
||||
"""
|
||||
TN:校验工艺路线是否正确:
|
||||
- 所有 Route 必须有 material_in 和 material_out
|
||||
- 所有路径最终指向给定的 final_material_out
|
||||
- 无循环依赖
|
||||
- 所有物料必须能到达 final_material_out
|
||||
"""
|
||||
# 1. 检查字段完整性
|
||||
invalid_routes = rqs.filter(
|
||||
models.Q(material_in__isnull=True) |
|
||||
models.Q(material_out__isnull=True)
|
||||
)
|
||||
if invalid_routes.exists():
|
||||
raise ParseError("存在 Route 记录缺失 material_in 或 material_out")
|
||||
|
||||
# 2. 构建依赖图(使用material_id作为键)
|
||||
graph = defaultdict(list) # {material_out_id: [material_in_id]}
|
||||
reverse_graph = defaultdict(list) # {material_in_id: [material_out_id]}
|
||||
all_material_ids = set()
|
||||
|
||||
for route in rqs.all():
|
||||
out_id = route.material_out.id
|
||||
in_id = route.material_in.id
|
||||
|
||||
graph[out_id].append(in_id)
|
||||
reverse_graph[in_id].append(out_id)
|
||||
all_material_ids.update({in_id, out_id})
|
||||
|
||||
# 3. 检查final_material_out是否是终点
|
||||
final_id = final_material_out.id
|
||||
if final_id in reverse_graph:
|
||||
# raise ParseError(
|
||||
# f"最终物料 {final_material_out.name}(ID:{final_id}) 不能作为任何Route的输入"
|
||||
# )
|
||||
raise ParseError("最终物料不可作为输入")
|
||||
|
||||
# 4. BFS检查路径可达性(使用material_id操作)
|
||||
visited = set()
|
||||
queue = deque([final_id])
|
||||
|
||||
while queue:
|
||||
current_id = queue.popleft()
|
||||
if current_id in visited:
|
||||
continue
|
||||
visited.add(current_id)
|
||||
for in_id in graph.get(current_id, []):
|
||||
queue.append(in_id)
|
||||
|
||||
# 5. 检查未到达的物料
|
||||
unreachable_ids = all_material_ids - visited
|
||||
if unreachable_ids:
|
||||
# unreachable_materials = Material.objects.filter(id__in=unreachable_ids).values_list('name', flat=True)
|
||||
# raise ParseError(
|
||||
# f"以下物料无法到达最终物料: {list(unreachable_materials)}"
|
||||
# )
|
||||
raise ParseError("存在无法到达的节点")
|
||||
|
||||
# 6. DFS检查循环依赖
|
||||
visited_cycle = set()
|
||||
path_cycle = set()
|
||||
|
||||
def has_cycle(material_id):
|
||||
if material_id in path_cycle:
|
||||
return True
|
||||
if material_id in visited_cycle:
|
||||
return False
|
||||
visited_cycle.add(material_id)
|
||||
path_cycle.add(material_id)
|
||||
for out_id in reverse_graph.get(material_id, []):
|
||||
if has_cycle(out_id):
|
||||
return True
|
||||
path_cycle.remove(material_id)
|
||||
return False
|
||||
|
||||
for material_id in all_material_ids:
|
||||
if has_cycle(material_id):
|
||||
# cycle_material = Material.objects.get(id=material_id)
|
||||
# raise ParseError(f"循环依赖涉及物料: {cycle_material.name}(ID:{material_id})")
|
||||
raise ParseError('存在循环依赖')
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_dag(cls, rqs):
|
||||
# 收集所有相关批次和边
|
||||
nodes_set = set()
|
||||
edges = []
|
||||
|
||||
for rq in rqs:
|
||||
if rq.material_in and rq.material_out:
|
||||
source = rq.material_in.id
|
||||
target = rq.material_out.id
|
||||
nodes_set.update([source, target])
|
||||
edges.append({
|
||||
'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")
|
||||
|
||||
# 构建节点数据,默认使用'rect'形状
|
||||
nodes = [{
|
||||
'id': item.id,
|
||||
'label': str(item),
|
||||
'shape': 'rect' # 可根据业务需求调整形状
|
||||
} for item in nodes_qs]
|
||||
|
||||
return {'nodes': nodes, 'edges': edges}
|
||||
class RouteMat(BaseModel):
|
||||
"""TN:工艺路线辅助物料"""
|
||||
route = models.ForeignKey(Route, verbose_name='关联路线', on_delete=models.CASCADE, related_name="routemat_route")
|
||||
material = models.ForeignKey(Material, verbose_name='辅助物料', on_delete=models.CASCADE)
|
|
@ -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 ""}'
|
||||
|
@ -149,8 +149,14 @@ class RoutePackSerializer(CustomModelSerializer):
|
|||
# raise ParseError('不可变更产品')
|
||||
# return super().update(instance, validated_data)
|
||||
|
||||
class RoutePackCopySerializer(serializers.Serializer):
|
||||
routepack = serializers.CharField(label='工艺包ID')
|
||||
new_name = serializers.CharField(label='新名称')
|
||||
material_in = serializers.CharField(label='原料ID')
|
||||
material_out = serializers.CharField(label='产品ID')
|
||||
|
||||
class RouteSerializer(CustomModelSerializer):
|
||||
name = serializers.CharField(source='__str__', read_only=True)
|
||||
material_ = MaterialSerializer(source='material', read_only=True)
|
||||
routepack_name = serializers.StringRelatedField(source='routepack.name', read_only=True)
|
||||
process_name = serializers.CharField(source='process.name', read_only=True)
|
||||
|
@ -163,7 +169,7 @@ class RouteSerializer(CustomModelSerializer):
|
|||
source='material_out.type', read_only=True)
|
||||
material_out_is_hidden = serializers.BooleanField(
|
||||
source='material_out.is_hidden', read_only=True)
|
||||
material_out_tracking = serializers.IntegerField(source='material_out.tracking', required=False)
|
||||
material_out_tracking = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Route
|
||||
|
@ -178,11 +184,14 @@ class RouteSerializer(CustomModelSerializer):
|
|||
process: Process = attrs.get('process', None)
|
||||
if process is None:
|
||||
raise ParseError('未提供操作工序')
|
||||
if process.mtype == Process.PRO_DIV and attrs.get('div_number', 1) <= 1:
|
||||
raise ParseError('切分数量必须大于1')
|
||||
if process.parent is not None:
|
||||
raise ParseError('操作工序不可为子工序')
|
||||
if process.mtype == Process.PRO_DIV and attrs.get('div_number', 1) < 1:
|
||||
raise ParseError('切分数量必须大于等于1')
|
||||
return super().validate(attrs)
|
||||
|
||||
def gen_material_out(self, instance: Route, material_out_tracking:int):
|
||||
@classmethod
|
||||
def gen_material_out(cls, instance: Route, material_out_tracking:int, user=None):
|
||||
"""
|
||||
自动形成物料
|
||||
"""
|
||||
|
@ -198,20 +207,17 @@ class RouteSerializer(CustomModelSerializer):
|
|||
material_out.cate = material.cate
|
||||
material_out.tracking = material_out_tracking
|
||||
material_out.save()
|
||||
instance.material_out = material_out
|
||||
instance.save()
|
||||
return
|
||||
return material_out
|
||||
material_out = Material.objects.get_queryset(all=True).filter(name=material.name, model=material.model, process=process, specification=material.specification).first()
|
||||
if material_out:
|
||||
material_out.is_deleted = False
|
||||
if material_out.parent is None:
|
||||
material_out.parent = material
|
||||
if material_out.id != material.id:
|
||||
material_out.parent = material
|
||||
material_out.cate = material.cate
|
||||
material_out.tracking = material_out_tracking
|
||||
material_out.save()
|
||||
instance.material_out = material_out
|
||||
instance.save()
|
||||
return
|
||||
return material_out
|
||||
material_out = Material.objects.create(**{'parent': instance.material, 'process': instance.process,
|
||||
'is_hidden': True, 'name': material.name,
|
||||
'number': material.number,
|
||||
|
@ -220,16 +226,17 @@ class RouteSerializer(CustomModelSerializer):
|
|||
'type': Material.MA_TYPE_HALFGOOD,
|
||||
'cate': material.cate,
|
||||
'tracking': material_out_tracking,
|
||||
'create_by': self.request.user,
|
||||
'update_by': self.request.user,
|
||||
'create_by': user,
|
||||
'update_by': user,
|
||||
})
|
||||
instance.material_out = material_out
|
||||
instance.save()
|
||||
return
|
||||
return material_out
|
||||
|
||||
def create(self, validated_data):
|
||||
process = validated_data['process']
|
||||
routepack = validated_data.get('routepack', None)
|
||||
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
|
||||
if routepack:
|
||||
pass
|
||||
# if Route.objects.filter(routepack=routepack, process=process).exists():
|
||||
|
@ -239,41 +246,77 @@ 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 instance.material:
|
||||
material_out.parent = instance.material
|
||||
material_out.save()
|
||||
# elif material_out.process != process:
|
||||
# raise ParseError('物料工序错误!请重新选择')
|
||||
else:
|
||||
if instance.material:
|
||||
self.gen_material_out(instance, validated_data.get("material_out_tracking", Material.MA_TRACKING_BATCH))
|
||||
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)
|
||||
process = validated_data.pop('process', None)
|
||||
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 instance.material:
|
||||
material_out.parent = instance.material
|
||||
material_out.save()
|
||||
# elif material_out.process != process:
|
||||
# raise ParseError('物料工序错误!请重新选择')
|
||||
else:
|
||||
if instance.material:
|
||||
self.gen_material_out(instance, validated_data.get("material_out_tracking", Material.MA_TRACKING_BATCH))
|
||||
return instance
|
||||
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
|
||||
|
||||
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)
|
||||
if instance.material_out:
|
||||
res['material_out_tracking'] = instance.material_out.tracking
|
||||
else:
|
||||
res['material_out_tracking'] = None
|
||||
return res
|
||||
|
||||
class SruleSerializer(CustomModelSerializer):
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
|
@ -295,3 +338,15 @@ class RouteMatSerializer(CustomModelSerializer):
|
|||
model = RouteMat
|
||||
fields = "__all__"
|
||||
read_only_fields = EXCLUDE_FIELDS_BASE
|
||||
|
||||
def validate(self, attrs):
|
||||
route:Route = attrs["route"]
|
||||
if route.from_route is not None:
|
||||
raise ParseError("该工艺步骤引用其他步骤,无法修改")
|
||||
return attrs
|
||||
|
||||
|
||||
class MaterialExportSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Material
|
||||
fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"]
|
|
@ -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:
|
||||
|
@ -146,18 +148,19 @@ def mgroup_run_change(mgroup: Mgroup, new_run: bool, last_timex: datetime, note:
|
|||
|
||||
def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
|
||||
routepack = RoutePack.objects.get(id=new_ticket_data['t_id'])
|
||||
routepack.get_gjson(need_update=True)
|
||||
if routepack.ticket and routepack.ticket.id!=ticket.id:
|
||||
raise ParseError('重复创建工单')
|
||||
if not Route.objects.filter(routepack=routepack).exists():
|
||||
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',
|
||||
|
@ -168,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):
|
||||
|
@ -179,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()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from apps.mtm.views import (MgroupViewSet, ShiftViewSet, TeamViewSet, MaterialVi
|
|||
RoutePackViewSet, SruleViewSet, RouteMatViewSet)
|
||||
|
||||
API_BASE_URL = 'api/mtm/'
|
||||
HTML_BASE_URL = 'mtm/'
|
||||
HTML_BASE_URL = 'dhtml/mtm/'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('mgroup', MgroupViewSet, basename='mgroup')
|
||||
|
|
|
@ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer,
|
|||
MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer,
|
||||
ShiftSerializer, TeamSerializer, ProcessSerializer,
|
||||
RouteSerializer, TeamMemberSerializer, RoutePackSerializer,
|
||||
SruleSerializer, RouteMatSerializer)
|
||||
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
|
||||
|
@ -20,6 +20,9 @@ from django.db import transaction
|
|||
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):
|
||||
|
@ -31,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():
|
||||
|
@ -58,6 +60,18 @@ class MaterialViewSet(CustomModelViewSet):
|
|||
daoru_material(settings.BASE_DIR + request.data.get('path', ''))
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, serializer_class=Serializer, perms_map={'post': 'material.create'})
|
||||
@transaction.atomic
|
||||
def cal_count(self, request, *args, **kwargs):
|
||||
"""统计数量
|
||||
|
||||
统计数量
|
||||
"""
|
||||
ins = self.get_object()
|
||||
from apps.mtm.services_2 import cal_material_count
|
||||
cal_material_count([ins.id])
|
||||
return Response()
|
||||
|
||||
@action(methods=['put'], detail=True, serializer_class=Serializer, perms_map={'put': '*'})
|
||||
@transaction.atomic
|
||||
def set_week_esitimate_consume(self, request, *args, **kwargs):
|
||||
|
@ -76,6 +90,23 @@ class MaterialViewSet(CustomModelViewSet):
|
|||
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):
|
||||
"""
|
||||
list:班次
|
||||
|
@ -190,7 +221,10 @@ class ProcessViewSet(CustomModelViewSet):
|
|||
serializer_class = ProcessSerializer
|
||||
select_related_fields = ['belong_dept']
|
||||
search_fields = ['name', 'cate']
|
||||
filterset_fields = ['cate']
|
||||
filterset_fields = {
|
||||
"cate": ['exact'],
|
||||
"parent": ['isnull', "exact"]
|
||||
}
|
||||
ordering = ['sort', 'create_time']
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
|
@ -228,26 +262,43 @@ class RoutePackViewSet(CustomModelViewSet):
|
|||
return Response(status=204)
|
||||
|
||||
@transaction.atomic
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.create'}, serializer_class=Serializer)
|
||||
@action(methods=['post'], detail=False, perms_map={'post': 'routepack.create'}, serializer_class=RoutePackCopySerializer)
|
||||
def copy(self, request, *args, **kwargs):
|
||||
"""复制工艺路线
|
||||
|
||||
复制工艺路线
|
||||
"""
|
||||
obj = self.get_object()
|
||||
data = request.data
|
||||
sr = RoutePackCopySerializer(data=data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
user = request.user
|
||||
now = timezone.now()
|
||||
new_name = vdata["new_name"]
|
||||
rp = RoutePack.objects.get(id=vdata["routepack"])
|
||||
matin = Material.objects.get(id=vdata["material_in"])
|
||||
matout = Material.objects.get(id=vdata["material_out"])
|
||||
obj_c = RoutePack()
|
||||
obj_c.name = f'{obj.name}_copy'
|
||||
obj_c.material = obj.material
|
||||
obj_c.name = new_name
|
||||
obj_c.material = matout
|
||||
obj_c.create_by = user
|
||||
obj_c.create_time = now
|
||||
obj_c.save()
|
||||
for route in Route.objects.filter(routepack=obj):
|
||||
genM = {}
|
||||
for ind, route in enumerate(Route.get_routes(routepack=rp)):
|
||||
route_new = Route()
|
||||
process = route.process
|
||||
for f in Route._meta.fields:
|
||||
if f.name not in ['id', 'create_by', 'update_by', 'create_time', 'update_time']:
|
||||
if f.name in ['process', 'sort', 'is_autotask', 'is_count_utask', 'out_rate', 'div_number', 'hour_work', 'batch_bind']:
|
||||
setattr(route_new, f.name, getattr(route, f.name, None))
|
||||
route_new.material = matout
|
||||
# material_out = RouteSerializer.gen_material_out(instance=route_new, material_out_tracking=route.material_out.tracking)
|
||||
# route_new.material_out = material_out
|
||||
# if ind == 0:
|
||||
# route_new.material_in = matin
|
||||
# elif route.material_in.process and route.material_in.process.id in genM:
|
||||
# route_new.material_in = genM[route.material_in.process.id]
|
||||
# genM[process.id] = material_out
|
||||
route_new.routepack = obj_c
|
||||
route_new.create_by = user
|
||||
route_new.create_time = now
|
||||
|
@ -257,23 +308,81 @@ class RoutePackViewSet(CustomModelViewSet):
|
|||
rm_new.route = route_new
|
||||
rm_new.material = rm.material
|
||||
rm_new.save()
|
||||
return Response({"id": route_new.id})
|
||||
|
||||
@transaction.atomic
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer)
|
||||
def toggle_state(self, request, *args, **kwargs):
|
||||
"""变更工艺路线状态
|
||||
|
||||
变更工艺路线状态
|
||||
"""
|
||||
ins:RoutePack = self.get_object()
|
||||
if ins.state == RoutePack.RP_S_CONFIRM:
|
||||
ins.state = RoutePack.RP_S_CREATE
|
||||
elif ins.state == RoutePack.RP_S_CREATE:
|
||||
if ins.ticket is not None:
|
||||
ins.get_gjson(need_update=True)
|
||||
ins.state = RoutePack.RP_S_CONFIRM
|
||||
else:
|
||||
raise ParseError("该路线未提交审核")
|
||||
else:
|
||||
raise ParseError("该状态下不可变更")
|
||||
ins.save()
|
||||
return Response()
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||
def dag(self, request, *args, **kwargs):
|
||||
"""获取总图
|
||||
|
||||
获取总图
|
||||
"""
|
||||
return Response(Route.get_dag(rqs=Route.objects.filter(routepack=self.get_object())))
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||
def dags(self, request, *args, **kwargs):
|
||||
"""获取所有子图
|
||||
|
||||
获取所有子图
|
||||
"""
|
||||
ins = self.get_object()
|
||||
return Response(ins.get_dags())
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||
def final_materials(self, request, *args, **kwargs):
|
||||
"""获取最终产品
|
||||
|
||||
获取最终产品"""
|
||||
ins:RoutePack = self.get_object()
|
||||
matIds = ins.get_final_material_ids()
|
||||
qs = Material.objects.filter(id__in=matIds)
|
||||
res = [{"id": x.id, "name": str(x)} for x in qs]
|
||||
return Response(res)
|
||||
class RouteViewSet(CustomModelViewSet):
|
||||
queryset = Route.objects.all()
|
||||
serializer_class = RouteSerializer
|
||||
filterset_class = RouteFilter
|
||||
ordering = ['sort', 'process__sort', 'create_time']
|
||||
ordering_fields = ['sort', 'process__sort', 'create_time', 'update_time']
|
||||
select_related_fields = ['material',
|
||||
'process', 'material_in', 'material_out', 'mgroup', 'routepack']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
obj:Route = self.get_object()
|
||||
routepack = obj.routepack
|
||||
def perform_update(self, serializer):
|
||||
ins:Route = serializer.instance
|
||||
if ins.from_route is not None:
|
||||
raise ParseError('该工艺步骤引用其他步骤, 无法编辑')
|
||||
old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process
|
||||
routepack = ins.routepack
|
||||
if routepack and routepack.state != RoutePack.RP_S_CREATE:
|
||||
raise ParseError('该状态下不可编辑')
|
||||
return super().update(request, *args, **kwargs)
|
||||
raise ParseError('该工艺路线非创建中不可编辑')
|
||||
ins_n:Route = serializer.save()
|
||||
if Route.objects.filter(from_route__id=ins.id).exists() and (ins_n.material_in != old_m_in or ins_n.material_out != old_m_out or ins_n.process != process):
|
||||
raise ParseError("该工艺步骤被其他步骤引用, 无法修改关键信息")
|
||||
|
||||
def perform_destroy(self, instance:Route):
|
||||
if Route.objects.filter(from_route=instance).exists():
|
||||
raise ParseError('该工艺步骤被其他步骤引用,无法删除')
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
|
||||
class SruleViewSet(CustomModelViewSet):
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OfmConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.ofm'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue