Compare commits
474 Commits
2.6.202506
...
master
Author | SHA1 | Date |
---|---|---|
|
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 |
|
@ -20,6 +20,10 @@ class WxCodeSerializer(serializers.Serializer):
|
||||||
code = serializers.CharField(label="code")
|
code = serializers.CharField(label="code")
|
||||||
|
|
||||||
|
|
||||||
|
class UserIdSerializer(serializers.Serializer):
|
||||||
|
user_id = serializers.CharField(label="用户id")
|
||||||
|
|
||||||
|
|
||||||
class PwResetSerializer(serializers.Serializer):
|
class PwResetSerializer(serializers.Serializer):
|
||||||
phone = serializers.CharField(label="手机号")
|
phone = serializers.CharField(label="手机号")
|
||||||
code = serializers.CharField(label="验证码")
|
code = serializers.CharField(label="验证码")
|
||||||
|
|
|
@ -3,7 +3,8 @@ from django.urls import path
|
||||||
from rest_framework_simplejwt.views import TokenRefreshView
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
|
|
||||||
from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView,
|
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/'
|
API_BASE_URL = 'api/auth/'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -18,5 +19,6 @@ urlpatterns = [
|
||||||
path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'),
|
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 + 'logout/', LogoutView.as_view(), name='session_logout'),
|
||||||
path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'),
|
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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -23,7 +23,8 @@ from apps.auth1.serializers import FaceLoginSerializer
|
||||||
|
|
||||||
|
|
||||||
from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer,
|
from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer,
|
||||||
PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer)
|
PwResetSerializer, SecretLoginSerializer,
|
||||||
|
SendCodeSerializer, WxCodeSerializer, UserIdSerializer)
|
||||||
from apps.system.models import User
|
from apps.system.models import User
|
||||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||||
from apps.auth1.authentication import get_user_by_username_or
|
from apps.auth1.authentication import get_user_by_username_or
|
||||||
|
@ -234,6 +235,29 @@ class SecretLogin(CreateAPIView):
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
raise ParseError('登录失败')
|
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):
|
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)
|
test_param = models.JSONField('测试查询参数', default=dict, blank=True)
|
||||||
default_param = models.JSONField('默认查询参数', default=dict, blank=True)
|
default_param = models.JSONField('默认查询参数', default=dict, blank=True)
|
||||||
cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True)
|
cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True)
|
||||||
|
enabled = models.BooleanField('启用', default=True)
|
||||||
|
|
||||||
|
|
||||||
# class Report(CommonBDModel):
|
# class Report(CommonBDModel):
|
||||||
|
|
|
@ -12,9 +12,12 @@ def check_sql_safe(sql: str):
|
||||||
"""检查sql安全性
|
"""检查sql安全性
|
||||||
"""
|
"""
|
||||||
sql_upper = sql.upper()
|
sql_upper = sql.upper()
|
||||||
|
# 将SQL按空格和分号分割成单词
|
||||||
|
words = [word for word in sql_upper.replace(';', ' ').split() if word]
|
||||||
for kw in forbidden_keywords:
|
for kw in forbidden_keywords:
|
||||||
if kw in sql_upper:
|
# 检查关键字是否作为独立单词出现
|
||||||
raise ParseError('sql查询有风险')
|
if kw in words:
|
||||||
|
raise ParseError(f'sql查询有风险-{kw}')
|
||||||
return sql
|
return sql
|
||||||
|
|
||||||
def format_json_with_placeholders(json_str, **kwargs):
|
def format_json_with_placeholders(json_str, **kwargs):
|
||||||
|
|
|
@ -64,6 +64,8 @@ class DatasetViewSet(CustomModelViewSet):
|
||||||
执行sql查询支持code
|
执行sql查询支持code
|
||||||
"""
|
"""
|
||||||
dt: Dataset = self.get_object()
|
dt: Dataset = self.get_object()
|
||||||
|
if not dt.enabled:
|
||||||
|
raise ParseError(f'{dt.name}-该查询未启用')
|
||||||
rdata = DatasetSerializer(instance=dt).data
|
rdata = DatasetSerializer(instance=dt).data
|
||||||
xquery = request.data.get('query', {})
|
xquery = request.data.get('query', {})
|
||||||
is_test = request.data.get('is_test', False)
|
is_test = request.data.get('is_test', False)
|
||||||
|
|
|
@ -102,7 +102,7 @@ class LabelTemplateViewSet(CustomModelViewSet):
|
||||||
serializer_class = LabelTemplateSerializer
|
serializer_class = LabelTemplateSerializer
|
||||||
filterset_class = LabelTemplateFilter
|
filterset_class = LabelTemplateFilter
|
||||||
|
|
||||||
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer)
|
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"})
|
||||||
def commands(self, request, *args, **kwargs):
|
def commands(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
获取标签指令
|
获取标签指令
|
||||||
|
|
|
@ -4,9 +4,10 @@ import json
|
||||||
import time
|
import time
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from apps.utils.thread import MyThread
|
from apps.utils.thread import MyThread
|
||||||
import struct
|
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
import requests
|
||||||
|
|
||||||
myLogger = logging.getLogger('log')
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ def get_checksum(body_msg):
|
||||||
|
|
||||||
def handle_bytes(arr):
|
def handle_bytes(arr):
|
||||||
if len(arr) < 8:
|
if len(arr) < 8:
|
||||||
return "返回数据长度错误"
|
return f"返回数据长度错误-{arr}"
|
||||||
|
|
||||||
if arr[0] != 0xEB or arr[1] != 0x90:
|
if arr[0] != 0xEB or arr[1] != 0x90:
|
||||||
return "数据头不正确"
|
return "数据头不正确"
|
||||||
|
@ -133,8 +134,9 @@ def get_tyy_data_2(*args, retry=1):
|
||||||
|
|
||||||
|
|
||||||
sc_all = {}
|
sc_all = {}
|
||||||
|
sc_lock = threading.Lock()
|
||||||
|
|
||||||
def get_tyy_data(*args):
|
def get_tyy_data_1(*args, retry=1):
|
||||||
host, port = args[0], int(args[1])
|
host, port = args[0], int(args[1])
|
||||||
global sc_all
|
global sc_all
|
||||||
sc = None
|
sc = None
|
||||||
|
@ -148,6 +150,17 @@ def get_tyy_data(*args):
|
||||||
sc.settimeout(5) # 设置超时
|
sc.settimeout(5) # 设置超时
|
||||||
sc.connect((host, port))
|
sc.connect((host, port))
|
||||||
sc_all[f"{host}_{port}"] = sc
|
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")
|
sc.sendall(b"R")
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
if retry > 0:
|
if retry > 0:
|
||||||
|
@ -185,12 +198,55 @@ def get_tyy_data(*args):
|
||||||
sc_all.pop(f"{host}_{port}", None)
|
sc_all.pop(f"{host}_{port}", None)
|
||||||
sc = None
|
sc = None
|
||||||
raise ParseError(f"采集器连接超时-{str(e)}")
|
raise ParseError(f"采集器连接超时-{str(e)}")
|
||||||
|
|
||||||
|
with sc_lock:
|
||||||
|
connect_and_send()
|
||||||
|
resp = sc.recv(1024)
|
||||||
|
res = handle_bytes(resp)
|
||||||
|
# myLogger.error(res)
|
||||||
|
if isinstance(res, str):
|
||||||
|
raise ParseError(f'采集器返回数据错误-{res}')
|
||||||
|
else:
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def get_tyy_data_3(*args, retry=2):
|
||||||
|
host, port = args[0], int(args[1])
|
||||||
|
for attempt in range(retry):
|
||||||
|
try:
|
||||||
|
# 每次请求都新建连接(确保无共享状态)
|
||||||
|
with socket.create_connection((host, port), timeout=10) as sc:
|
||||||
|
sc.sendall(b"R")
|
||||||
|
|
||||||
|
# 接收完整响应(避免数据不完整)
|
||||||
|
# resp = b""
|
||||||
|
# while True:
|
||||||
|
# chunk = sc.recv(4096)
|
||||||
|
# if not chunk:
|
||||||
|
# break
|
||||||
|
# resp += chunk
|
||||||
|
resp = sc.recv(4096)
|
||||||
|
if not resp:
|
||||||
|
raise ParseError("设备未启动")
|
||||||
|
|
||||||
|
res = handle_bytes(resp)
|
||||||
|
if isinstance(res, str):
|
||||||
|
raise ParseError(f"采集器返回数据错误: {res}")
|
||||||
|
return res
|
||||||
|
except (socket.timeout, ConnectionError) as e:
|
||||||
|
if attempt == retry - 1: # 最后一次尝试失败才报错
|
||||||
|
raise ParseError(f"采集器连接失败: {str(e)}")
|
||||||
|
time.sleep(0.5) # 失败后等待 1s 再重试
|
||||||
|
except ParseError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise ParseError(f"未知错误: {str(e)}")
|
||||||
|
|
||||||
connect_and_send()
|
|
||||||
resp = sc.recv(1024)
|
def get_tyy_data(*args):
|
||||||
res = handle_bytes(resp)
|
host, port = args[0], int(args[1])
|
||||||
# myLogger.error(res)
|
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")
|
||||||
if isinstance(res, str):
|
res = r.json()
|
||||||
raise ParseError(f'采集器返回数据错误-{res}')
|
if "err_msg" in res:
|
||||||
else:
|
raise ParseError(res["err_msg"])
|
||||||
return res
|
return res
|
|
@ -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]
|
m = method.split("(")[0]
|
||||||
args = method.split("(")[1].split(")")[0].split(",")
|
args = method.split("(")[1].split(")")[0].split(",")
|
||||||
module, func = m.rsplit(".", 1)
|
module, func = m.rsplit(".", 1)
|
||||||
|
|
|
@ -62,7 +62,7 @@ class Mpoint(CommonBModel):
|
||||||
cal_coefficient = models.FloatField("计算系数", null=True, blank=True)
|
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)
|
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
|
@classmethod
|
||||||
def cache_key(cls, code: str):
|
def cache_key(cls, code: str):
|
||||||
|
|
|
@ -86,10 +86,10 @@ def db_ins_mplogx():
|
||||||
if bill_date is None:
|
if bill_date is None:
|
||||||
raise Exception("bill_date is None")
|
raise Exception("bill_date is None")
|
||||||
query = """
|
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
|
FROM sa_weigh_view
|
||||||
WHERE bill_date >= %s and de_real_quantity > 0
|
WHERE bill_date >= %s and de_real_quantity > 0
|
||||||
AND inv_name IN %s
|
AND inv_code IN %s
|
||||||
ORDER BY bill_date
|
ORDER BY bill_date
|
||||||
"""
|
"""
|
||||||
cursor.execute(query, (bill_date, tuple(batchs)))
|
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:
|
if st:
|
||||||
|
|
||||||
return st, "ending"
|
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:
|
if st:
|
||||||
|
|
||||||
return st, "start"
|
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:
|
if st:
|
||||||
|
|
||||||
return st, "end"
|
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)
|
val = abs(first_val - last_val)
|
||||||
else:
|
else:
|
||||||
xtype = "normal"
|
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)
|
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 django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from apps.enm.views import (MpointViewSet, MpLogxViewSet, MpointStatViewSet,
|
from apps.enm.views import (MpointViewSet, MpointStatViewSet,
|
||||||
EnStatViewSet, EnStat2ViewSet, XscriptViewSet)
|
EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView)
|
||||||
|
|
||||||
API_BASE_URL = 'api/enm/'
|
API_BASE_URL = 'api/enm/'
|
||||||
HTML_BASE_URL = 'dhtml/enm/'
|
HTML_BASE_URL = 'dhtml/enm/'
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register('mpoint', MpointViewSet, basename='mpoint')
|
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('mpointstat', MpointStatViewSet, basename='mpointstat')
|
||||||
router.register('enstat', EnStatViewSet, basename='enstat')
|
router.register('enstat', EnStatViewSet, basename='enstat')
|
||||||
router.register('enstat2', EnStat2ViewSet, basename='enstat2')
|
router.register('enstat2', EnStat2ViewSet, basename='enstat2')
|
||||||
router.register('xscript', XscriptViewSet, basename='xscript')
|
router.register('xscript', XscriptViewSet, basename='xscript')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
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.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.views import APIView
|
||||||
from apps.enm.tasks import cal_mpointstats_duration
|
from apps.enm.tasks import cal_mpointstats_duration
|
||||||
from apps.enm.services import king_sync, MpointCache
|
from apps.enm.services import king_sync, MpointCache
|
||||||
from django.db import transaction
|
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
|
from django.db.models import Sum
|
||||||
import logging
|
import logging
|
||||||
from django.core.cache import cache
|
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')
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
class MpointViewSet(CustomModelViewSet):
|
class MpointViewSet(CustomModelViewSet):
|
||||||
"""
|
"""
|
||||||
list:测点
|
list:测点
|
||||||
|
@ -84,6 +91,34 @@ class MpointViewSet(CustomModelViewSet):
|
||||||
king_sync(getattr(settings, "KING_PROJECTNAME", ""))
|
king_sync(getattr(settings, "KING_PROJECTNAME", ""))
|
||||||
return Response()
|
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):
|
class XscriptViewSet(CustomModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -138,6 +173,97 @@ class XscriptViewSet(CustomModelViewSet):
|
||||||
# select_related_fields = ['mpoint']
|
# select_related_fields = ['mpoint']
|
||||||
# filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept']
|
# 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):
|
class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
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:
|
try:
|
||||||
with transaction.atomic():
|
MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok)
|
||||||
MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok)
|
InmService.update_mb_after_test(mi)
|
||||||
InmService.update_mb_after_test(mi)
|
|
||||||
except ParseError as e:
|
except ParseError as e:
|
||||||
MIOItem.objects.filter(id=mi.id).update(test_date=None)
|
MIOItem.objects.filter(id=mi.id).update(test_date=None)
|
||||||
|
|
|
@ -37,7 +37,9 @@ class MioFilter(filters.FilterSet):
|
||||||
"item_mio__test_user": ["isnull"],
|
"item_mio__test_user": ["isnull"],
|
||||||
"item_mio__w_mioitem__number": ["exact"],
|
"item_mio__w_mioitem__number": ["exact"],
|
||||||
"mgroup": ["exact"],
|
"mgroup": ["exact"],
|
||||||
"item_mio__batch": ["exact"]
|
"item_mio__batch": ["exact"],
|
||||||
|
"inout_date": ["gte", "lte", "exact"],
|
||||||
|
"belong_dept": ["exact"]
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_materials__type(self, queryset, name, value):
|
def filter_materials__type(self, queryset, name, value):
|
||||||
|
|
|
@ -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,7 +5,7 @@ from apps.sam.models import Customer, Order
|
||||||
from apps.mtm.models import Material, Mgroup
|
from apps.mtm.models import Material, Mgroup
|
||||||
from apps.system.models import User
|
from apps.system.models import User
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.db.models import Max
|
from django.db.models import Max, Sum
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,6 +39,10 @@ class MaterialBatch(BaseModel):
|
||||||
defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True)
|
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):
|
class MaterialBatchA(BaseModel):
|
||||||
"""
|
"""
|
||||||
TN:组合件物料批次
|
TN:组合件物料批次
|
||||||
|
@ -52,12 +56,15 @@ class MaterialBatchA(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
MIO_TYPE_PREFIX = {
|
MIO_TYPE_PREFIX = {
|
||||||
'do_out': 'SCLL', # 生产领料 (Shēngchǎn Lǐngliào)
|
'do_in': 'SCRK', # 生产入库
|
||||||
'sale_out': 'XSFH', # 销售发货 (Xiāoshòu Fāhuò)
|
'do_out': 'SCLL', # 生产领料
|
||||||
'pur_in': 'CGRK', # 采购入库 (Cǎigòu Rùkù)
|
'sale_out': 'XSFH', # 销售发货
|
||||||
'do_in': 'SCRK', # 生产入库 (Shēngchǎn Rùkù)
|
'pur_in': 'CGRK', # 采购入库
|
||||||
'other_in': 'QTRK', # 其他入库 (Qítā Rùkù)
|
'pur_out': 'CGTH', # 采购退货
|
||||||
'other_out': 'QTCK' # 其他出库 (Qítā Chūkù)
|
'borrow_out': 'LYCK', # 领用出库
|
||||||
|
'return_in': 'THRK', # 退还入库
|
||||||
|
'other_in': 'QTRK', # 其他入库
|
||||||
|
'other_out': 'QTCK' # 其他出库
|
||||||
}
|
}
|
||||||
|
|
||||||
class MIO(CommonBDModel):
|
class MIO(CommonBDModel):
|
||||||
|
@ -67,15 +74,21 @@ class MIO(CommonBDModel):
|
||||||
MIO_TYPE_DO_OUT = 'do_out'
|
MIO_TYPE_DO_OUT = 'do_out'
|
||||||
MIO_TYPE_SALE_OUT = 'sale_out'
|
MIO_TYPE_SALE_OUT = 'sale_out'
|
||||||
MIO_TYPE_PUR_IN = 'pur_in'
|
MIO_TYPE_PUR_IN = 'pur_in'
|
||||||
|
MIO_TYPE_PUR_OUT = 'pur_out'
|
||||||
MIO_TYPE_DO_IN = 'do_in'
|
MIO_TYPE_DO_IN = 'do_in'
|
||||||
MIO_TYPE_OTHER_IN = 'other_in'
|
MIO_TYPE_OTHER_IN = 'other_in'
|
||||||
MIO_TYPE_OTHER_OUT = 'other_out'
|
MIO_TYPE_OTHER_OUT = 'other_out'
|
||||||
|
MIO_TYPE_BORROW_OUT = 'borrow_out'
|
||||||
|
MIO_TYPE_RETURN_IN = 'return_in'
|
||||||
|
|
||||||
MIO_TYPES = (
|
MIO_TYPES = (
|
||||||
(MIO_TYPE_DO_OUT, '生产领料'),
|
(MIO_TYPE_DO_OUT, '生产领料'),
|
||||||
(MIO_TYPE_SALE_OUT, '销售发货'),
|
(MIO_TYPE_SALE_OUT, '销售发货'),
|
||||||
(MIO_TYPE_PUR_IN, '采购入库'),
|
(MIO_TYPE_PUR_IN, '采购入库'),
|
||||||
|
(MIO_TYPE_PUR_OUT, '采购退货'),
|
||||||
(MIO_TYPE_DO_IN, '生产入库'),
|
(MIO_TYPE_DO_IN, '生产入库'),
|
||||||
|
(MIO_TYPE_BORROW_OUT, '领用出库'),
|
||||||
|
(MIO_TYPE_RETURN_IN, '退还入库'),
|
||||||
(MIO_TYPE_OTHER_IN, '其他入库'),
|
(MIO_TYPE_OTHER_IN, '其他入库'),
|
||||||
(MIO_TYPE_OTHER_OUT, '其他出库')
|
(MIO_TYPE_OTHER_OUT, '其他出库')
|
||||||
)
|
)
|
||||||
|
@ -124,6 +137,13 @@ class MIO(CommonBDModel):
|
||||||
last_number = 1
|
last_number = 1
|
||||||
return f"{prefix}-{today_str}-{last_number:04d}"
|
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):
|
class MIOItem(BaseModel):
|
||||||
"""
|
"""
|
||||||
TN:出入库明细
|
TN:出入库明细
|
||||||
|
@ -139,6 +159,7 @@ class MIOItem(BaseModel):
|
||||||
material = models.ForeignKey(
|
material = models.ForeignKey(
|
||||||
Material, verbose_name='物料', on_delete=models.CASCADE)
|
Material, verbose_name='物料', on_delete=models.CASCADE)
|
||||||
batch = models.TextField('批次号', db_index=True)
|
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 = models.DecimalField('出入数量', max_digits=12, decimal_places=3)
|
||||||
count_tested = models.PositiveIntegerField('已检数', null=True, blank=True)
|
count_tested = models.PositiveIntegerField('已检数', null=True, blank=True)
|
||||||
test_date = models.DateField('检验日期', 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)
|
count_n_qt = models.PositiveIntegerField('其他', default=0)
|
||||||
|
|
||||||
is_testok = models.BooleanField('检验是否合格', null=True, blank=True)
|
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
|
@classmethod
|
||||||
def count_fields(cls):
|
def count_fields(cls):
|
||||||
|
@ -201,10 +227,11 @@ class MIOItemw(BaseModel):
|
||||||
TN:单件记录
|
TN:单件记录
|
||||||
"""
|
"""
|
||||||
number = models.TextField('编号')
|
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'
|
wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw'
|
||||||
, null=True, blank=True)
|
, null=True, blank=True)
|
||||||
mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem')
|
mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem')
|
||||||
ftest = models.ForeignKey("qm.ftest", verbose_name='关联检验记录', on_delete=models.PROTECT,
|
ftest = models.ForeignKey("qm.ftest", verbose_name='关联检验记录', on_delete=models.PROTECT,
|
||||||
related_name='mioitemw_ftest', null=True, blank=True)
|
related_name='mioitemw_ftest', null=True, blank=True)
|
||||||
note = models.TextField('备注', null=True, blank=True)
|
note = models.TextField('备注', null=True, blank=True)
|
||||||
|
|
||||||
|
|
|
@ -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.constants import EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS
|
||||||
from apps.utils.serializers import CustomModelSerializer
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
from apps.mtm.models import Material
|
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 django.db import transaction
|
||||||
from server.settings import get_sysconfig
|
from server.settings import get_sysconfig
|
||||||
from apps.wpmw.models import Wpr
|
from apps.wpmw.models import Wpr
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class WareHourseSerializer(CustomModelSerializer):
|
class WareHourseSerializer(CustomModelSerializer):
|
||||||
|
@ -29,6 +30,15 @@ class MaterialBatchAListSerializer(CustomModelSerializer):
|
||||||
fields = ['material', 'batch', 'rate', 'mb', 'id', 'material_']
|
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):
|
class MaterialBatchSerializer(CustomModelSerializer):
|
||||||
warehouse_name = serializers.CharField(
|
warehouse_name = serializers.CharField(
|
||||||
source='warehouse.name', read_only=True)
|
source='warehouse.name', read_only=True)
|
||||||
|
@ -38,11 +48,18 @@ class MaterialBatchSerializer(CustomModelSerializer):
|
||||||
source='supplier', read_only=True)
|
source='supplier', read_only=True)
|
||||||
material_ = MaterialSerializer(source='material', read_only=True)
|
material_ = MaterialSerializer(source='material', read_only=True)
|
||||||
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
||||||
|
count_mioing = serializers.IntegerField(read_only=True, label='正在出入库数量')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MaterialBatch
|
model = MaterialBatch
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
read_only_fields = EXCLUDE_FIELDS_BASE
|
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):
|
class MaterialBatchDetailSerializer(CustomModelSerializer):
|
||||||
|
@ -109,14 +126,15 @@ class MIOItemCreateSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MIOItem
|
model = MIOItem
|
||||||
fields = ['mio', 'warehouse', 'material',
|
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 = {
|
extra_kwargs = {
|
||||||
'mio': {'required': True}, 'warehouse': {'required': False},
|
'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):
|
def create(self, validated_data):
|
||||||
mio:MIO = validated_data['mio']
|
mio:MIO = validated_data['mio']
|
||||||
|
mio_type = mio.type
|
||||||
mb = validated_data.get('mb', None)
|
mb = validated_data.get('mb', None)
|
||||||
wm = validated_data.get('wm', None)
|
wm = validated_data.get('wm', None)
|
||||||
assemb = validated_data.pop('assemb', [])
|
assemb = validated_data.pop('assemb', [])
|
||||||
|
@ -135,9 +153,14 @@ class MIOItemCreateSerializer(CustomModelSerializer):
|
||||||
validated_data["batch"] = wm.batch
|
validated_data["batch"] = wm.batch
|
||||||
|
|
||||||
material: Material = validated_data['material']
|
material: Material = validated_data['material']
|
||||||
batch = validated_data['batch']
|
batch = validated_data.get("batch", None)
|
||||||
|
if not batch:
|
||||||
|
batch = "无"
|
||||||
if material.is_hidden:
|
if material.is_hidden:
|
||||||
raise ParseError('隐式物料不可出入库')
|
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:
|
if mio.state != MIO.MIO_CREATE:
|
||||||
raise ParseError('出入库记录非创建中不可新增')
|
raise ParseError('出入库记录非创建中不可新增')
|
||||||
|
@ -148,50 +171,59 @@ class MIOItemCreateSerializer(CustomModelSerializer):
|
||||||
mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN])
|
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()):
|
if mis.exists() and (not mis.exclude(test_date=None).exists()):
|
||||||
raise ParseError('该批次的物料未经检验')
|
raise ParseError('该批次的物料未经检验')
|
||||||
with transaction.atomic():
|
|
||||||
count = validated_data["count"]
|
count = validated_data["count"]
|
||||||
batch = validated_data["batch"]
|
batch = validated_data["batch"]
|
||||||
mioitemw = validated_data.pop('mioitemw', [])
|
mioitemw = validated_data.pop('mioitemw', [])
|
||||||
instance = super().create(validated_data)
|
instance:MIOItem = super().create(validated_data)
|
||||||
assemb_dict = {}
|
assemb_dict = {}
|
||||||
for i in assemb:
|
for i in assemb:
|
||||||
assemb_dict[i['material'].id] = i
|
assemb_dict[i['material'].id] = i
|
||||||
if material.is_assemb and '_in' in mio.type: # 仅入库且是组合件的时候需要填写下一级
|
if material.is_assemb and '_in' in mio.type: # 仅入库且是组合件的时候需要填写下一级
|
||||||
components = material.components
|
components = material.components
|
||||||
for k, v in components.items():
|
for k, v in components.items():
|
||||||
if k in assemb_dict:
|
if k in assemb_dict:
|
||||||
mia = assemb_dict[k]
|
mia = assemb_dict[k]
|
||||||
MIOItemA.objects.create(
|
MIOItemA.objects.create(
|
||||||
mioitem=instance, rate=v, **mia)
|
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:
|
else:
|
||||||
raise ParseError('缺少组合件')
|
raise ParseError('请提供产品明细编号')
|
||||||
if material.tracking == Material.MA_TRACKING_SINGLE:
|
elif wm:
|
||||||
if len(mioitemw) == 0:
|
wpr_qs = Wpr.get_qs_by_wm(wm)
|
||||||
if mb:
|
if wpr_qs.count() == validated_data["count"]:
|
||||||
wpr_qs = Wpr.get_qs_by_mb(mb)
|
for item in wpr_qs:
|
||||||
if wpr_qs.count() == validated_data["count"]:
|
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
raise ParseError('不支持自动生成请提供产品明细')
|
raise ParseError('请提供产品明细编号')
|
||||||
elif len(mioitemw) >= 1:
|
elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1:
|
||||||
mio_type = mio.type
|
MIOItemw.objects.create(mioitem=instance, number=batch)
|
||||||
for item in mioitemw:
|
else:
|
||||||
if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in":
|
raise ParseError('不支持自动生成请提供产品明细')
|
||||||
raise ParseError(f'{item["number"]}_请提供产品明细ID')
|
elif len(mioitemw) >= 1:
|
||||||
else:
|
mio_type = mio.type
|
||||||
MIOItemw.objects.create(mioitem=instance, **item)
|
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
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -202,16 +234,14 @@ class MIOItemAListSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MIOItemA
|
model = MIOItemA
|
||||||
fields = ['material', 'batch', 'rate', 'mioitem',
|
fields = "__all__"
|
||||||
'id', 'material_', 'material_name']
|
read_only_fields = EXCLUDE_FIELDS_BASE
|
||||||
|
|
||||||
|
|
||||||
class MIOItemSerializer(CustomModelSerializer):
|
class MIOItemSerializer(CustomModelSerializer):
|
||||||
warehouse_name = serializers.CharField(
|
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||||
source='warehouse.name', read_only=True)
|
|
||||||
material_ = MaterialSerializer(source='material', read_only=True)
|
material_ = MaterialSerializer(source='material', read_only=True)
|
||||||
assemb = MIOItemAListSerializer(
|
assemb = serializers.SerializerMethodField(label="组合件信息")
|
||||||
source='a_mioitem', read_only=True, many=True)
|
|
||||||
material_name = serializers.StringRelatedField(
|
material_name = serializers.StringRelatedField(
|
||||||
source='material', read_only=True)
|
source='material', read_only=True)
|
||||||
inout_date = serializers.DateField(source='mio.inout_date', read_only=True)
|
inout_date = serializers.DateField(source='mio.inout_date', read_only=True)
|
||||||
|
@ -222,6 +252,24 @@ class MIOItemSerializer(CustomModelSerializer):
|
||||||
model = MIOItem
|
model = MIOItem
|
||||||
fields = '__all__'
|
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):
|
class MIODoSerializer(CustomModelSerializer):
|
||||||
|
@ -235,8 +283,11 @@ class MIODoSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MIO
|
model = MIO
|
||||||
fields = ['id', 'number', 'note', 'do_user',
|
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}, 'number': {"required": False, "allow_blank": True}}
|
extra_kwargs = {'inout_date': {'required': True},
|
||||||
|
'do_user': {'required': True},
|
||||||
|
'number': {"required": False, "allow_blank": True},
|
||||||
|
'type': {'required': True}}
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
if 'mgroup' in attrs and attrs['mgroup']:
|
if 'mgroup' in attrs and attrs['mgroup']:
|
||||||
|
@ -246,10 +297,13 @@ class MIODoSerializer(CustomModelSerializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
type = validated_data['type']
|
||||||
|
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValidationError('出入库类型错误')
|
||||||
if not validated_data.get("number", None):
|
if not validated_data.get("number", None):
|
||||||
validated_data["number"] = MIO.get_a_number(validated_data["type"])
|
validated_data["number"] = MIO.get_a_number(validated_data["type"])
|
||||||
if validated_data['type'] not in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_DO_IN]:
|
|
||||||
raise ValidationError('出入库类型错误')
|
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
@ -294,11 +348,17 @@ class MIOPurSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MIO
|
model = MIO
|
||||||
fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user']
|
fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user', 'type']
|
||||||
extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}}
|
extra_kwargs = {'inout_date': {'required': True},
|
||||||
|
'number': {"required": False, "allow_blank": True},
|
||||||
|
'type': {'required': True}}
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['type'] = MIO.MIO_TYPE_PUR_IN
|
type = validated_data["type"]
|
||||||
|
if type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValidationError('出入库类型错误')
|
||||||
if not validated_data.get("number", None):
|
if not validated_data.get("number", None):
|
||||||
validated_data["number"] = MIO.get_a_number(validated_data["type"])
|
validated_data["number"] = MIO.get_a_number(validated_data["type"])
|
||||||
pu_order: PuOrder = validated_data.get('pu_order', None)
|
pu_order: PuOrder = validated_data.get('pu_order', None)
|
||||||
|
@ -383,3 +443,23 @@ class MIOItemPurInTestSerializer(CustomModelSerializer):
|
||||||
attrs['weight_kgs'] = [float(i) for i in weight_kgs]
|
attrs['weight_kgs'] = [float(i) for i in weight_kgs]
|
||||||
attrs['count_sampling'] = len(attrs['weight_kgs'])
|
attrs['count_sampling'] = len(attrs['weight_kgs'])
|
||||||
return super().validate(attrs)
|
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 rest_framework.exceptions import ParseError
|
||||||
from apps.wpmw.models import Wpr
|
from apps.wpmw.models import Wpr
|
||||||
from apps.mtm.models import Material
|
from apps.mtm.models import Material
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
|
class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
|
||||||
ftest = FtestProcessSerializer(required=False)
|
ftest = FtestProcessSerializer(required=False)
|
||||||
|
wpr_number_out = serializers.CharField(source="wpr.number_out", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MIOItemw
|
model = MIOItemw
|
||||||
fields = ["id", "number", "wpr", "note", "mioitem", "ftest"]
|
fields = ["id", "number", "wpr", "note", "mioitem", "ftest", "wpr_number_out"]
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
mioitem: MIOItem = attrs["mioitem"]
|
mioitem: MIOItem = attrs["mioitem"]
|
||||||
|
@ -43,7 +45,6 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
|
||||||
ftest_sr.update(instance=ftest, validated_data=ftest_data)
|
ftest_sr.update(instance=ftest, validated_data=ftest_data)
|
||||||
return mioitemw
|
return mioitemw
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
wpr: Wpr = validated_data.get("wpr", None)
|
wpr: Wpr = validated_data.get("wpr", None)
|
||||||
if wpr:
|
if wpr:
|
||||||
|
@ -56,7 +57,6 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
|
||||||
mioitemw = self.save_ftest(mioitemw, ftest_data)
|
mioitemw = self.save_ftest(mioitemw, ftest_data)
|
||||||
return mioitemw
|
return mioitemw
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
validated_data.pop("mioitem")
|
validated_data.pop("mioitem")
|
||||||
ftest_data = validated_data.pop("ftest", None)
|
ftest_data = validated_data.pop("ftest", None)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from apps.wpmw.models import Wpr
|
||||||
from apps.qm.models import Ftest, Defect
|
from apps.qm.models import Ftest, Defect
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
def do_out(item: MIOItem):
|
def do_out(item: MIOItem, is_reverse: bool = False):
|
||||||
"""
|
"""
|
||||||
生产领料到车间
|
生产领料到车间
|
||||||
"""
|
"""
|
||||||
|
@ -23,8 +23,6 @@ def do_out(item: MIOItem):
|
||||||
mgroup = mio.mgroup
|
mgroup = mio.mgroup
|
||||||
do_user = mio.do_user
|
do_user = mio.do_user
|
||||||
material:Material = item.material
|
material:Material = item.material
|
||||||
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
|
|
||||||
return
|
|
||||||
|
|
||||||
# 获取defect
|
# 获取defect
|
||||||
defect:Defect = None
|
defect:Defect = None
|
||||||
|
@ -94,29 +92,36 @@ def do_out(item: MIOItem):
|
||||||
raise ParseError(f"批次错误!{e}")
|
raise ParseError(f"批次错误!{e}")
|
||||||
mb.count = mb.count - xcount
|
mb.count = mb.count - xcount
|
||||||
if mb.count < 0:
|
if mb.count < 0:
|
||||||
raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
|
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
|
||||||
else:
|
else:
|
||||||
mb.save()
|
mb.save()
|
||||||
|
|
||||||
|
if material.into_wm:
|
||||||
# 领到车间库存(或工段)
|
# 领到车间库存(或工段)
|
||||||
wm, new_create = WMaterial.objects.get_or_create(
|
wm, new_create = WMaterial.objects.get_or_create(
|
||||||
batch=xbatch, material=xmaterial,
|
batch=xbatch, material=xmaterial,
|
||||||
belong_dept=belong_dept, mgroup=mgroup,
|
belong_dept=belong_dept, mgroup=mgroup,
|
||||||
state=WMaterial.WM_OK, defect=defect)
|
state=WMaterial.WM_OK, defect=defect)
|
||||||
if new_create:
|
if new_create:
|
||||||
wm.create_by = do_user
|
wm.create_by = do_user
|
||||||
wm.batch_ofrom = mb.batch if mb else None
|
wm.batch_ofrom = mb.batch if mb else None
|
||||||
wm.material_ofrom = mb.material if mb else None
|
wm.material_ofrom = mb.material if mb else None
|
||||||
wm.count = wm.count + item.count
|
wm.count = wm.count + item.count
|
||||||
wm.update_by = do_user
|
wm.update_by = do_user
|
||||||
wm.save()
|
wm.save()
|
||||||
|
|
||||||
# 开始变动wpr
|
# 开始变动wpr
|
||||||
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
|
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
|
||||||
|
if material.into_wm is False:
|
||||||
|
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
|
||||||
mioitemws = MIOItemw.objects.filter(mioitem=item)
|
mioitemws = MIOItemw.objects.filter(mioitem=item)
|
||||||
if mioitemws.count() != item.count:
|
if mioitemws.count() != item.count:
|
||||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
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:
|
for mioitemw in mioitemws:
|
||||||
Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)
|
Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)
|
||||||
|
|
||||||
|
@ -136,8 +141,7 @@ def do_in(item: MIOItem):
|
||||||
mgroup = mio.mgroup
|
mgroup = mio.mgroup
|
||||||
do_user = mio.do_user
|
do_user = mio.do_user
|
||||||
material = item.material
|
material = item.material
|
||||||
if material.into_wm is False: # 根据配置不进行入车间库存的处理
|
|
||||||
return
|
|
||||||
action_list = []
|
action_list = []
|
||||||
mias = MIOItemA.objects.filter(mioitem=item)
|
mias = MIOItemA.objects.filter(mioitem=item)
|
||||||
is_zhj = False # 是否组合件入仓库
|
is_zhj = False # 是否组合件入仓库
|
||||||
|
@ -172,38 +176,39 @@ def do_in(item: MIOItem):
|
||||||
raise ParseError("存在非正数!")
|
raise ParseError("存在非正数!")
|
||||||
|
|
||||||
xbatchs.append(xbatch)
|
xbatchs.append(xbatch)
|
||||||
|
if material.into_wm:
|
||||||
wm_qs = WMaterial.objects.filter(
|
wm_qs = WMaterial.objects.filter(
|
||||||
batch=xbatch,
|
batch=xbatch,
|
||||||
material=xmaterial,
|
material=xmaterial,
|
||||||
belong_dept=belong_dept,
|
belong_dept=belong_dept,
|
||||||
mgroup=mgroup,
|
mgroup=mgroup,
|
||||||
defect=defect,
|
defect=defect,
|
||||||
state=WMaterial.WM_OK)
|
state=WMaterial.WM_OK)
|
||||||
count_x = wm_qs.count()
|
count_x = wm_qs.count()
|
||||||
if count_x == 1:
|
if count_x == 1:
|
||||||
wm = wm_qs.first()
|
wm = wm_qs.first()
|
||||||
elif count_x == 0:
|
elif count_x == 0:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
|
f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
|
||||||
else:
|
else:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
|
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
|
||||||
|
|
||||||
|
# 扣减车间库存
|
||||||
|
new_count = wm.count - xcount
|
||||||
|
if new_count >= 0:
|
||||||
|
wm.count = new_count
|
||||||
|
wm.update_by = do_user
|
||||||
|
wm.save()
|
||||||
|
else:
|
||||||
|
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
|
||||||
|
|
||||||
|
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
|
||||||
|
if production_dept is None:
|
||||||
|
production_dept = wm_production_dept
|
||||||
|
elif wm_production_dept and production_dept != wm_production_dept:
|
||||||
|
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
|
||||||
|
|
||||||
# 扣减车间库存
|
|
||||||
new_count = wm.count - xcount
|
|
||||||
if new_count >= 0:
|
|
||||||
wm.count = new_count
|
|
||||||
wm.update_by = do_user
|
|
||||||
wm.save()
|
|
||||||
else:
|
|
||||||
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
|
|
||||||
|
|
||||||
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
|
|
||||||
if production_dept is None:
|
|
||||||
production_dept = wm_production_dept
|
|
||||||
elif wm_production_dept and production_dept != wm_production_dept:
|
|
||||||
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
|
|
||||||
# 增加mb
|
# 增加mb
|
||||||
if not is_zhj:
|
if not is_zhj:
|
||||||
mb, _ = MaterialBatch.objects.get_or_create(
|
mb, _ = MaterialBatch.objects.get_or_create(
|
||||||
|
@ -226,9 +231,16 @@ def do_in(item: MIOItem):
|
||||||
|
|
||||||
# 开始变动wpr
|
# 开始变动wpr
|
||||||
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
|
if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
|
||||||
|
if material.into_wm is False:
|
||||||
|
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
|
||||||
mioitemws = MIOItemw.objects.filter(mioitem=item)
|
mioitemws = MIOItemw.objects.filter(mioitem=item)
|
||||||
if mioitemws.count() != item.count:
|
if mioitemws.count() != item.count:
|
||||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
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:
|
for mioitemw in mioitemws:
|
||||||
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)
|
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)
|
||||||
|
|
||||||
|
@ -271,6 +283,9 @@ class InmService:
|
||||||
"""
|
"""
|
||||||
更新库存, 支持反向操作
|
更新库存, 支持反向操作
|
||||||
"""
|
"""
|
||||||
|
if not MIOItem.objects.filter(mio=instance).exists():
|
||||||
|
raise ParseError("出入库记录缺失明细,无法操作")
|
||||||
|
|
||||||
if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单
|
if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单
|
||||||
# 这里还需要对入厂检验进行处理
|
# 这里还需要对入厂检验进行处理
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
|
@ -278,25 +293,32 @@ class InmService:
|
||||||
else:
|
else:
|
||||||
for item in MIOItem.objects.filter(mio=instance):
|
for item in MIOItem.objects.filter(mio=instance):
|
||||||
BatchSt.g_create(
|
BatchSt.g_create(
|
||||||
batch=item.batch, mio=instance, material_start=item.material, reuse_node=True)
|
batch=item.batch, mio=instance, material_start=item.material)
|
||||||
from apps.pum.services import PumService
|
from apps.pum.services import PumService
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
cls.update_mb(instance, -1)
|
cls.update_mb(instance, -1)
|
||||||
else:
|
else:
|
||||||
cls.update_mb(instance, 1)
|
cls.update_mb(instance, 1)
|
||||||
PumService.mio_purin(instance, is_reverse)
|
PumService.mio_pur(instance, is_reverse)
|
||||||
|
elif instance.type == MIO.MIO_TYPE_PUR_OUT:
|
||||||
|
from apps.pum.services import PumService
|
||||||
|
if is_reverse:
|
||||||
|
cls.update_mb(instance, 1)
|
||||||
|
else:
|
||||||
|
cls.update_mb(instance, -1)
|
||||||
|
PumService.mio_pur(instance, is_reverse)
|
||||||
elif instance.type == MIO.MIO_TYPE_OTHER_IN:
|
elif instance.type == MIO.MIO_TYPE_OTHER_IN:
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
BatchLog.clear(mio=instance)
|
BatchLog.clear(mio=instance)
|
||||||
else:
|
else:
|
||||||
for item in MIOItem.objects.filter(mio=instance):
|
for item in MIOItem.objects.filter(mio=instance):
|
||||||
BatchSt.g_create(
|
BatchSt.g_create(
|
||||||
batch=item.batch, mio=instance, material_start=item.material, reuse_node=True)
|
batch=item.batch, mio=instance, material_start=item.material)
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
cls.update_mb(instance, -1)
|
cls.update_mb(instance, -1)
|
||||||
else:
|
else:
|
||||||
cls.update_mb(instance, 1)
|
cls.update_mb(instance, 1)
|
||||||
elif instance.type == MIO.MIO_TYPE_DO_IN:
|
elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]:
|
||||||
mioitems = MIOItem.objects.filter(mio=instance)
|
mioitems = MIOItem.objects.filter(mio=instance)
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
for item in mioitems:
|
for item in mioitems:
|
||||||
|
@ -304,6 +326,14 @@ class InmService:
|
||||||
else:
|
else:
|
||||||
for item in mioitems:
|
for item in mioitems:
|
||||||
do_in(item)
|
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:
|
elif instance.type == MIO.MIO_TYPE_SALE_OUT:
|
||||||
from apps.sam.services import SamService
|
from apps.sam.services import SamService
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
|
@ -316,14 +346,6 @@ class InmService:
|
||||||
cls.update_mb(instance, 1)
|
cls.update_mb(instance, 1)
|
||||||
else:
|
else:
|
||||||
cls.update_mb(instance, -1)
|
cls.update_mb(instance, -1)
|
||||||
elif instance.type == MIO.MIO_TYPE_DO_OUT:
|
|
||||||
mioitems = MIOItem.objects.filter(mio=instance)
|
|
||||||
if is_reverse:
|
|
||||||
for item in mioitems:
|
|
||||||
do_in(item)
|
|
||||||
else:
|
|
||||||
for item in mioitems:
|
|
||||||
do_out(item)
|
|
||||||
else:
|
else:
|
||||||
raise ParseError('不支持该出入库操作')
|
raise ParseError('不支持该出入库操作')
|
||||||
|
|
||||||
|
@ -349,6 +371,7 @@ class InmService:
|
||||||
out = -1
|
out = -1
|
||||||
默认使用count字段做加减
|
默认使用count字段做加减
|
||||||
"""
|
"""
|
||||||
|
mio_type = i.mio.type
|
||||||
material: Material = i.material
|
material: Material = i.material
|
||||||
warehouse = i.warehouse
|
warehouse = i.warehouse
|
||||||
tracking = material.tracking
|
tracking = material.tracking
|
||||||
|
@ -387,7 +410,7 @@ class InmService:
|
||||||
if change_count < 0:
|
if change_count < 0:
|
||||||
raise ParseError("存在负数!")
|
raise ParseError("存在负数!")
|
||||||
state = WMaterial.WM_OK
|
state = WMaterial.WM_OK
|
||||||
if defect:
|
if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
|
||||||
state = WMaterial.WM_NOTOK
|
state = WMaterial.WM_NOTOK
|
||||||
mb, _ = MaterialBatch.objects.get_or_create(
|
mb, _ = MaterialBatch.objects.get_or_create(
|
||||||
material=material,
|
material=material,
|
||||||
|
@ -419,7 +442,7 @@ class InmService:
|
||||||
elif in_or_out == -1:
|
elif in_or_out == -1:
|
||||||
mb.count = mb.count - change_count
|
mb.count = mb.count - change_count
|
||||||
if mb.count < 0:
|
if mb.count < 0:
|
||||||
raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
|
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
|
||||||
else:
|
else:
|
||||||
mb.save()
|
mb.save()
|
||||||
if tracking == Material.MA_TRACKING_SINGLE:
|
if tracking == Material.MA_TRACKING_SINGLE:
|
||||||
|
@ -430,7 +453,7 @@ class InmService:
|
||||||
if mioitemws.count() != change_count:
|
if mioitemws.count() != change_count:
|
||||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
raise ParseError("出入库与明细数量不一致,操作失败")
|
||||||
for mioitemw in mioitemws:
|
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:
|
else:
|
||||||
raise ParseError("不支持的操作")
|
raise ParseError("不支持的操作")
|
||||||
|
|
||||||
|
@ -438,3 +461,26 @@ class InmService:
|
||||||
ana_batch_thread(xbatchs)
|
ana_batch_thread(xbatchs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def revert_and_del(cls, mioitem: MIOItem):
|
||||||
|
mio = mioitem.mio
|
||||||
|
if mio.submit_time is None:
|
||||||
|
raise ParseError("未提交的出入库明细不允许撤销")
|
||||||
|
if mioitem.test_date is not None:
|
||||||
|
raise ParseError("已检验的出入库明细不允许撤销")
|
||||||
|
if mio.type == MIO.MIO_TYPE_PUR_IN:
|
||||||
|
from apps.pum.services import PumService
|
||||||
|
cls.update_mb_item(mioitem, -1)
|
||||||
|
BatchLog.clear(mioitem=mioitem)
|
||||||
|
PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem)
|
||||||
|
mioitem.delete()
|
||||||
|
elif mio.type == MIO.MIO_TYPE_OTHER_IN:
|
||||||
|
cls.update_mb_item(mioitem, -1)
|
||||||
|
BatchLog.clear(mioitem=mioitem)
|
||||||
|
mioitem.delete()
|
||||||
|
elif mio.type == MIO.MIO_TYPE_DO_OUT:
|
||||||
|
do_in(mioitem)
|
||||||
|
BatchLog.clear(mioitem=mioitem)
|
||||||
|
mioitem.delete()
|
||||||
|
else:
|
||||||
|
raise ParseError("不支持该出入库单明细撤销")
|
|
@ -1,6 +1,6 @@
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from apps.mtm.models import Process, Material
|
from apps.mtm.models import Process, Material
|
||||||
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw
|
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw, MIO
|
||||||
from apps.utils.tools import ranstr
|
from apps.utils.tools import ranstr
|
||||||
from apps.mtm.services_2 import cal_material_count
|
from apps.mtm.services_2 import cal_material_count
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
|
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
|
||||||
|
|
||||||
qct = Qct.get(mioitem.material, tag="inm")
|
qct = Qct.get(mioitem.material, tag="inm", type="in")
|
||||||
if qct is None:
|
if qct is None:
|
||||||
raise ParseError("未找到检验表")
|
raise ParseError("未找到检验表")
|
||||||
|
|
||||||
|
@ -84,8 +84,11 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
|
||||||
test_user = mioitem.mio.mio_user
|
test_user = mioitem.mio.mio_user
|
||||||
test_date = mioitem.mio.inout_date
|
test_date = mioitem.mio.inout_date
|
||||||
|
|
||||||
wb = load_workbook(path)
|
wb = load_workbook(path, data_only=True)
|
||||||
sheet = wb["Sheet1"]
|
if "Sheet1" in wb.sheetnames: # 检查是否存在
|
||||||
|
sheet = wb["Sheet1"] # 获取工作表
|
||||||
|
else:
|
||||||
|
raise ParseError("未找到Sheet1")
|
||||||
|
|
||||||
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
|
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
|
||||||
|
|
||||||
|
@ -120,4 +123,39 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
|
||||||
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id()))
|
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)
|
FtestItem.objects.bulk_create(ftestitems)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def daoru_mioitems(path:str, mio:MIO):
|
||||||
|
from apps.utils.snowflake import idWorker
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
wb = load_workbook(path, data_only=True)
|
||||||
|
if "Sheet1" in wb.sheetnames: # 检查是否存在
|
||||||
|
sheet = wb["Sheet1"] # 获取工作表
|
||||||
|
else:
|
||||||
|
raise ParseError("未找到Sheet1")
|
||||||
|
|
||||||
|
mioitems = []
|
||||||
|
ind = 2
|
||||||
|
while sheet[f"a{ind}"].value:
|
||||||
|
batch = sheet[f"b{ind}"].value
|
||||||
|
material_number = sheet[f"a{ind}"].value
|
||||||
|
try:
|
||||||
|
material = Material.objects.get(number=material_number)
|
||||||
|
except Exception as e:
|
||||||
|
raise ParseError(f"未找到物料:{material_number} {e}")
|
||||||
|
if batch:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
batch = "无"
|
||||||
|
count = sheet[f"c{ind}"].value
|
||||||
|
warehouse_name = sheet[f"d{ind}"].value
|
||||||
|
try:
|
||||||
|
warehouse = WareHouse.objects.get(name=warehouse_name)
|
||||||
|
except Exception as e:
|
||||||
|
raise ParseError(f"未找到仓库:{warehouse_name} {e}")
|
||||||
|
mioitems.append(MIOItem(mio=mio, warehouse=warehouse, material=material, batch=batch, count=count, id=idWorker.get_id()))
|
||||||
|
ind = ind + 1
|
||||||
|
|
||||||
|
MIOItem.objects.bulk_create(mioitems)
|
|
@ -19,6 +19,7 @@ router.register('mio/pur', MioPurViewSet)
|
||||||
router.register('mio/other', MioOtherViewSet)
|
router.register('mio/other', MioOtherViewSet)
|
||||||
router.register('mioitem', MIOItemViewSet, basename='mioitem')
|
router.register('mioitem', MIOItemViewSet, basename='mioitem')
|
||||||
router.register('mioitemw', MIOItemwViewSet, basename='mioitemw')
|
router.register('mioitemw', MIOItemwViewSet, basename='mioitemw')
|
||||||
|
# router.register('pack', PackViewSet, basename='pack')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
path(API_BASE_URL, include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,22 +9,25 @@ from django.utils import timezone
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db.models import Sum
|
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 (
|
from apps.inm.serializers import (
|
||||||
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
|
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
|
||||||
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
|
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
|
||||||
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
|
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
|
||||||
MIOItemwSerializer)
|
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer)
|
||||||
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
|
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
from apps.inm.services import InmService
|
from apps.inm.services import InmService
|
||||||
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test
|
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test, daoru_mioitems
|
||||||
from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin,
|
from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin,
|
||||||
CustomListModelMixin)
|
CustomListModelMixin)
|
||||||
from apps.utils.permission import has_perm
|
from apps.utils.permission import has_perm
|
||||||
from .filters import MaterialBatchFilter, MioFilter
|
from .filters import MaterialBatchFilter, MioFilter
|
||||||
from apps.qm.serializers import FtestProcessSerializer
|
from apps.qm.serializers import FtestProcessSerializer
|
||||||
from apps.mtm.models import Material
|
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.
|
# Create your views here.
|
||||||
|
@ -145,9 +148,19 @@ class MIOViewSet(CustomModelViewSet):
|
||||||
serializer_class = MIOListSerializer
|
serializer_class = MIOListSerializer
|
||||||
retrieve_serializer_class = MIODetailSerializer
|
retrieve_serializer_class = MIODetailSerializer
|
||||||
filterset_class = MioFilter
|
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
|
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):
|
def add_info_for_list(self, data):
|
||||||
# 获取检验状态
|
# 获取检验状态
|
||||||
mio_dict = {}
|
mio_dict = {}
|
||||||
|
@ -168,7 +181,7 @@ class MIOViewSet(CustomModelViewSet):
|
||||||
if self.action in ['create', 'update', 'partial_update']:
|
if self.action in ['create', 'update', 'partial_update']:
|
||||||
type = self.request.data.get('type')
|
type = self.request.data.get('type')
|
||||||
user = self.request.user
|
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']):
|
if not has_perm(user, ['mio.do']):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return MIODoSerializer
|
return MIODoSerializer
|
||||||
|
@ -180,7 +193,7 @@ class MIOViewSet(CustomModelViewSet):
|
||||||
if not has_perm(user, ['mio.sale']):
|
if not has_perm(user, ['mio.sale']):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return MIOSaleSerializer
|
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']):
|
if not has_perm(user, ['mio.pur']):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return MIOPurSerializer
|
return MIOPurSerializer
|
||||||
|
@ -192,27 +205,29 @@ class MIOViewSet(CustomModelViewSet):
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
|
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
|
||||||
|
@transaction.atomic
|
||||||
def submit(self, request, *args, **kwargs):
|
def submit(self, request, *args, **kwargs):
|
||||||
"""提交
|
"""提交
|
||||||
|
|
||||||
提交
|
提交
|
||||||
"""
|
"""
|
||||||
ins = self.get_object()
|
ins:MIO = self.get_object()
|
||||||
if ins.inout_date is None:
|
if ins.inout_date is None:
|
||||||
raise ParseError('出入库日期未填写')
|
raise ParseError('出入库日期未填写')
|
||||||
if ins.state != MIO.MIO_CREATE:
|
if ins.state != MIO.MIO_CREATE:
|
||||||
raise ParseError('记录状态异常')
|
raise ParseError('记录状态异常')
|
||||||
with transaction.atomic():
|
now = timezone.now()
|
||||||
ins.submit_time = timezone.now()
|
ins.submit_user = request.user
|
||||||
ins.state = MIO.MIO_SUBMITED
|
ins.submit_time = now
|
||||||
ins.submit_user = request.user
|
ins.update_by = request.user
|
||||||
ins.update_by = request.user
|
ins.state = MIO.MIO_SUBMITED
|
||||||
ins.save()
|
ins.save()
|
||||||
InmService.update_inm(ins)
|
InmService.update_inm(ins)
|
||||||
InmService.update_material_count(ins)
|
InmService.update_material_count(ins)
|
||||||
return Response(MIOListSerializer(instance=ins).data)
|
return Response(MIOListSerializer(instance=ins).data)
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
|
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
|
||||||
|
@transaction.atomic
|
||||||
def revert(self, request, *args, **kwargs):
|
def revert(self, request, *args, **kwargs):
|
||||||
"""撤回
|
"""撤回
|
||||||
|
|
||||||
|
@ -224,16 +239,84 @@ class MIOViewSet(CustomModelViewSet):
|
||||||
raise ParseError('记录状态异常')
|
raise ParseError('记录状态异常')
|
||||||
if ins.submit_user != user:
|
if ins.submit_user != user:
|
||||||
raise ParseError('非提交人不可撤回')
|
raise ParseError('非提交人不可撤回')
|
||||||
with transaction.atomic():
|
ins.submit_user = None
|
||||||
ins.submit_time = None
|
ins.update_by = user
|
||||||
ins.state = MIO.MIO_CREATE
|
ins.state = MIO.MIO_CREATE
|
||||||
ins.update_by = user
|
ins.submit_time = None
|
||||||
ins.save()
|
ins.save()
|
||||||
InmService.update_inm(ins, is_reverse=True)
|
InmService.update_inm(ins, is_reverse=True)
|
||||||
InmService.update_material_count(ins)
|
InmService.update_material_count(ins)
|
||||||
return Response()
|
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):
|
class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
list: 出入库明细
|
list: 出入库明细
|
||||||
|
@ -243,6 +326,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
||||||
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
|
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
|
||||||
queryset = MIOItem.objects.all()
|
queryset = MIOItem.objects.all()
|
||||||
serializer_class = MIOItemSerializer
|
serializer_class = MIOItemSerializer
|
||||||
|
retrieve_serializer_class = MioItemDetailSerializer
|
||||||
create_serializer_class = MIOItemCreateSerializer
|
create_serializer_class = MIOItemCreateSerializer
|
||||||
select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
|
select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
|
@ -252,22 +336,58 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
|
||||||
"mio__type": ["exact", "in"],
|
"mio__type": ["exact", "in"],
|
||||||
"mio__inout_date": ["gte", "lte", "exact"],
|
"mio__inout_date": ["gte", "lte", "exact"],
|
||||||
"material": ["exact"],
|
"material": ["exact"],
|
||||||
|
"material__type": ["exact"],
|
||||||
"test_date": ["isnull", "exact"]
|
"test_date": ["isnull", "exact"]
|
||||||
}
|
}
|
||||||
ordering = ['create_time']
|
ordering = ['create_time']
|
||||||
ordering_fields = ['create_time', 'test_date']
|
ordering_fields = ['create_time', 'test_date']
|
||||||
|
search_fields =['batch', 'a_mioitem__batch']
|
||||||
|
|
||||||
def add_info_for_list(self, data):
|
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
|
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):
|
def perform_destroy(self, instance):
|
||||||
if instance.mio.state != MIO.MIO_CREATE:
|
MIOViewSet.lock_and_check_can_update(instance.mio)
|
||||||
raise ParseError('出入库记录非创建中不可删除')
|
|
||||||
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
|
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
|
||||||
raise PermissionDenied('无权限删除')
|
raise PermissionDenied('无权限删除')
|
||||||
return super().perform_destroy(instance)
|
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)
|
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def test(self, request, *args, **kwargs):
|
def test(self, request, *args, **kwargs):
|
||||||
|
@ -367,7 +487,7 @@ class MIOItemwViewSet(CustomModelViewSet):
|
||||||
perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'}
|
perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'}
|
||||||
queryset = MIOItemw.objects.all()
|
queryset = MIOItemw.objects.all()
|
||||||
serializer_class = MIOItemwCreateUpdateSerializer
|
serializer_class = MIOItemwCreateUpdateSerializer
|
||||||
filterset_fields = ['mioitem']
|
filterset_fields = ['mioitem', 'wpr']
|
||||||
ordering = ["number", "create_time"]
|
ordering = ["number", "create_time"]
|
||||||
ordering_fields = ["number", "create_time"]
|
ordering_fields = ["number", "create_time"]
|
||||||
|
|
||||||
|
@ -383,20 +503,20 @@ class MIOItemwViewSet(CustomModelViewSet):
|
||||||
mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count()
|
mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count()
|
||||||
mioitem.save()
|
mioitem.save()
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
ins: MIOItemw = serializer.save()
|
MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio)
|
||||||
mioitem: MIOItem = ins.mioitem
|
ins:MIOItemw = serializer.save()
|
||||||
self.cal_mioitem_count(mioitem)
|
self.cal_mioitem_count(ins.mioitem)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
mioitemw = serializer.save()
|
ins:MIOItemw = serializer.instance
|
||||||
self.cal_mioitem_count(mioitemw.mioitem)
|
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):
|
def perform_destroy(self, instance: MIOItemw):
|
||||||
mioitem = instance.mioitem
|
mioitem = instance.mioitem
|
||||||
|
MIOViewSet.lock_and_check_can_update(mioitem.mio)
|
||||||
ftest = instance.ftest
|
ftest = instance.ftest
|
||||||
instance.delete()
|
instance.delete()
|
||||||
if ftest:
|
if ftest:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django_filters import rest_framework as filters
|
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 django.db.models.expressions import F
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
|
|
||||||
class MaterialFilter(filters.FilterSet):
|
class MaterialFilter(filters.FilterSet):
|
||||||
|
@ -45,6 +46,8 @@ class GoalFilter(filters.FilterSet):
|
||||||
|
|
||||||
|
|
||||||
class RouteFilter(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:
|
class Meta:
|
||||||
model = Route
|
model = Route
|
||||||
fields = {
|
fields = {
|
||||||
|
@ -58,5 +61,18 @@ class RouteFilter(filters.FilterSet):
|
||||||
"mgroup": ["exact", "in", "isnull"],
|
"mgroup": ["exact", "in", "isnull"],
|
||||||
"mgroup__name": ["exact", "contains"],
|
"mgroup__name": ["exact", "contains"],
|
||||||
"mgroup__belong_dept": ["exact"],
|
"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,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='上级路线'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,6 +4,8 @@ from rest_framework.exceptions import ParseError
|
||||||
from apps.utils.models import CommonBDModel
|
from apps.utils.models import CommonBDModel
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
class Process(CommonBModel):
|
class Process(CommonBModel):
|
||||||
"""
|
"""
|
||||||
|
@ -42,6 +44,12 @@ class Process(CommonBModel):
|
||||||
"""获取可产出的materialIds
|
"""获取可产出的materialIds
|
||||||
"""
|
"""
|
||||||
return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct())
|
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.
|
# Create your models here.
|
||||||
class Material(CommonAModel):
|
class Material(CommonAModel):
|
||||||
|
@ -101,6 +109,8 @@ class Material(CommonAModel):
|
||||||
brothers = models.JSONField('兄弟件', default=list, null=False, blank=True)
|
brothers = models.JSONField('兄弟件', default=list, null=False, blank=True)
|
||||||
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
|
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
|
||||||
into_wm = models.BooleanField('是否进入车间库存', default=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:
|
class Meta:
|
||||||
verbose_name = '物料表'
|
verbose_name = '物料表'
|
||||||
|
@ -169,6 +179,32 @@ class Mgroup(CommonBModel):
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
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):
|
class TeamMember(BaseModel):
|
||||||
|
@ -324,7 +360,8 @@ class RoutePack(CommonADModel):
|
||||||
route_dict[r.id] = {
|
route_dict[r.id] = {
|
||||||
"label": r.process.name if r.process else "",
|
"label": r.process.name if r.process else "",
|
||||||
"source": r.material_in.id,
|
"source": r.material_in.id,
|
||||||
"target": r.material_out.id
|
"target": r.material_out.id,
|
||||||
|
"id": r.id
|
||||||
}
|
}
|
||||||
|
|
||||||
# 获取所有物料信息
|
# 获取所有物料信息
|
||||||
|
@ -380,7 +417,9 @@ class Route(CommonADModel):
|
||||||
batch_bind = models.BooleanField('是否绑定批次', default=True)
|
batch_bind = models.BooleanField('是否绑定批次', default=True)
|
||||||
materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials",
|
materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials",
|
||||||
through="mtm.routemat", blank=True)
|
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):
|
def __str__(self):
|
||||||
x = ""
|
x = ""
|
||||||
|
@ -510,6 +549,7 @@ class Route(CommonADModel):
|
||||||
'source': source,
|
'source': source,
|
||||||
'target': target,
|
'target': target,
|
||||||
'label': rq.process.name,
|
'label': rq.process.name,
|
||||||
|
'id': rq.id
|
||||||
})
|
})
|
||||||
# 将批次号排序
|
# 将批次号排序
|
||||||
nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time")
|
nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time")
|
||||||
|
|
|
@ -24,7 +24,7 @@ class MaterialSimpleSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Material
|
model = Material
|
||||||
fields = ['id', 'name', 'number', 'model',
|
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):
|
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 ""}'
|
return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}'
|
||||||
|
@ -246,30 +246,34 @@ class RouteSerializer(CustomModelSerializer):
|
||||||
# material = validated_data.get('material', None)
|
# material = validated_data.get('material', None)
|
||||||
# if material and process and Route.objects.filter(material=material, process=process).exists():
|
# if material and process and Route.objects.filter(material=material, process=process).exists():
|
||||||
# raise ValidationError('已选择该工序!!')
|
# raise ValidationError('已选择该工序!!')
|
||||||
with transaction.atomic():
|
|
||||||
instance = super().create(validated_data)
|
instance:Route = super().create(validated_data)
|
||||||
material_out = instance.material_out
|
material_out = instance.material_out
|
||||||
if material_out:
|
if material_out:
|
||||||
if material_out.process is None:
|
if material_out.process is None:
|
||||||
material_out.process = process
|
material_out.process = process
|
||||||
if material_out_tracking != material_out.tracking:
|
if material_out_tracking != material_out.tracking:
|
||||||
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
|
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
|
||||||
if material_out.parent is None and instance.material:
|
if material_out.parent is None and instance.material:
|
||||||
material_out.parent = instance.material
|
material_out.parent = instance.material
|
||||||
material_out.save()
|
material_out.save()
|
||||||
# elif material_out.process != process:
|
# elif material_out.process != process:
|
||||||
# raise ParseError('物料工序错误!请重新选择')
|
# raise ParseError('物料工序错误!请重新选择')
|
||||||
else:
|
else:
|
||||||
if instance.material:
|
if instance.material:
|
||||||
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
|
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
|
||||||
instance.save()
|
instance.save()
|
||||||
rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
|
rx = Route.objects.filter(
|
||||||
if rx:
|
material_in=instance.material_in, material_out=instance.material_out,
|
||||||
msg = ""
|
process=process).exclude(id=instance.id).order_by("create_time").first()
|
||||||
if rx.routepack:
|
if rx:
|
||||||
msg = rx.routepack.name
|
instance.from_route = rx
|
||||||
raise ParseError(f"该工艺步骤已存在-{msg}")
|
instance.save()
|
||||||
return instance
|
# msg = ""
|
||||||
|
# if rx.routepack:
|
||||||
|
# msg = rx.routepack.name
|
||||||
|
# raise ParseError(f"该工艺步骤已存在-{msg}")
|
||||||
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
validated_data.pop('material', None)
|
validated_data.pop('material', None)
|
||||||
|
@ -277,30 +281,34 @@ class RouteSerializer(CustomModelSerializer):
|
||||||
material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH)
|
material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH)
|
||||||
if material_out_tracking is None:
|
if material_out_tracking is None:
|
||||||
material_out_tracking = Material.MA_TRACKING_BATCH
|
material_out_tracking = Material.MA_TRACKING_BATCH
|
||||||
with transaction.atomic():
|
|
||||||
instance = super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
material_out = instance.material_out
|
material_out = instance.material_out
|
||||||
if material_out:
|
if material_out:
|
||||||
if material_out.process is None:
|
if material_out.process is None:
|
||||||
material_out.process = process
|
material_out.process = process
|
||||||
if material_out_tracking != material_out.tracking:
|
if material_out_tracking != material_out.tracking:
|
||||||
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
|
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
|
||||||
if material_out.parent is None and instance.material:
|
if material_out.parent is None and instance.material:
|
||||||
material_out.parent = instance.material
|
material_out.parent = instance.material
|
||||||
material_out.save()
|
material_out.save()
|
||||||
# elif material_out.process != process:
|
# elif material_out.process != process:
|
||||||
# raise ParseError('物料工序错误!请重新选择')
|
# raise ParseError('物料工序错误!请重新选择')
|
||||||
else:
|
else:
|
||||||
if instance.material:
|
if instance.material:
|
||||||
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
|
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
|
||||||
instance.save()
|
instance.save()
|
||||||
rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
|
rx = Route.objects.filter(
|
||||||
if rx:
|
material_in=instance.material_in, material_out=instance.material_out,
|
||||||
msg = ""
|
process=process).exclude(id=instance.id).order_by("create_time").first()
|
||||||
if rx.routepack:
|
if rx:
|
||||||
msg = rx.routepack.name
|
instance.from_route = rx
|
||||||
raise ParseError(f"该工艺步骤已存在-{msg}")
|
instance.save()
|
||||||
return instance
|
# msg = ""
|
||||||
|
# if rx.routepack:
|
||||||
|
# msg = rx.routepack.name
|
||||||
|
# raise ParseError(f"该工艺步骤已存在-{msg}")
|
||||||
|
return instance
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
res = super().to_representation(instance)
|
res = super().to_representation(instance)
|
||||||
|
@ -329,4 +337,16 @@ class RouteMatSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RouteMat
|
model = RouteMat
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = EXCLUDE_FIELDS_BASE
|
read_only_fields = EXCLUDE_FIELDS_BASE
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
route:Route = attrs["route"]
|
||||||
|
if route.from_route is not None:
|
||||||
|
raise ParseError("该工艺步骤引用其他步骤,无法修改")
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialExportSerializer(CustomModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Material
|
||||||
|
fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"]
|
|
@ -51,33 +51,35 @@ def daoru_material(path: str):
|
||||||
'辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70}
|
'辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70}
|
||||||
from apps.utils.snowflake import idWorker
|
from apps.utils.snowflake import idWorker
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
wb = load_workbook(path)
|
wb = load_workbook(path, read_only=True)
|
||||||
sheet = wb['物料']
|
sheet = wb.active
|
||||||
process_l = Process.objects.all()
|
process_l = Process.objects.all()
|
||||||
process_d = {p.name: p for p in process_l}
|
process_d = {p.name: p for p in process_l}
|
||||||
i = 3
|
i = 3
|
||||||
if sheet['a2'].value != '物料编号':
|
if sheet['a2'].value != '物料编号':
|
||||||
raise ParseError('列错误导入失败')
|
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(' ', '')
|
type_str = sheet[f'b{i}'].value.replace(' ', '')
|
||||||
try:
|
try:
|
||||||
type = type_dict[type_str]
|
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
|
number = str(sheet[f'a{i}'].value).replace(' ', '') if sheet[f'a{i}'].value else None
|
||||||
if sheet[f'c{i}'].value:
|
if sheet[f'd{i}'].value:
|
||||||
name = str(sheet[f'c{i}'].value).replace(' ', '')
|
name = str(sheet[f'd{i}'].value).replace(' ', '')
|
||||||
else:
|
else:
|
||||||
raise ParseError(f'{i}行物料信息错误: 物料名称必填')
|
raise ParseError(f'{i}行物料信息错误: 物料名称必填')
|
||||||
specification = str(sheet[f'd{i}'].value).replace(
|
specification = str(sheet[f'e{i}'].value).replace(
|
||||||
'×', '*').replace(' ', '') if sheet[f'd{i}'].value else None
|
'×', '*').replace(' ', '') if sheet[f'e{i}'].value else None
|
||||||
model = str(sheet[f'e{i}'].value).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'f{i}'].value.replace(' ', '')
|
unit = sheet[f'g{i}'].value.replace(' ', '')
|
||||||
count_safe = float(sheet[f'h{i}'].value) if sheet[f'h{i}'].value else None
|
count_safe = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None
|
||||||
unit_price = 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:
|
except Exception as e:
|
||||||
raise ParseError(f'{i}行物料信息错误: {e}')
|
raise ParseError(f'{i}行物料信息错误: {e}')
|
||||||
if type in [20, 30]:
|
if type in [20, 30]:
|
||||||
try:
|
try:
|
||||||
process = process_d[sheet[f'g{i}'].value.replace(' ', '')]
|
process = process_d[sheet[f'h{i}'].value.replace(' ', '')]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ParseError(f'{i}行物料信息错误: {e}')
|
raise ParseError(f'{i}行物料信息错误: {e}')
|
||||||
try:
|
try:
|
||||||
|
@ -87,7 +89,7 @@ def daoru_material(path: str):
|
||||||
filters['process'] = process
|
filters['process'] = process
|
||||||
default = {'type': type, 'name': name, 'specification': specification,
|
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(),
|
'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(
|
material, is_created = Material.objects.get_or_create(
|
||||||
**filters, defaults=default)
|
**filters, defaults=default)
|
||||||
if not is_created:
|
if not is_created:
|
||||||
|
@ -153,12 +155,12 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
raise ParseError('缺少步骤')
|
raise ParseError('缺少步骤')
|
||||||
r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
|
r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
|
||||||
first_route = r_qs.first()
|
first_route = r_qs.first()
|
||||||
last_route = r_qs.last()
|
|
||||||
if first_route.batch_bind:
|
if first_route.batch_bind:
|
||||||
first_route.batch_bind = False
|
first_route.batch_bind = False
|
||||||
first_route.save(update_fields=['batch_bind'])
|
first_route.save(update_fields=['batch_bind'])
|
||||||
if last_route.material_out != routepack.material:
|
# last_route = r_qs.last()
|
||||||
raise ParseError('最后一步产出与工艺包不一致')
|
# if last_route.material_out != routepack.material:
|
||||||
|
# raise ParseError('最后一步产出与工艺包不一致')
|
||||||
ticket_data = ticket.ticket_data
|
ticket_data = ticket.ticket_data
|
||||||
ticket_data.update({
|
ticket_data.update({
|
||||||
't_model': 'routepack',
|
't_model': 'routepack',
|
||||||
|
@ -169,8 +171,8 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
ticket.save()
|
ticket.save()
|
||||||
if routepack.ticket is None:
|
if routepack.ticket is None:
|
||||||
routepack.ticket = ticket
|
routepack.ticket = ticket
|
||||||
routepack.state = RoutePack.RP_S_AUDIT
|
routepack.state = RoutePack.RP_S_AUDIT
|
||||||
routepack.save()
|
routepack.save()
|
||||||
|
|
||||||
|
|
||||||
def routepack_audit_end(ticket: Ticket):
|
def routepack_audit_end(ticket: Ticket):
|
||||||
|
@ -180,7 +182,7 @@ def routepack_audit_end(ticket: Ticket):
|
||||||
|
|
||||||
def routepack_ticket_change(ticket: Ticket):
|
def routepack_ticket_change(ticket: Ticket):
|
||||||
routepack = RoutePack.objects.get(id=ticket.ticket_data['t_id'])
|
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.state = RoutePack.RP_S_CREATE
|
||||||
routepack.save()
|
routepack.save()
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer,
|
||||||
MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer,
|
MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer,
|
||||||
ShiftSerializer, TeamSerializer, ProcessSerializer,
|
ShiftSerializer, TeamSerializer, ProcessSerializer,
|
||||||
RouteSerializer, TeamMemberSerializer, RoutePackSerializer,
|
RouteSerializer, TeamMemberSerializer, RoutePackSerializer,
|
||||||
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer)
|
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer, MaterialExportSerializer)
|
||||||
from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days
|
from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin
|
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin
|
||||||
|
@ -21,6 +21,8 @@ from django.db.models import Q
|
||||||
from apps.wf.models import Ticket
|
from apps.wf.models import Ticket
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from apps.utils.export import export_excel
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
class MaterialViewSet(CustomModelViewSet):
|
class MaterialViewSet(CustomModelViewSet):
|
||||||
|
@ -32,14 +34,13 @@ class MaterialViewSet(CustomModelViewSet):
|
||||||
queryset = Material.objects.all()
|
queryset = Material.objects.all()
|
||||||
serializer_class = MaterialSerializer
|
serializer_class = MaterialSerializer
|
||||||
filterset_class = MaterialFilter
|
filterset_class = MaterialFilter
|
||||||
search_fields = ['name', 'code', 'number', 'specification', 'model']
|
search_fields = ['name', 'code', 'number', 'specification', 'model', 'bin_number_main']
|
||||||
select_related_fields = ['process']
|
select_related_fields = ['process']
|
||||||
ordering = ['name', 'model', 'specification',
|
ordering = ['name', 'model', 'specification',
|
||||||
'type', 'process', 'process__sort', 'sort', 'id', 'number']
|
'type', 'process', 'process__sort', 'sort', 'id', 'number']
|
||||||
ordering_fields = ['name', 'model', 'specification',
|
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):
|
def perform_destroy(self, instance):
|
||||||
from apps.inm.models import MaterialBatch
|
from apps.inm.models import MaterialBatch
|
||||||
if MaterialBatch.objects.filter(material=instance).exists():
|
if MaterialBatch.objects.filter(material=instance).exists():
|
||||||
|
@ -88,6 +89,23 @@ class MaterialViewSet(CustomModelViewSet):
|
||||||
def cates(self, request, *args, **kwargs):
|
def cates(self, request, *args, **kwargs):
|
||||||
res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct()
|
res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct()
|
||||||
return Response(set(res))
|
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):
|
class ShiftViewSet(ListModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -293,7 +311,7 @@ class RoutePackViewSet(CustomModelViewSet):
|
||||||
return Response({"id": route_new.id})
|
return Response({"id": route_new.id})
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action(methods=['post'], detail=True, permission_classes = [IsAdminUser], serializer_class=Serializer)
|
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer)
|
||||||
def toggle_state(self, request, *args, **kwargs):
|
def toggle_state(self, request, *args, **kwargs):
|
||||||
"""变更工艺路线状态
|
"""变更工艺路线状态
|
||||||
|
|
||||||
|
@ -349,12 +367,22 @@ class RouteViewSet(CustomModelViewSet):
|
||||||
select_related_fields = ['material',
|
select_related_fields = ['material',
|
||||||
'process', 'material_in', 'material_out', 'mgroup', 'routepack']
|
'process', 'material_in', 'material_out', 'mgroup', 'routepack']
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def perform_update(self, serializer):
|
||||||
obj:Route = self.get_object()
|
ins:Route = serializer.instance
|
||||||
routepack = obj.routepack
|
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:
|
if routepack and routepack.state != RoutePack.RP_S_CREATE:
|
||||||
raise ParseError('该状态下不可编辑')
|
raise ParseError('该工艺路线非创建中不可编辑')
|
||||||
return super().update(request, *args, **kwargs)
|
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):
|
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'
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
from apps.ofm.models import MroomBooking, BorrowRecord
|
||||||
|
|
||||||
|
from .models import LendingSeal
|
||||||
|
from apps.utils.filters import MyJsonListFilter
|
||||||
|
|
||||||
|
class MroomBookingFilterset(filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = MroomBooking
|
||||||
|
fields = {
|
||||||
|
'slot_b__mroom': ['exact', 'in'],
|
||||||
|
'slot_b__booking': ['exact'],
|
||||||
|
'slot_b__mdate': ['exact', 'gte', 'lte'],
|
||||||
|
'create_by': ['exact'],
|
||||||
|
"id": ["exact"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SealFilter(filters.FilterSet):
|
||||||
|
seal = MyJsonListFilter(label='按印章名称查询', field_name="seal")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = LendingSeal
|
||||||
|
fields = ['seal']
|
||||||
|
|
||||||
|
|
||||||
|
class BorrowRecordFilter(filters.FilterSet):
|
||||||
|
file_name = filters.CharFilter(label='按文件名称查询', field_name="borrow_file__name", lookup_expr='icontains')
|
||||||
|
borrow_user = filters.CharFilter(label='按借阅人查询', field_name="create_by__name", lookup_expr='icontains')
|
||||||
|
class Meta:
|
||||||
|
model = BorrowRecord
|
||||||
|
fields = ['file_name', 'borrow_user']
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-06-25 09:29
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Mroom',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.CharField(max_length=50, unique=True, verbose_name='会议室名称')),
|
||||||
|
('location', models.CharField(max_length=100, verbose_name='位置')),
|
||||||
|
('capacity', models.PositiveIntegerField(verbose_name='容纳人数')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MroomBooking',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('title', models.CharField(max_length=100, verbose_name='会议主题')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MroomSlot',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('mdate', models.DateField(db_index=True, verbose_name='会议日期')),
|
||||||
|
('slot', models.PositiveIntegerField(help_text='0-47', verbose_name='时段')),
|
||||||
|
('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_b', to='ofm.mroombooking')),
|
||||||
|
('mroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_m', to='ofm.mroom')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('mroom', 'mdate', 'slot')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-05 03:07
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0002_alter_state_filter_dept'),
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('ofm', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LendingSeal',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('seal', models.JSONField(default=list, help_text='{"seal_name": "印章名称"}', verbose_name='印章信息')),
|
||||||
|
('filename', models.TextField(verbose_name='文件名称')),
|
||||||
|
('file', models.TextField(verbose_name='文件内容')),
|
||||||
|
('file_count', models.PositiveIntegerField(verbose_name='用印份数')),
|
||||||
|
('is_lending', models.BooleanField(default=False, verbose_name='是否借出')),
|
||||||
|
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='联系方式')),
|
||||||
|
('lending_date', models.DateField(blank=True, null=True, verbose_name='借出日期')),
|
||||||
|
('return_date', models.DateField(blank=True, null=True, verbose_name='拟归还日期')),
|
||||||
|
('actual_return_date', models.DateField(blank=True, null=True, verbose_name='实际归还日期')),
|
||||||
|
('reason', models.CharField(blank=True, max_length=100, null=True, verbose_name='借用理由')),
|
||||||
|
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||||
|
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('submit_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seal_submit_user', to=settings.AUTH_USER_MODEL, verbose_name='提交人')),
|
||||||
|
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seal_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-08 03:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0002_lendingseal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='lendingseal',
|
||||||
|
name='submit_user',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-10 06:26
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('wf', '0002_alter_state_filter_dept'),
|
||||||
|
('ofm', '0003_remove_lendingseal_submit_user'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Vehicle',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('start_time', models.DateField(blank=True, null=True, verbose_name='出车时间')),
|
||||||
|
('end_time', models.DateField(blank=True, null=True, verbose_name='还车时间')),
|
||||||
|
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='出发地点')),
|
||||||
|
('destination', models.CharField(blank=True, max_length=100, null=True, verbose_name='到达地点')),
|
||||||
|
('start_km', models.PositiveIntegerField(verbose_name='出发公里数')),
|
||||||
|
('end_km', models.PositiveIntegerField(verbose_name='归还公里数')),
|
||||||
|
('actual_km', models.PositiveIntegerField(editable=False, verbose_name='实际行驶公里数')),
|
||||||
|
('is_city', models.BooleanField(default=True, verbose_name='是否市内用车')),
|
||||||
|
('reason', models.CharField(max_length=100, verbose_name='用车事由')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-10 06:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0004_vehicle'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vehicle',
|
||||||
|
name='via',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='途经地点'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-11 01:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
('ofm', '0005_vehicle_via'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vehicle',
|
||||||
|
name='belong_dept',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-11 06:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('ofm', '0006_vehicle_belong_dept'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='lendingseal',
|
||||||
|
name='seal',
|
||||||
|
field=models.JSONField(default=list, help_text='[公章,法人章,财务章,合同章,业务章,其他章]', verbose_name='印章信息'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FileRecord',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='资料名称')),
|
||||||
|
('number', models.CharField(blank=True, max_length=50, null=True, verbose_name='档案编号')),
|
||||||
|
('counts', models.CharField(blank=True, max_length=10, null=True, verbose_name='文件份数')),
|
||||||
|
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='存放位置')),
|
||||||
|
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='存档人电话')),
|
||||||
|
('reciver', models.CharField(blank=True, max_length=50, null=True, verbose_name='接收人(综合办)')),
|
||||||
|
('remark', models.TextField(blank=True, max_length=200, null=True, verbose_name='备注')),
|
||||||
|
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BorrowRecord',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('borrow_date', models.DateField(blank=True, null=True, verbose_name='借阅日期')),
|
||||||
|
('return_date', models.DateField(blank=True, null=True, verbose_name='归还日期')),
|
||||||
|
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='借阅人电话')),
|
||||||
|
('remark', models.JSONField(default=list, help_text=['借阅', '复印', '查阅'], verbose_name='用途')),
|
||||||
|
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||||
|
('borrow_file', models.ManyToManyField(related_name='borrow_records', to='ofm.FileRecord')),
|
||||||
|
('borrow_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='borrow_user', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-12 06:42
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0007_auto_20250911_1441'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='borrowrecord',
|
||||||
|
name='borrow_user',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-12 07:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0002_alter_state_filter_dept'),
|
||||||
|
('ofm', '0008_remove_borrowrecord_borrow_user'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='borrowrecord',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrow_ticket', to='wf.ticket', verbose_name='关联工单'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-19 01:21
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
('ofm', '0009_borrowrecord_ticket'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='lendingseal',
|
||||||
|
name='seal_other',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='其他印章'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Publicity',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('number', models.CharField(max_length=50, verbose_name='记录编号')),
|
||||||
|
('title', models.CharField(max_length=100, verbose_name='送审稿件标题')),
|
||||||
|
('participants', models.CharField(max_length=50, verbose_name='所有撰稿人')),
|
||||||
|
('level', models.JSONField(default=list, help_text=['重要', '一般', '非涉密'], verbose_name='用途')),
|
||||||
|
('content', models.JSONField(default=list, help_text=['武器装备科研生产综合事项', '其它'], verbose_name='稿件内容涉及')),
|
||||||
|
('other_content', models.CharField(blank=True, max_length=100, null=True, verbose_name='其它内容')),
|
||||||
|
('report_purpose', models.CharField(blank=True, max_length=100, null=True, verbose_name='宣传报道目的')),
|
||||||
|
('channel', models.JSONField(default=list, help_text=['互联网', '信息平台', '官微', '公开发行物', '其它'], verbose_name='发布渠道')),
|
||||||
|
('channel_other', models.CharField(blank=True, max_length=50, null=True, verbose_name='其它渠道')),
|
||||||
|
('other_channel', models.CharField(blank=True, max_length=50, null=True, verbose_name='其它渠道')),
|
||||||
|
('report_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='报道名称')),
|
||||||
|
('review', models.JSONField(default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], verbose_name='第一撰稿人自审')),
|
||||||
|
('dept_opinion', models.JSONField(default=list, help_text=['同意', '不同意'], verbose_name='部门负责人意见')),
|
||||||
|
('dept_opinion_review', models.CharField(blank=True, max_length=100, null=True, verbose_name='部门审查意见')),
|
||||||
|
('publicity_opinion', models.JSONField(default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], verbose_name='宣传统战部审查意见')),
|
||||||
|
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-24 05:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0010_auto_20250919_0921'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='channel_other',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='pfile',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='稿件路径'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='pub_dept',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='部室/研究院'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-24 06:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0003_workflow_view_path'),
|
||||||
|
('ofm', '0011_auto_20250924_1359'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_ticket', to='wf.ticket', verbose_name='关联工单'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-25 07:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0003_workflow_view_path'),
|
||||||
|
('ofm', '0012_publicity_ticket'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroomslot',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mrooms_ticket', to='wf.ticket', verbose_name='关联会议室'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-28 02:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0003_workflow_view_path'),
|
||||||
|
('ofm', '0013_mroomslot_ticket'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroombooking',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mrooms_ticket', to='wf.ticket', verbose_name='关联会议室'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroomslot',
|
||||||
|
name='is_inuse',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='是否占用'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='mroomslot',
|
||||||
|
unique_together={('mroom', 'mdate', 'slot', 'is_inuse')},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='mroomslot',
|
||||||
|
name='ticket',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-28 06:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0014_auto_20250928_1023'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vehicle',
|
||||||
|
name='end_km',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='归还公里数'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-29 07:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
('ofm', '0015_alter_vehicle_end_km'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroombooking',
|
||||||
|
name='belong_dept',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroombooking',
|
||||||
|
name='key_participants',
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name='主要参会领导'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroombooking',
|
||||||
|
name='note',
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name='备注'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mroombooking',
|
||||||
|
name='participant_count',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='参会人数'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-10 08:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0016_auto_20250929_1551'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='secret_period',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='秘密期限'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='level',
|
||||||
|
field=models.JSONField(default=list, help_text=['重要', '一般', '非涉密'], verbose_name='涉密等级'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-11 01:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0017_auto_20251010_1631'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='dept_opinion',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['同意', '不同意'], null=True, verbose_name='部门负责人意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='记录编号'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='publicity_opinion',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], null=True, verbose_name='宣传统战部审查意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='review',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True, verbose_name='第一撰稿人自审'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-11 03:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0018_auto_20251011_0922'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='dept_opinion',
|
||||||
|
field=models.JSONField(default=list, help_text=['同意', '不同意'], verbose_name='部门负责人意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='publicity_opinion',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], verbose_name='宣传统战部审查意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='review',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], verbose_name='第一撰稿人自审'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-11 06:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0019_auto_20251011_1128'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='dept_opinion',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['同意', '不同意'], null=True, verbose_name='部门负责人意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='publicity_opinion',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], null=True, verbose_name='宣传统战部审查意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='review',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True, verbose_name='第一撰稿人自审'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-13 01:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0020_auto_20251011_1427'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='publicity_opinion',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='宣传报道意见'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-17 06:50
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0021_alter_publicity_publicity_opinion'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='mroomslot',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-21 06:08
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0004_workflow_view_path2'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
('ofm', '0022_alter_mroomslot_unique_together'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PatentInfo',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='拟申请专利名称')),
|
||||||
|
('author', models.CharField(max_length=100, verbose_name='发明人(设计人)')),
|
||||||
|
('type', models.CharField(choices=[('invention', '发明专利'), ('utility', '实用新型专利'), ('design', '外观设计专利')], default='invention', max_length=50, verbose_name='专利类型')),
|
||||||
|
('is_public', models.BooleanField(default=False, verbose_name='是否公开')),
|
||||||
|
('area', models.CharField(choices=[('Domestic', '国内申请'), ('Foreign', '国外申请'), (' PCT', 'PCT申请')], default='Domestic', max_length=50, verbose_name='拟申请地域')),
|
||||||
|
('identified', models.BooleanField(default=False, verbose_name='是否进行过科技成果鉴定')),
|
||||||
|
('published_article', models.BooleanField(default=False, verbose_name='是否发表过文章')),
|
||||||
|
('exhibited', models.BooleanField(default=False, verbose_name='是否参与过展会展出')),
|
||||||
|
('applied_to_production', models.BooleanField(default=False, verbose_name='是否参与应用于生产/销售')),
|
||||||
|
('participated_in_exchange', models.BooleanField(default=False, verbose_name='是否参与过技术交流')),
|
||||||
|
('tech_background_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='技术背景材料页数')),
|
||||||
|
('tech_disclosure_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='技术交底材料页数')),
|
||||||
|
('novelty_report_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='查新检索报告页数')),
|
||||||
|
('diagrams_or_photos_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='图/照片页数或张数')),
|
||||||
|
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentInfo_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-22 02:05
|
||||||
|
|
||||||
|
import apps.ofm.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0023_patentinfo'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='applied_to_production',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='diagrams_or_photos_pages',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='exhibited',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='identified',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='novelty_report_pages',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='participated_in_exchange',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='published_article',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='tech_background_pages',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='tech_disclosure_pages',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='other_area',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='其它申请地域'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='tech_file',
|
||||||
|
field=models.JSONField(default=list, help_text='技术文件信息列表,每个条目包含name(名称)page(页数)字段', verbose_name='技术文件'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='patentinfo',
|
||||||
|
name='tech_status',
|
||||||
|
field=models.JSONField(blank=True, default=apps.ofm.models.PatentInfo.default_document_state, help_text='技术状态信息列表,每个条目包含name(名称)、status(状态)、file(文件)字段', verbose_name='技术状态'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='borrowrecord',
|
||||||
|
name='remark',
|
||||||
|
field=models.JSONField(default=list, help_text="['借阅', '复印', '查阅']", verbose_name='用途'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='channel',
|
||||||
|
field=models.JSONField(default=list, help_text="['互联网', '信息平台', '官微', '公开发行物', '其它']", verbose_name='发布渠道'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='content',
|
||||||
|
field=models.JSONField(default=list, help_text="['武器装备科研生产综合事项', '其它']", verbose_name='稿件内容涉及'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='dept_opinion',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text="['同意', '不同意']", null=True, verbose_name='部门负责人意见'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='level',
|
||||||
|
field=models.JSONField(default=list, help_text="['重要', '一般', '非涉密']", verbose_name='涉密等级'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicity',
|
||||||
|
name='review',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text="['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布']", null=True, verbose_name='第一撰稿人自审'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,333 @@
|
||||||
|
from django.db import models, transaction
|
||||||
|
from apps.utils.models import CommonADModel, BaseModel, CommonBDModel
|
||||||
|
from apps.system.models import User
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
from datetime import datetime
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
|
MTASK_CREATED = 10
|
||||||
|
MTASK_ASSGINED = 20
|
||||||
|
MTASK_STOP = 34
|
||||||
|
MTASK_SUBMIT = 40
|
||||||
|
MTASK_STATES = (
|
||||||
|
(MTASK_CREATED, '创建中'),
|
||||||
|
(MTASK_ASSGINED, '已下达'),
|
||||||
|
(MTASK_STOP, '已停止'),
|
||||||
|
(MTASK_SUBMIT, '已提交')
|
||||||
|
)
|
||||||
|
phone_validator = RegexValidator(r'^1[3456789]\d{9}$', '手机号码格式不正确')
|
||||||
|
|
||||||
|
|
||||||
|
class Mroom(CommonADModel):
|
||||||
|
"""TN: 会议室基本信息"""
|
||||||
|
name = models.CharField('会议室名称', max_length=50, unique=True)
|
||||||
|
location = models.CharField('位置', max_length=100)
|
||||||
|
capacity = models.PositiveIntegerField('容纳人数')
|
||||||
|
|
||||||
|
class MroomBooking(CommonBDModel):
|
||||||
|
"""TN: 会议室预定信息"""
|
||||||
|
# belong_dept 是预定部门
|
||||||
|
title = models.CharField('会议主题', max_length=100)
|
||||||
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联会议室',
|
||||||
|
on_delete=models.SET_NULL, related_name='mrooms_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
note = models.TextField('备注', null=True, blank=True)
|
||||||
|
participant_count = models.PositiveIntegerField('参会人数', default=0)
|
||||||
|
key_participants = models.TextField("主要参会领导", null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MroomSlot(BaseModel):
|
||||||
|
"""TN: 会议室时段"""
|
||||||
|
mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m")
|
||||||
|
booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b")
|
||||||
|
mdate = models.DateField('会议日期', db_index=True)
|
||||||
|
slot = models.PositiveIntegerField('时段', help_text='0-47')
|
||||||
|
is_inuse = models.BooleanField('是否占用', default=True)
|
||||||
|
|
||||||
|
|
||||||
|
# class Seal(BaseModel):
|
||||||
|
# """TN: 印章类型"""
|
||||||
|
# name = models.CharField('印章名称', max_length=50, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class LendingSeal(CommonBDModel):
|
||||||
|
"""TN: 印章外出用印信息"""
|
||||||
|
|
||||||
|
seal = models.JSONField('印章信息',default=list ,help_text='[公章,法人章,财务章,合同章,业务章,其他章]')
|
||||||
|
seal_other = models.CharField('其他印章', max_length=50, blank=True, null=True)
|
||||||
|
filename = models.TextField('文件名称')
|
||||||
|
file = models.TextField('文件内容')
|
||||||
|
file_count = models.PositiveIntegerField('用印份数')
|
||||||
|
is_lending= models.BooleanField('是否借出', default=False)
|
||||||
|
contacts = models.CharField('联系方式', max_length=50, validators=[phone_validator], blank=True, null=True)
|
||||||
|
lending_date = models.DateField('借出日期', blank=True, null=True)
|
||||||
|
return_date = models.DateField('拟归还日期', blank=True, null=True)
|
||||||
|
actual_return_date = models.DateField('实际归还日期', blank=True, null=True)
|
||||||
|
reason = models.CharField('借用理由', max_length=100, blank=True, null=True)
|
||||||
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
note = models.TextField('备注', null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Vehicle(CommonBDModel):
|
||||||
|
"""TN: 用车申请"""
|
||||||
|
start_time = models.DateField('出车时间', blank=True, null=True)
|
||||||
|
end_time = models.DateField('还车时间', blank=True, null=True)
|
||||||
|
location = models.CharField('出发地点', null=True, blank=True, max_length=100)
|
||||||
|
via = models.CharField('途经地点', null=True, blank=True, max_length=100)
|
||||||
|
destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
|
||||||
|
start_km = models.PositiveIntegerField('出发公里数')
|
||||||
|
end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True)
|
||||||
|
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
|
||||||
|
is_city = models.BooleanField('是否市内用车', default=True)
|
||||||
|
reason = models.CharField('用车事由', max_length=100)
|
||||||
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.end_km:
|
||||||
|
if self.start_km <= self.end_km:
|
||||||
|
self.actual_km = self.end_km - self.start_km
|
||||||
|
else:
|
||||||
|
raise ParseError('归还公里数不能小于出发公里数')
|
||||||
|
else:
|
||||||
|
self.actual_km = 0
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FileRecord(CommonBDModel):
|
||||||
|
"""TN: 档案台账"""
|
||||||
|
name = models.CharField('资料名称', max_length=100)
|
||||||
|
number = models.CharField('档案编号', max_length=50, null=True, blank=True)
|
||||||
|
counts = models.CharField('文件份数', max_length=10, null=True, blank=True)
|
||||||
|
location = models.CharField('存放位置', max_length=100, null=True, blank=True)
|
||||||
|
contacts = models.CharField('存档人电话', max_length=50, validators=[phone_validator], blank=True, null=True)
|
||||||
|
reciver = models.CharField('接收人(综合办)', max_length=50, null=True, blank=True)
|
||||||
|
remark = models.TextField('备注', max_length=200, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BorrowRecord(CommonBDModel):
|
||||||
|
"""TN: 借阅、复印、查阅记录"""
|
||||||
|
borrow_file = models.ManyToManyField(FileRecord, related_name="borrow_records")
|
||||||
|
borrow_date = models.DateField('借阅日期', null=True, blank=True)
|
||||||
|
return_date = models.DateField('归还日期', null=True, blank=True)
|
||||||
|
contacts = models.CharField('借阅人电话', max_length=50, validators=[phone_validator], null=True, blank=True)
|
||||||
|
remark = models.JSONField('用途', default=list, help_text=str(['借阅', '复印', '查阅']))
|
||||||
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.SET_NULL, related_name='borrow_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Publicity(CommonBDModel):
|
||||||
|
"""TN: 公示栏"""
|
||||||
|
number = models.CharField('记录编号', max_length=50, blank=True, null=True)
|
||||||
|
title = models.CharField('送审稿件标题', max_length=100)
|
||||||
|
participants = models.CharField('所有撰稿人', max_length=50)
|
||||||
|
pub_dept = models.CharField('部室/研究院', null=True, blank=True, max_length=50)
|
||||||
|
pfile = models.CharField('稿件路径', null=True, blank=True, max_length=100)
|
||||||
|
level = models.JSONField('涉密等级', default=list, help_text=str(['重要', '一般', '非涉密']))
|
||||||
|
content = models.JSONField('稿件内容涉及', default=list, help_text=str([
|
||||||
|
"武器装备科研生产综合事项",
|
||||||
|
"其它"
|
||||||
|
]))
|
||||||
|
other_content = models.CharField('其它内容', max_length=100, blank=True, null=True)
|
||||||
|
report_purpose = models.CharField('宣传报道目的', max_length=100, blank=True, null=True)
|
||||||
|
channel = models.JSONField('发布渠道', default=list, help_text=str(['互联网', '信息平台', '官微', '公开发行物', '其它']))
|
||||||
|
other_channel = models.CharField('其它渠道', max_length=50, blank=True, null=True)
|
||||||
|
report_name = models.CharField('报道名称', max_length=50, blank=True, null=True)
|
||||||
|
review = models.JSONField('第一撰稿人自审', default=list, help_text=str(['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布']), null=True,blank=True)
|
||||||
|
dept_opinion = models.JSONField('部门负责人意见', default=list, help_text=str(['同意', '不同意']), null=True, blank=True)
|
||||||
|
secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
|
||||||
|
dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
|
||||||
|
publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
|
||||||
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
|
||||||
|
# 记录编号自动生成
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.number:
|
||||||
|
last_number = self.__class__.objects.filter(number__startswith=f"GXKG-{datetime.now().year}-").order_by('-number').first()
|
||||||
|
if last_number:
|
||||||
|
try:
|
||||||
|
last_num = int(last_number.number.split('-')[-1])
|
||||||
|
except ValueError:
|
||||||
|
last_num = 0
|
||||||
|
else:
|
||||||
|
last_num =0
|
||||||
|
# 格式化编号,带补零
|
||||||
|
self.number = f"GXKG-{datetime.now().year}-{last_num+1:02d}"
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PatentInfo(CommonBDModel):
|
||||||
|
"""TN: 专利申密审批表单样式"""
|
||||||
|
PATENT_TYPE_CHOICES = (
|
||||||
|
('invention', '发明专利'),
|
||||||
|
('utility', '实用新型专利'),
|
||||||
|
('design', '外观设计专利'),
|
||||||
|
)
|
||||||
|
APPLY_AREAS = (
|
||||||
|
('Domestic', '国内申请'),
|
||||||
|
('Foreign', '国外申请'),
|
||||||
|
(' PCT', 'PCT申请'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def default_document_state(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
name = models.CharField('拟申请专利名称', max_length=100)
|
||||||
|
author = models.CharField('发明人(设计人)', max_length=100)
|
||||||
|
type = models.CharField('专利类型', max_length=50, choices=PATENT_TYPE_CHOICES, default='invention')
|
||||||
|
is_public = models.BooleanField('是否公开', default=False)
|
||||||
|
area = models.CharField('拟申请地域', max_length=50, choices=APPLY_AREAS, default='Domestic')
|
||||||
|
other_area = models.CharField('其它申请地域', max_length=50, blank=True, null=True)
|
||||||
|
tech_status = models.JSONField('技术状态', default=default_document_state, blank=True, help_text='技术状态信息列表,每个条目包含name(名称)、status(状态)、file(文件)字段')
|
||||||
|
tech_file = models.JSONField('技术文件', default=list, help_text='技术文件信息列表,每个条目包含name(名称)page(页数)字段')
|
||||||
|
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.SET_NULL, related_name='patentInfo_ticket', null=True, blank=True, db_constraint=False)
|
||||||
|
|
||||||
|
# class PaperOfm(CommonADModel):
|
||||||
|
# """TN: 论文申密审批表单"""
|
||||||
|
# PAPER_TYPE_CHOICES = (
|
||||||
|
# ('research', '研究论文'),
|
||||||
|
# ('comprehensive', '综合'),
|
||||||
|
# )
|
||||||
|
# name = models.CharField('拟申请专利名称', max_length=100)
|
||||||
|
# author = models.CharField('发明人(设计人)', max_length=100)
|
||||||
|
# paper_type = models.CharField('论文类型', max_length=50, choices=PAPER_TYPE_CHOICES, default='research')
|
||||||
|
# is_chinese_core = models.BooleanField('是否为中文核心', default=False)
|
||||||
|
# is_sci = models.BooleanField('是否被SCI/EI收录', default=False)
|
||||||
|
# has_appraisal = models.BooleanField('是否进行过科技成果鉴定', default=False)
|
||||||
|
# has_published_article = models.BooleanField('是否发表过文章', default=False)
|
||||||
|
# has_exhibited = models.BooleanField('是否参与过展会展出', default=False)
|
||||||
|
# has_applied_in_production = models.BooleanField('是否参与应用于生产/销售', default=False)
|
||||||
|
# has_technical_exchange = models.BooleanField('是否参与过技术交流', default=False)
|
||||||
|
# paper_page_count = models.PositiveIntegerField('论文页数', null=True, blank=True)
|
||||||
|
# image_count = models.PositiveIntegerField('图/照片张数', null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
# class Platform(CommonADModel):
|
||||||
|
# name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
# def __str__(self):
|
||||||
|
# return self.name
|
||||||
|
|
||||||
|
# class Project(CommonADModel):
|
||||||
|
# name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
# def __str__(self):
|
||||||
|
# return self.name
|
||||||
|
|
||||||
|
|
||||||
|
# class PatentRecord(CommonADModel):
|
||||||
|
# """TN: 专利台账登记"""
|
||||||
|
# volume_number = models.CharField(max_length=50, null=True, blank=True, verbose_name="卷号")
|
||||||
|
# application_number = models.CharField(max_length=50, verbose_name="申请号(交局后补登)")
|
||||||
|
# title = models.CharField(max_length=255, verbose_name="名称")
|
||||||
|
|
||||||
|
# patent_type = models.CharField(
|
||||||
|
# max_length=20,
|
||||||
|
# choices=[
|
||||||
|
# ("invention", "发明"),
|
||||||
|
# ("utility_model", "实用新型"),
|
||||||
|
# ("design", "外观设计")
|
||||||
|
# ],
|
||||||
|
# verbose_name="专利类型"
|
||||||
|
# )
|
||||||
|
# organization = models.CharField(max_length=100, verbose_name="单位")
|
||||||
|
# inventors = models.CharField(max_length=255, verbose_name="发明人")
|
||||||
|
# agent = models.CharField(max_length=255, null=True, blank=True, verbose_name="代理人")
|
||||||
|
# affiliated_platforms = models.ManyToManyField('Platform', blank=True, verbose_name="归属平台")
|
||||||
|
# affiliated_projects = models.ManyToManyField('Project', blank=True, verbose_name="归属项目")
|
||||||
|
# application_date = models.DateField(null=True, blank=True, verbose_name="申请日")
|
||||||
|
# authorization_date = models.DateField(null=True, blank=True, verbose_name="授权日")
|
||||||
|
# validity_years = models.IntegerField(null=True, blank=True, verbose_name="有效年限(年)")
|
||||||
|
# annuity_paid = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,verbose_name="年费缴纳")
|
||||||
|
|
||||||
|
# status = models.CharField(
|
||||||
|
# max_length=20,
|
||||||
|
# choices=[
|
||||||
|
# ("not_disclosed", "未公开"),
|
||||||
|
# ("under_examination", "实审中"),
|
||||||
|
# ("first_office_action", "一通"),
|
||||||
|
# ("second_office_action", "二通"),
|
||||||
|
# ("rejected", "驳回"),
|
||||||
|
# ("reexamination", "复审"),
|
||||||
|
# ("authorized", "授权")
|
||||||
|
# ],
|
||||||
|
# verbose_name="状态"
|
||||||
|
# )
|
||||||
|
# award_info = models.TextField(null=True, blank=True, verbose_name="报奖情况")
|
||||||
|
# bonus_amount = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,verbose_name="奖金金额(元)")
|
||||||
|
|
||||||
|
|
||||||
|
# class PaperRecord(models.Model):
|
||||||
|
# """TN: 论文台账登记"""
|
||||||
|
# index = models.PositiveIntegerField(verbose_name="序号")
|
||||||
|
# paper_code = models.CharField(max_length=100, blank=True, null=True, verbose_name="论文编号(投稿后补登)")
|
||||||
|
# title = models.CharField(max_length=255, verbose_name="名称")
|
||||||
|
# paper_type = models.CharField(max_length=100, verbose_name="论文类型")
|
||||||
|
# affiliation = models.CharField(max_length=255, verbose_name="单位")
|
||||||
|
# authors = models.CharField(max_length=255, verbose_name="作者")
|
||||||
|
# corresponding_author = models.CharField(max_length=255, blank=True, null=True, verbose_name="通讯作者")
|
||||||
|
# affiliated_platforms = models.ManyToManyField('Platform', blank=True, verbose_name="归属平台")
|
||||||
|
# affiliated_projects = models.ManyToManyField('Project', blank=True, verbose_name="归属项目")
|
||||||
|
# acceptance_date = models.DateField(blank=True, null=True, verbose_name="接受日期")
|
||||||
|
# publication_date = models.DateField(blank=True, null=True, verbose_name="发表日期")
|
||||||
|
# page_fee_paid = models.DecimalField(
|
||||||
|
# max_digits=10,
|
||||||
|
# decimal_places=2,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# verbose_name="版面费缴纳"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# status = models.CharField(
|
||||||
|
# max_length=50,
|
||||||
|
# choices=[
|
||||||
|
# ("under_review", "审稿中"),
|
||||||
|
# ("revise_1", "一修"),
|
||||||
|
# ("revise_2", "二修"),
|
||||||
|
# ("accepted", "接收"),
|
||||||
|
# ("published", "发表")
|
||||||
|
# ],
|
||||||
|
# default="under_review", verbose_name="状态"
|
||||||
|
# )
|
||||||
|
# award_status = models.CharField(max_length=255, blank=True, null=True, verbose_name="报奖情况")
|
||||||
|
# bonus_amount = models.DecimalField(max_digits=10,decimal_places=2,blank=True,null=True,verbose_name="奖金发放")
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectApproval(CommonBDModel):
|
||||||
|
# """TN: 立项审批表"""
|
||||||
|
# project_start_date = models.DateField("立项日期", null=True, blank=True)
|
||||||
|
# is_self_initiated = models.BooleanField("自立项目", default=False)
|
||||||
|
# is_city_level = models.BooleanField("市级项目", default=False)
|
||||||
|
# is_province_level = models.BooleanField("省级项目", default=False)
|
||||||
|
# construction_period = models.CharField("建设期", max_length=100, null=True, blank=True)
|
||||||
|
# project_members = models.TextField("项目组员", null=True, blank=True)
|
||||||
|
# project_budget = models.DecimalField("项目预算(万元)", max_digits=12, decimal_places=2, null=True, blank=True)
|
||||||
|
# project_description = models.TextField("项目基本情况", null=True, blank=True)
|
||||||
|
# project_performance = models.TextField("目标绩效", null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectInfo(CommonBDModel):
|
||||||
|
# """TN: 项目信息表
|
||||||
|
|
||||||
|
# """
|
||||||
|
# serial_number = models.CharField("序号", max_length=50, null=True, blank=True)
|
||||||
|
# red_head_doc_no = models.CharField("红头发文号/公示页", max_length=100, null=True, blank=True)
|
||||||
|
# name = models.CharField("名称", max_length=200, null=True, blank=True)
|
||||||
|
# project_type = models.CharField("项目类型", max_length=100, null=True, blank=True)
|
||||||
|
# platform = models.CharField("所属平台", max_length=100, null=True, blank=True)
|
||||||
|
# project_source = models.CharField("项目来源", max_length=100, null=True, blank=True)
|
||||||
|
# construction_period = models.CharField("建设期", max_length=100, null=True, blank=True)
|
||||||
|
# project_funding = models.DecimalField("项目资金(财政与自筹)", max_digits=15, decimal_places=2, null=True, blank=True)
|
||||||
|
# support_period = models.CharField("项目支持期", max_length=100, null=True, blank=True)
|
||||||
|
# undertaking_unit = models.CharField("承担单位", max_length=200, null=True, blank=True)
|
||||||
|
# responsible_person = models.CharField("负责人", max_length=50, null=True, blank=True)
|
||||||
|
|
||||||
|
# project_members = models.TextField("项目人员", null=True, blank=True)
|
||||||
|
# milestone = models.TextField("里程碑节点", null=True, blank=True)
|
||||||
|
# mid_term_status = models.TextField("项目中期情况", null=True, blank=True)
|
||||||
|
# acceptance_status = models.TextField("项目验收情况", null=True, blank=True)
|
||||||
|
# sci_tech_achievements = models.TextField("科技成果", null=True, blank=True)
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo)
|
||||||
|
# Publicity, PatetInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
|
||||||
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
from apps.utils.constants import EXCLUDE_FIELDS
|
||||||
|
from apps.wf.serializers import TicketSimpleSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class MroomSerializer(CustomModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Mroom
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class MroomBookingSerializer(CustomModelSerializer):
|
||||||
|
mroom = serializers.PrimaryKeyRelatedField(queryset=Mroom.objects.all(), write_only=True, label="会议室")
|
||||||
|
mdate = serializers.DateField(write_only=True, label="预订日期")
|
||||||
|
slots = serializers.ListField(child=serializers.IntegerField(), write_only=True, label="时段索引")
|
||||||
|
create_by_name = serializers.CharField(source='create_by.username', read_only=True)
|
||||||
|
create_by_phone = serializers.CharField(source='create_by.phone', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = MroomBooking
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
extra_kwargs = {'belong_dept': {'required': True}}
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
mroom = validated_data.pop('mroom')
|
||||||
|
slots = validated_data.pop('slots')
|
||||||
|
mdate = validated_data.pop('mdate')
|
||||||
|
booking = super().create(validated_data)
|
||||||
|
MroomSlot.objects.filter(booking=booking).delete()
|
||||||
|
for slot in slots:
|
||||||
|
if slot < 0 or slot > 47:
|
||||||
|
raise ParseError("时段索引超出范围")
|
||||||
|
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
|
||||||
|
if ms_exists:
|
||||||
|
raise ParseError("时段已预订,请刷新重选")
|
||||||
|
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
|
||||||
|
return booking
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
mroom = validated_data.pop('mroom')
|
||||||
|
slots = validated_data.pop('slots')
|
||||||
|
mdate = validated_data.pop('mdate')
|
||||||
|
booking = super().update(instance, validated_data)
|
||||||
|
MroomSlot.objects.filter(booking=instance).delete()
|
||||||
|
for slot in slots:
|
||||||
|
if slot < 0 or slot > 47:
|
||||||
|
raise ParseError("时段索引超出范围")
|
||||||
|
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
|
||||||
|
if ms_exists:
|
||||||
|
raise ParseError("时段已预订,请刷新重选")
|
||||||
|
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
|
||||||
|
return booking
|
||||||
|
|
||||||
|
|
||||||
|
class MroomSlotSerializer(CustomModelSerializer):
|
||||||
|
booking_title = serializers.CharField(source='booking.title', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = MroomSlot
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class LendingSealSerializer(CustomModelSerializer):
|
||||||
|
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = LendingSeal
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
class VehicleSerializer(CustomModelSerializer):
|
||||||
|
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Vehicle
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS + ['actual_km']
|
||||||
|
|
||||||
|
|
||||||
|
class FileRecordSerializer(CustomModelSerializer):
|
||||||
|
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = FileRecord
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
class BorrowRecordSerializer(CustomModelSerializer):
|
||||||
|
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
borrow_file = serializers.PrimaryKeyRelatedField(queryset=FileRecord.objects.all(), many=True, write_only=True, label="借阅文件")
|
||||||
|
file_detail = FileRecordSerializer(source='borrow_file', many=True, read_only=True, label="借阅文件详情")
|
||||||
|
file_name = serializers.SerializerMethodField()
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = BorrowRecord
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
def get_file_name(self, obj):
|
||||||
|
return [file.name for file in obj.borrow_file.all()]
|
||||||
|
|
||||||
|
|
||||||
|
class PublicitySerializer(CustomModelSerializer):
|
||||||
|
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Publicity
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
class PatentInfoSerializer(CustomModelSerializer):
|
||||||
|
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = PatentInfo
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
# class PaperSerializer(CustomModelSerializer):
|
||||||
|
# class Meta:
|
||||||
|
# model = PaperOfm
|
||||||
|
# fields = '__all__'
|
||||||
|
# read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
# class PlatformSerializer(serializers.ModelSerializer):
|
||||||
|
# class Meta:
|
||||||
|
# model = Platform
|
||||||
|
# fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectSerializer(serializers.ModelSerializer):
|
||||||
|
# class Meta:
|
||||||
|
# model = Project
|
||||||
|
# fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectMemberSerializer(CustomModelSerializer):
|
||||||
|
# affiliated_platforms = serializers.PrimaryKeyRelatedField(
|
||||||
|
# many=True,
|
||||||
|
# queryset=Platform.objects.all(),
|
||||||
|
# write_only=True
|
||||||
|
# )
|
||||||
|
# affiliated_platforms_detail = PlatformSerializer(
|
||||||
|
# source='affiliated_platforms', many=True, read_only=True
|
||||||
|
# )
|
||||||
|
|
||||||
|
# affiliated_projects = serializers.PrimaryKeyRelatedField(
|
||||||
|
# many=True,
|
||||||
|
# queryset=Project.objects.all(),
|
||||||
|
# write_only=True
|
||||||
|
# )
|
||||||
|
# affiliated_projects_detail = ProjectSerializer(
|
||||||
|
# source='affiliated_projects', many=True, read_only=True
|
||||||
|
# )
|
||||||
|
# class Meta:
|
||||||
|
# model = PatentRecord
|
||||||
|
# fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
# class PaperRecordSerializer(CustomModelSerializer):
|
||||||
|
# class Meta:
|
||||||
|
# model = PaperRecord
|
||||||
|
# fields = '__all__'
|
||||||
|
# read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectApprovalSerializer(CustomModelSerializer):
|
||||||
|
# class Meta:
|
||||||
|
# model = ProjectApproval
|
||||||
|
# fields = '__all__'
|
||||||
|
# read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectInfoSerializer(CustomModelSerializer):
|
||||||
|
# class Meta:
|
||||||
|
# model = ProjectInfo
|
||||||
|
# fields = '__all__'
|
||||||
|
# read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
|
||||||
|
from apps.wf.models import Ticket
|
||||||
|
# TicketFlow, Transition, Workflow, CustomField, State,
|
||||||
|
from apps.ofm.models import LendingSeal, Vehicle, BorrowRecord, Publicity, MroomBooking, MroomSlot, PatentInfo
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
|
|
||||||
|
def seal_submit_validate(ins: LendingSeal):
|
||||||
|
if ins.submit_time:
|
||||||
|
raise ParseError('该日志已提交!')
|
||||||
|
if ins.mtask and ins.mtask.state == LendingSeal.MTASK_STOP:
|
||||||
|
raise ParseError('该任务已停止!')
|
||||||
|
|
||||||
|
def bind_mroombooking(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = MroomBooking.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
ticket_data = ticket.ticket_data
|
||||||
|
ticket_data.update({
|
||||||
|
't_model': 'mroombooking',
|
||||||
|
't_id': ins.id,
|
||||||
|
})
|
||||||
|
ticket.ticket_data = ticket_data
|
||||||
|
ticket.create_by = ins.create_by
|
||||||
|
ticket.save()
|
||||||
|
if ins.ticket is None:
|
||||||
|
ins.ticket = ticket
|
||||||
|
ins.save()
|
||||||
|
|
||||||
|
def mroombooking_reject(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = MroomBooking.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
MroomSlot.objects.filter(booking=ins).update(is_inuse=False)
|
||||||
|
|
||||||
|
def bind_lendingseal(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = LendingSeal.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
ins.actual_return_date = None
|
||||||
|
ticket_data = ticket.ticket_data
|
||||||
|
ticket_data.update({
|
||||||
|
't_model': 'LendingSeal',
|
||||||
|
't_id': ins.id,
|
||||||
|
})
|
||||||
|
ticket.ticket_data = ticket_data
|
||||||
|
ticket.create_by = ins.create_by
|
||||||
|
ticket.save()
|
||||||
|
if ins.ticket is None:
|
||||||
|
ins.ticket = ticket
|
||||||
|
ins.save()
|
||||||
|
# 如果驳回到开始状态
|
||||||
|
|
||||||
|
def lending_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
|
||||||
|
try:
|
||||||
|
obj = LendingSeal.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
except LendingSeal.DoesNotExist:
|
||||||
|
raise ParseError("Publicity t_id 不存在")
|
||||||
|
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
|
||||||
|
|
||||||
|
for k, v in data_save.items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
def bind_vehicle(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = Vehicle.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
ticket_data = ticket.ticket_data
|
||||||
|
ticket_data.update({
|
||||||
|
't_model': 'Vehicle',
|
||||||
|
't_id': ins.id,
|
||||||
|
})
|
||||||
|
ins.actual_km = None
|
||||||
|
ins.end_time = None
|
||||||
|
ticket.ticket_data = ticket_data
|
||||||
|
ticket.create_by = ins.create_by
|
||||||
|
ticket.save()
|
||||||
|
if ins.ticket is None:
|
||||||
|
ins.ticket = ticket
|
||||||
|
ins.save()
|
||||||
|
|
||||||
|
def vehicle_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
|
||||||
|
try:
|
||||||
|
obj = Vehicle.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
except Vehicle.DoesNotExist:
|
||||||
|
raise ParseError("Publicity t_id 不存在")
|
||||||
|
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
|
||||||
|
|
||||||
|
for k, v in data_save.items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
def bind_file(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
ticket_data = ticket.ticket_data
|
||||||
|
ticket_data.update({
|
||||||
|
't_model': 'BorrowRecord',
|
||||||
|
't_id': ins.id,
|
||||||
|
})
|
||||||
|
ins.return_date = None
|
||||||
|
ticket.ticket_data = ticket_data
|
||||||
|
ticket.create_by = ins.create_by
|
||||||
|
ticket.save()
|
||||||
|
if ins.ticket is None:
|
||||||
|
ins.ticket = ticket
|
||||||
|
ins.save()
|
||||||
|
|
||||||
|
def file_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
|
||||||
|
try:
|
||||||
|
obj = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
except BorrowRecord.DoesNotExist:
|
||||||
|
raise ParseError("Publicity t_id 不存在")
|
||||||
|
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
|
||||||
|
|
||||||
|
for k, v in data_save.items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
def bind_publicity(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = Publicity.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
ticket_data = ticket.ticket_data
|
||||||
|
ticket_data.update({
|
||||||
|
't_model': 'publicity',
|
||||||
|
't_id': ins.id,
|
||||||
|
})
|
||||||
|
ins.dept_opinion = None
|
||||||
|
ins.secret_period = None
|
||||||
|
ins.dept_opinion_review = None
|
||||||
|
ins.publicity_opinion = None
|
||||||
|
ticket.ticket_data = ticket_data
|
||||||
|
ticket.create_by = ins.create_by
|
||||||
|
ticket.save()
|
||||||
|
if ins.ticket is None:
|
||||||
|
ins.ticket = ticket
|
||||||
|
ins.save()
|
||||||
|
|
||||||
|
def save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
|
||||||
|
try:
|
||||||
|
obj = Publicity.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
except Publicity.DoesNotExist:
|
||||||
|
raise ParseError("Publicity t_id 不存在")
|
||||||
|
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
|
||||||
|
|
||||||
|
for k, v in data_save.items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
def bind_patent(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
ins = PatentInfo.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
ticket_data = ticket.ticket_data
|
||||||
|
ticket_data.update({
|
||||||
|
't_model': 'patent',
|
||||||
|
't_id': ins.id,
|
||||||
|
})
|
||||||
|
ticket.ticket_data = ticket_data
|
||||||
|
ticket.create_by = ins.create_by
|
||||||
|
ticket.save()
|
||||||
|
if ins.ticket is None:
|
||||||
|
ins.ticket = ticket
|
||||||
|
ins.save()
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet,
|
||||||
|
FileborrowViewSet, PublicityViewSet, PatentInfoViewSet)
|
||||||
|
# SealModelViewSet,
|
||||||
|
# , PublicityViewSet, , PaperViewSet, PlatformViewSet,
|
||||||
|
# ProjectViewSet, PatentRecordViewSet, PaperRecordViewSet, ProjectApprovalViewSet, ProjectInfoViewSet)
|
||||||
|
|
||||||
|
API_BASE_URL = 'api/ofm/'
|
||||||
|
HTML_BASE_URL = 'dhtml/ofm/'
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register('mroom', MroomViewSet, basename='mroom')
|
||||||
|
router.register('mroombooking', MroomBookingViewSet, basename='mroombooking')
|
||||||
|
router.register('mroomslot', MroomSlotViewSet, basename='mroomslot')
|
||||||
|
# router.register('sealmanage', SealManageViewSet, basename='sealmanage')
|
||||||
|
router.register('lendingseal', LendingSealViewSet, basename='lendingseal')
|
||||||
|
router.register('vehicle', VehicleViewSet, basename='vehicle')
|
||||||
|
router.register('filerecord', FilerecordViewSet, basename='filerecord')
|
||||||
|
router.register('fileborrow', FileborrowViewSet, basename='fileborrow')
|
||||||
|
router.register('publicity', PublicityViewSet, basename='publicity')
|
||||||
|
router.register('patentinfo', PatentInfoViewSet, basename='patentinfo')
|
||||||
|
# router.register('paper', PaperViewSet, basename='paper')
|
||||||
|
# router.register('platform', PlatformViewSet, basename='platform')
|
||||||
|
# router.register('project', ProjectViewSet, basename='project')
|
||||||
|
# router.register('patentrecord', PatentRecordViewSet, basename='patentrecord')
|
||||||
|
# router.register('paperrecord', PaperRecordViewSet, basename='paperrecord')
|
||||||
|
# router.register('projectapproval', ProjectApprovalViewSet, basename='projectapproval')
|
||||||
|
# router.register('projectinfo', ProjectInfoViewSet, basename='projectinfo')
|
||||||
|
urlpatterns = [
|
||||||
|
path(API_BASE_URL, include(router.urls)),
|
||||||
|
]
|
|
@ -0,0 +1,259 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
|
||||||
|
from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo
|
||||||
|
# Publicity, , PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
|
||||||
|
from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer,
|
||||||
|
VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer, PatentInfoSerializer)
|
||||||
|
# ,SealSerializer,
|
||||||
|
# LendingSealSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer,
|
||||||
|
# PatentInfoSerializer, PaperSerializer, PlatformSerializer, ProjectSerializer, ProjectMemberSerializer, PaperRecordSerializer, ProjectApprovalSerializer, ProjectInfoSerializer)
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from apps.utils.mixins import CustomListModelMixin
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
from apps.ofm.filters import MroomBookingFilterset, SealFilter, BorrowRecordFilter
|
||||||
|
|
||||||
|
|
||||||
|
class MroomViewSet(CustomModelViewSet):
|
||||||
|
"""list: 会议室
|
||||||
|
|
||||||
|
会议室
|
||||||
|
"""
|
||||||
|
queryset = Mroom.objects.all()
|
||||||
|
serializer_class = MroomSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class MroomBookingViewSet(CustomModelViewSet):
|
||||||
|
"""list: 会议室预订
|
||||||
|
|
||||||
|
会议室预订
|
||||||
|
"""
|
||||||
|
queryset = MroomBooking.objects.all()
|
||||||
|
serializer_class = MroomBookingSerializer
|
||||||
|
select_related_fields = ["create_by", "ticket", "belong_dept"]
|
||||||
|
filterset_class = MroomBookingFilterset
|
||||||
|
|
||||||
|
def add_info_for_list(self, data):
|
||||||
|
booking_ids = [d["id"] for d in data]
|
||||||
|
slots = MroomSlot.objects.filter(booking__in=booking_ids).order_by("booking", "mroom", "mdate", "slot")
|
||||||
|
booking_info = {}
|
||||||
|
for slot in slots:
|
||||||
|
booking_id = slot.booking.id
|
||||||
|
|
||||||
|
if booking_id not in booking_info:
|
||||||
|
booking_info[booking_id] = {
|
||||||
|
"mdate": slot.mdate.strftime("%Y-%m-%d"), # 格式化日期
|
||||||
|
"mroom": slot.mroom.id,
|
||||||
|
"mroom_name": slot.mroom.name, # 会议室名称
|
||||||
|
"time_ranges": [], # 存储时间段(如 ["8:00-9:00", "10:00-11:30"])
|
||||||
|
"current_slots": [], # 临时存储连续的slot(用于合并)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查是否连续(当前slot是否紧接上一个slot)
|
||||||
|
current_slots = booking_info[booking_id]["current_slots"]
|
||||||
|
if not current_slots or slot.slot == current_slots[-1] + 1:
|
||||||
|
current_slots.append(slot.slot)
|
||||||
|
else:
|
||||||
|
# 如果不连续,先把当前连续的slot转换成时间段
|
||||||
|
if current_slots:
|
||||||
|
start_time = self._slot_to_time(current_slots[0])
|
||||||
|
end_time = self._slot_to_time(current_slots[-1] + 1)
|
||||||
|
booking_info[booking_id]["time_ranges"].append(f"{start_time}-{end_time}")
|
||||||
|
current_slots.clear()
|
||||||
|
current_slots.append(slot.slot)
|
||||||
|
|
||||||
|
# 处理最后剩余的连续slot
|
||||||
|
for info in booking_info.values():
|
||||||
|
if info["current_slots"]:
|
||||||
|
start_time = self._slot_to_time(info["current_slots"][0])
|
||||||
|
end_time = self._slot_to_time(info["current_slots"][-1] + 1)
|
||||||
|
info["time_ranges"].append(f"{start_time}-{end_time}")
|
||||||
|
info["slots"] = info.pop("current_slots") # 清理临时数据
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
item.update(booking_info.get(item["id"], {}))
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _slot_to_time(slot):
|
||||||
|
"""将slot (0-47) 转换为 HH:MM 格式的时间字符串"""
|
||||||
|
hours = slot // 2
|
||||||
|
minutes = (slot % 2) * 30
|
||||||
|
return f"{hours:02d}:{minutes:02d}"
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
ins:MroomBooking = self.get_object()
|
||||||
|
ticket = ins.ticket
|
||||||
|
if ticket is None or ticket.state.type == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ParseError("存在审批单,不允许修改")
|
||||||
|
if ins.create_by and ins.create_by != self.request.user:
|
||||||
|
raise ParseError("只允许创建者修改")
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if instance.create_by and instance.create_by != self.request.user:
|
||||||
|
raise ParseError("只允许创建者删除")
|
||||||
|
ticket = instance.ticket
|
||||||
|
if ticket is None or ticket.state.type == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ParseError("存在审批单,不允许删除")
|
||||||
|
if ticket:
|
||||||
|
ticket.delete()
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class MroomSlotViewSet(CustomListModelMixin, CustomGenericViewSet):
|
||||||
|
"""list:
|
||||||
|
|
||||||
|
会议室预订时段
|
||||||
|
"""
|
||||||
|
queryset = MroomSlot.objects.all()
|
||||||
|
serializer_class = MroomSlotSerializer
|
||||||
|
filterset_fields = ["mroom", "mdate", "booking", "is_inuse"]
|
||||||
|
|
||||||
|
|
||||||
|
class LendingSealViewSet(CustomModelViewSet):
|
||||||
|
"""list: 印章外出
|
||||||
|
|
||||||
|
印章外出
|
||||||
|
"""
|
||||||
|
perms_map = {'get': '*', 'post': 'seal.update',
|
||||||
|
'put': 'seal.update', 'delete': 'seal.delete'}
|
||||||
|
queryset = LendingSeal.objects.all()
|
||||||
|
serializer_class = LendingSealSerializer
|
||||||
|
filterset_class = SealFilter
|
||||||
|
ordering = ["-create_time"]
|
||||||
|
data_filter = True
|
||||||
|
|
||||||
|
|
||||||
|
class VehicleViewSet(CustomModelViewSet):
|
||||||
|
"""list: 车辆
|
||||||
|
|
||||||
|
车辆
|
||||||
|
"""
|
||||||
|
queryset = Vehicle.objects.all()
|
||||||
|
serializer_class = VehicleSerializer
|
||||||
|
ordering = ["-create_time"]
|
||||||
|
|
||||||
|
|
||||||
|
class FilerecordViewSet(CustomModelViewSet):
|
||||||
|
"""list: 文件
|
||||||
|
|
||||||
|
文件
|
||||||
|
"""
|
||||||
|
queryset = FileRecord.objects.all()
|
||||||
|
serializer_class = FileRecordSerializer
|
||||||
|
filterset_fields = [ "name", "number"]
|
||||||
|
ordering = ["-create_time", "number", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
class FileborrowViewSet(CustomModelViewSet):
|
||||||
|
"""list: 文件借阅
|
||||||
|
|
||||||
|
文件借阅
|
||||||
|
"""
|
||||||
|
queryset = BorrowRecord.objects.all()
|
||||||
|
serializer_class = BorrowRecordSerializer
|
||||||
|
filterset_class = BorrowRecordFilter
|
||||||
|
ordering = ["-create_time"]
|
||||||
|
|
||||||
|
|
||||||
|
class PublicityViewSet(CustomModelViewSet):
|
||||||
|
"""list: 公告
|
||||||
|
|
||||||
|
公告
|
||||||
|
"""
|
||||||
|
queryset = Publicity.objects.all()
|
||||||
|
serializer_class = PublicitySerializer
|
||||||
|
filterset_fields = ["title","number"]
|
||||||
|
ordering = ["-create_time", "number"]
|
||||||
|
|
||||||
|
|
||||||
|
class PatentInfoViewSet(CustomModelViewSet):
|
||||||
|
"""list: 专利
|
||||||
|
|
||||||
|
专利
|
||||||
|
"""
|
||||||
|
queryset = PatentInfo.objects.all()
|
||||||
|
serializer_class = PatentInfoSerializer
|
||||||
|
filterset_fields = ["name", "author", "type"]
|
||||||
|
ordering = ["-create_time", "name", "author", "type"]
|
||||||
|
|
||||||
|
|
||||||
|
# class PaperViewSet(CustomModelViewSet):
|
||||||
|
# """list: 论文申密审批
|
||||||
|
|
||||||
|
# 论文申密审批
|
||||||
|
# """
|
||||||
|
# queryset = PaperOfm.objects.all()
|
||||||
|
# serializer_class = PaperSerializer
|
||||||
|
# filterset_fields = ["name", "author"]
|
||||||
|
# ordering = ["create_time", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
# class PlatformViewSet(CustomModelViewSet):
|
||||||
|
# """list: 平台
|
||||||
|
|
||||||
|
# 平台
|
||||||
|
# """
|
||||||
|
# queryset = Platform.objects.all()
|
||||||
|
# serializer_class = PlatformSerializer
|
||||||
|
# filterset_fields = ["name"]
|
||||||
|
# ordering = ["create_time", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectViewSet(CustomModelViewSet):
|
||||||
|
# """list: 项目
|
||||||
|
|
||||||
|
# 项目
|
||||||
|
# """
|
||||||
|
# queryset = Project.objects.all()
|
||||||
|
# serializer_class = ProjectSerializer
|
||||||
|
# filterset_fields = ["name"]
|
||||||
|
# ordering = ["create_time", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
# class PatentRecordViewSet(CustomModelViewSet):
|
||||||
|
# """list: 专利台账登记
|
||||||
|
|
||||||
|
# 专利台账登记
|
||||||
|
# """
|
||||||
|
# queryset = PatentRecord.objects.all()
|
||||||
|
# serializer_class = ProjectMemberSerializer
|
||||||
|
# filterset_fields = ["patent", "type"]
|
||||||
|
# ordering = ["create_time", "patent", "type"]
|
||||||
|
|
||||||
|
|
||||||
|
# class PaperRecordViewSet(CustomModelViewSet):
|
||||||
|
# """list: 论文台账登记
|
||||||
|
|
||||||
|
# 论文台账登记
|
||||||
|
# """
|
||||||
|
# queryset = PaperRecord.objects.all()
|
||||||
|
# serializer_class = ProjectMemberSerializer
|
||||||
|
# filterset_fields = ["index", "title", "paper_code","paper_type", "authors"]
|
||||||
|
# ordering = ["create_time", "paper", "type"]
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectApprovalViewSet(CustomModelViewSet):
|
||||||
|
# """list: 立项审批表
|
||||||
|
|
||||||
|
# 立项审批表
|
||||||
|
# """
|
||||||
|
# queryset = ProjectApproval.objects.all()
|
||||||
|
# serializer_class = ProjectApprovalSerializer
|
||||||
|
# filterset_fields = ["project_start_date"]
|
||||||
|
# ordering = ["project_start_date"]
|
||||||
|
|
||||||
|
|
||||||
|
# class ProjectInfoViewSet(CustomModelViewSet):
|
||||||
|
# """list: 项目信息
|
||||||
|
|
||||||
|
# 项目信息
|
||||||
|
# """
|
||||||
|
# queryset = ProjectInfo.objects.all()
|
||||||
|
# serializer_class = ProjectInfoSerializer
|
||||||
|
# filterset_fields = ["serial_number", "name", "platform", "project_source"]
|
||||||
|
# ordering = ["serial_number", "name"]
|
|
@ -31,7 +31,6 @@ class UtaskSerializer(CustomModelSerializer):
|
||||||
"priority": {"required": False, "allow_null": True},
|
"priority": {"required": False, "allow_null": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if not validated_data.get('number', None):
|
if not validated_data.get('number', None):
|
||||||
validated_data["number"] = Utask.get_a_number()
|
validated_data["number"] = Utask.get_a_number()
|
||||||
|
|
|
@ -408,11 +408,14 @@ class PmService:
|
||||||
mtask.submit_time = now
|
mtask.submit_time = now
|
||||||
mtask.submit_user = user
|
mtask.submit_user = user
|
||||||
mtask.save()
|
mtask.save()
|
||||||
|
utask = mtask.utask
|
||||||
|
if utask:
|
||||||
|
cls.utask_submit(utask, raise_e=False)
|
||||||
else:
|
else:
|
||||||
raise ParseError('该任务状态不可提交')
|
raise ParseError('该任务状态不可提交')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def utask_submit(cls, utask: Utask):
|
def utask_submit(cls, utask: Utask, raise_e=True):
|
||||||
"""
|
"""
|
||||||
生产大任务提交
|
生产大任务提交
|
||||||
"""
|
"""
|
||||||
|
@ -420,4 +423,5 @@ class PmService:
|
||||||
utask.state = Utask.UTASK_SUBMIT
|
utask.state = Utask.UTASK_SUBMIT
|
||||||
utask.save()
|
utask.save()
|
||||||
else:
|
else:
|
||||||
raise ParseError('存在子任务未提交')
|
if raise_e:
|
||||||
|
raise ParseError('存在子任务未提交')
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Create your tasks here
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
from apps.pm.models import Mtask, Utask
|
||||||
|
from apps.utils.tasks import CustomTask
|
||||||
|
from celery import shared_task
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.db.models import F
|
||||||
|
from apps.pm.services import PmService
|
||||||
|
|
||||||
|
@shared_task(base=CustomTask)
|
||||||
|
def complete_mtask():
|
||||||
|
"""
|
||||||
|
将2天前未提交的任务且数量已达标的任务标记为已完成
|
||||||
|
"""
|
||||||
|
now = datetime.now().date()
|
||||||
|
Mtask.objects.filter(state=Mtask.MTASK_ASSGINED,
|
||||||
|
end_date__lte=now-timedelta(days=2),
|
||||||
|
count_ok__gte=F('count')).update(state=Mtask.MTASK_SUBMIT)
|
||||||
|
Mtask.objects.filter(state=Mtask.MTASK_ASSGINED,
|
||||||
|
end_date__lte=now-timedelta(days=7)).update(state=Mtask.MTASK_SUBMIT)
|
||||||
|
|
||||||
|
utasks = Utask.objects.filter(state__in=[Utask.UTASK_ASSGINED, Utask.UTASK_WORKING],
|
||||||
|
end_date__lte=now-timedelta(days=2))
|
||||||
|
|
||||||
|
for utask in utasks:
|
||||||
|
PmService.utask_submit(utask=utask, raise_e=False)
|
|
@ -43,7 +43,6 @@ class PuPlanItemSerializer(CustomModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
read_only_fields = EXCLUDE_FIELDS + ['pu_order']
|
read_only_fields = EXCLUDE_FIELDS + ['pu_order']
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
pu_plan = validated_data['pu_plan']
|
pu_plan = validated_data['pu_plan']
|
||||||
if pu_plan.state != PuPlan.PUPLAN_CREATE:
|
if pu_plan.state != PuPlan.PUPLAN_CREATE:
|
||||||
|
@ -65,7 +64,6 @@ class PuPlanItemSerializer(CustomModelSerializer):
|
||||||
else:
|
else:
|
||||||
validated_data['belong_dept'] = belong_dept
|
validated_data['belong_dept'] = belong_dept
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
validated_data.pop('pu_plan')
|
validated_data.pop('pu_plan')
|
||||||
pu_plan = instance.pu_plan
|
pu_plan = instance.pu_plan
|
||||||
|
@ -114,7 +112,6 @@ class PuOrderItemSerializer(CustomModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count']
|
read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count']
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
pu_order = validated_data['pu_order']
|
pu_order = validated_data['pu_order']
|
||||||
material = validated_data['material']
|
material = validated_data['material']
|
||||||
|
@ -126,7 +123,6 @@ class PuOrderItemSerializer(CustomModelSerializer):
|
||||||
PumService.cal_pu_order_total_price(pu_order)
|
PumService.cal_pu_order_total_price(pu_order)
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
validated_data.pop('material')
|
validated_data.pop('material')
|
||||||
validated_data.pop('pu_order')
|
validated_data.pop('pu_order')
|
||||||
|
|
|
@ -2,20 +2,24 @@ from rest_framework.exceptions import ValidationError
|
||||||
from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder
|
from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder
|
||||||
from django.db.models import F, Sum
|
from django.db.models import F, Sum
|
||||||
from apps.inm.models import MIO, MIOItem
|
from apps.inm.models import MIO, MIOItem
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
|
|
||||||
class PumService:
|
class PumService:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def cal_pu_order_total_price(puorder: PuOrder):
|
def cal_pu_order_total_price(puorder: PuOrder):
|
||||||
total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0
|
total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0
|
||||||
puorder.total_price = total_price
|
puorder.total_price = total_price
|
||||||
puorder.save()
|
puorder.save()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def cal_pu_plan_total_price(puplan: PuPlan):
|
def cal_pu_plan_total_price(puplan: PuPlan):
|
||||||
total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0
|
total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0
|
||||||
puplan.total_price = total_price
|
puplan.total_price = total_price
|
||||||
puplan.save()
|
puplan.save()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def change_puplan_state_when_puorder_sumbit(puorder: PuOrder):
|
def change_puplan_state_when_puorder_sumbit(puorder: PuOrder):
|
||||||
puplanIds = PuPlanItem.objects.filter(
|
puplanIds = PuPlanItem.objects.filter(
|
||||||
pu_order=puorder).values_list('pu_plan', flat=True)
|
pu_order=puorder).values_list('pu_plan', flat=True)
|
||||||
|
@ -29,24 +33,45 @@ class PumService:
|
||||||
puplan.state = state
|
puplan.state = state
|
||||||
puplan.save()
|
puplan.save()
|
||||||
|
|
||||||
def mio_purin(mio: MIO, is_reverse: bool = False):
|
@staticmethod
|
||||||
|
def mio_pur(mio: MIO, is_reverse: bool = False, mioitem: MIOItem = None):
|
||||||
"""
|
"""
|
||||||
采购入库成功后的操作
|
采购入库成功后的操作
|
||||||
"""
|
"""
|
||||||
pu_order = mio.pu_order
|
pu_order = mio.pu_order
|
||||||
if pu_order is None:
|
if pu_order is None:
|
||||||
return
|
return
|
||||||
for i in MIOItem.objects.filter(mio=mio):
|
if mioitem is None:
|
||||||
pu_orderitem = PuOrderItem.objects.get(
|
qs = MIOItem.objects.filter(mio=mio)
|
||||||
material=i.material, pu_order=pu_order)
|
else:
|
||||||
|
qs = MIOItem.objects.filter(id=mioitem.id)
|
||||||
|
|
||||||
|
|
||||||
|
if mio.type == MIO.MIO_TYPE_PUR_IN:
|
||||||
if is_reverse:
|
if is_reverse:
|
||||||
|
xtype = "out"
|
||||||
|
else:
|
||||||
|
xtype = "in"
|
||||||
|
elif mio.type == MIO.MIO_TYPE_PUR_OUT:
|
||||||
|
if is_reverse:
|
||||||
|
xtype = "in"
|
||||||
|
else:
|
||||||
|
xtype = "out"
|
||||||
|
|
||||||
|
for i in qs:
|
||||||
|
try:
|
||||||
|
pu_orderitem = PuOrderItem.objects.get(
|
||||||
|
material=i.material, pu_order=pu_order)
|
||||||
|
except PuOrderItem.DoesNotExist:
|
||||||
|
raise ParseError(f'{str(i.material)}-采购订单中不存在该物料')
|
||||||
|
if xtype == "out":
|
||||||
delivered_count = pu_orderitem.delivered_count - i.count
|
delivered_count = pu_orderitem.delivered_count - i.count
|
||||||
else:
|
else:
|
||||||
delivered_count = pu_orderitem.delivered_count + i.count
|
delivered_count = pu_orderitem.delivered_count + i.count
|
||||||
if delivered_count > pu_orderitem.count:
|
if delivered_count > pu_orderitem.count:
|
||||||
raise ValidationError(f'{i.material.name}-超出采购订单所需数量')
|
raise ValidationError(f'{str(i.material)}-超出采购订单所需数量')
|
||||||
elif delivered_count < 0:
|
elif delivered_count < 0:
|
||||||
raise ValidationError(f'{i.material.name}-数量小于0')
|
raise ValidationError(f'{str(i.material)}-数量小于0')
|
||||||
pu_orderitem.delivered_count = delivered_count
|
pu_orderitem.delivered_count = delivered_count
|
||||||
pu_orderitem.save()
|
pu_orderitem.save()
|
||||||
pu_order_state = PuOrder.PUORDER_SHIP
|
pu_order_state = PuOrder.PUORDER_SHIP
|
||||||
|
@ -69,3 +94,4 @@ class PumService:
|
||||||
if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE:
|
if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE:
|
||||||
puplan.state = PuPlan.PUPLAN_DONE
|
puplan.state = PuPlan.PUPLAN_DONE
|
||||||
puplan.save()
|
puplan.save()
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,6 @@ class PuPlanItemViewSet(CustomModelViewSet):
|
||||||
ordering_fields = ['create_time', 'material', 'need_date', 'need_count']
|
ordering_fields = ['create_time', 'material', 'need_date', 'need_count']
|
||||||
ordering = ['create_time']
|
ordering = ['create_time']
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
pu_plan = instance.pu_plan
|
pu_plan = instance.pu_plan
|
||||||
|
@ -104,7 +103,6 @@ class PuOrderViewSet(CustomModelViewSet):
|
||||||
search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model']
|
search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model']
|
||||||
select_related_fields = ['create_by', 'update_by', 'supplier']
|
select_related_fields = ['create_by', 'update_by', 'supplier']
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if instance.state != PuOrder.PUORDER_CREATE:
|
if instance.state != PuOrder.PUORDER_CREATE:
|
||||||
raise ParseError('采购订单非创建中不可删除')
|
raise ParseError('采购订单非创建中不可删除')
|
||||||
|
@ -145,7 +143,6 @@ class PuOrderItemViewSet(CustomModelViewSet):
|
||||||
filterset_fields = ['material', 'pu_order']
|
filterset_fields = ['material', 'pu_order']
|
||||||
ordering = ['create_time']
|
ordering = ['create_time']
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
pu_order = instance.pu_order
|
pu_order = instance.pu_order
|
||||||
if pu_order.state != PuOrder.PUORDER_CREATE:
|
if pu_order.state != PuOrder.PUORDER_CREATE:
|
||||||
|
|
|
@ -25,6 +25,8 @@ class QctFilter(filters.FilterSet):
|
||||||
"testitems": ["exact"],
|
"testitems": ["exact"],
|
||||||
"defects": ["exact"],
|
"defects": ["exact"],
|
||||||
"qctmat__material": ["exact"],
|
"qctmat__material": ["exact"],
|
||||||
|
"qctmat__use_for_in": ["exact"],
|
||||||
|
"qctmat__use_for_out": ["exact"],
|
||||||
"qctmat__tracing": ["exact"],
|
"qctmat__tracing": ["exact"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,18 +40,24 @@ class TestItemFilter(filters.FilterSet):
|
||||||
|
|
||||||
|
|
||||||
class FtestWorkFilter(filters.FilterSet):
|
class FtestWorkFilter(filters.FilterSet):
|
||||||
|
cbatch = filters.CharFilter(label='批次号', method='filter_cbatch')
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FtestWork
|
model = FtestWork
|
||||||
fields = {
|
fields = {
|
||||||
"material__process__name": ["exact", "contains"],
|
"material__process__name": ["exact", "contains"],
|
||||||
"material": ["exact"],
|
"material": ["exact"],
|
||||||
"wm": ["exact"],
|
"wm": ["exact", "isnull"],
|
||||||
"mb": ["exact"],
|
"mb": ["exact", "isnull"],
|
||||||
"batch": ["exact"],
|
"batch": ["exact"],
|
||||||
"type": ["exact"],
|
"type": ["exact"],
|
||||||
"type2": ["exact"],
|
"type2": ["exact"],
|
||||||
"shift": ["exact"]
|
"shift": ["exact"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def filter_cbatch(self, queryset, name, value):
|
||||||
|
qs1 = queryset.filter(wm__batch=value)
|
||||||
|
qs2 = queryset.filter(mb__batch=value)
|
||||||
|
return qs1.union(qs2)
|
||||||
|
|
||||||
class FtestFilter(filters.FilterSet):
|
class FtestFilter(filters.FilterSet):
|
||||||
wpr = filters.CharFilter(label="wprId", method="filter_wpr")
|
wpr = filters.CharFilter(label="wprId", method="filter_wpr")
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-07-18 07:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('qm', '0051_alter_ftestwork_batch'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='qctmat',
|
||||||
|
name='use_for_in',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='可用于消耗'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='qctmat',
|
||||||
|
name='use_for_out',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='可用于产出'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-05 01:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('qm', '0052_auto_20250718_1558'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ftest',
|
||||||
|
name='is_ok',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='是否合格'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-10 01:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('qm', '0053_alter_ftest_is_ok'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ptest',
|
||||||
|
name='val_xj',
|
||||||
|
field=models.CharField(blank=True, choices=[('S', '析晶'), ('R', '不析晶'), ('θ', '未化')], help_text=[('S', '析晶'), ('R', '不析晶'), ('θ', '未化')], max_length=10, null=True, verbose_name='析晶'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -172,9 +172,14 @@ class Qct(CommonAModel):
|
||||||
return QctMat.objects.filter(qct=self)
|
return QctMat.objects.filter(qct=self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, material:Material, tag:str):
|
def get(cls, material:Material, tag:str, type:str=None):
|
||||||
try:
|
try:
|
||||||
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag)
|
if type == "in":
|
||||||
|
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_in=True)
|
||||||
|
elif type == "out":
|
||||||
|
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_out=True)
|
||||||
|
else:
|
||||||
|
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag)
|
||||||
except Qct.DoesNotExist:
|
except Qct.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
qct = Qct.objects.get(name="默认检验表")
|
qct = Qct.objects.get(name="默认检验表")
|
||||||
|
@ -195,8 +200,12 @@ class Qct(CommonAModel):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_qs(cls, materialId:str, tag:str):
|
def get_qs(cls, materialId:str, tag:str, type:str):
|
||||||
qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag)
|
qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag)
|
||||||
|
if type == "in":
|
||||||
|
qct_qs = qct_qs.filter(qctmat__use_for_in=True)
|
||||||
|
elif type == "out":
|
||||||
|
qct_qs = qct_qs.filter(qctmat__use_for_out=True)
|
||||||
if not qct_qs.exists():
|
if not qct_qs.exists():
|
||||||
qct_qs = Qct.objects.filter(name="默认检验表")
|
qct_qs = Qct.objects.filter(name="默认检验表")
|
||||||
return qct_qs
|
return qct_qs
|
||||||
|
@ -224,6 +233,8 @@ class QctMat(BaseModel):
|
||||||
material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE)
|
material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE)
|
||||||
tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES,
|
tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES,
|
||||||
max_length=20, help_text=str(QC_TRACE_CHOICES))
|
max_length=20, help_text=str(QC_TRACE_CHOICES))
|
||||||
|
use_for_in = models.BooleanField("可用于消耗", default=True)
|
||||||
|
use_for_out = models.BooleanField("可用于产出", default=True)
|
||||||
max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True)
|
max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -326,7 +337,7 @@ class Ftest(CommonBDModel):
|
||||||
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user')
|
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user')
|
||||||
check_user = models.ForeignKey(
|
check_user = models.ForeignKey(
|
||||||
User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True)
|
User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True)
|
||||||
is_ok = models.BooleanField('是否合格', default=False)
|
is_ok = models.BooleanField('是否合格', default=True)
|
||||||
note = models.TextField('备注', default='', blank=True)
|
note = models.TextField('备注', default='', blank=True)
|
||||||
ftest_work = models.ForeignKey(
|
ftest_work = models.ForeignKey(
|
||||||
FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True)
|
FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
@ -342,13 +353,12 @@ class Ftest(CommonBDModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def init_by_qct(cls, qct, test_user, test_date):
|
def init_by_qct(cls, qct, test_user, test_date):
|
||||||
with transaction.atomic():
|
ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date)
|
||||||
ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date)
|
for testitem in qct.testitems.all():
|
||||||
for testitem in qct.testitems.all():
|
FtestItem.objects.create(ftest=ftest, testitem=testitem)
|
||||||
FtestItem.objects.create(ftest=ftest, testitem=testitem)
|
for defect in qct.defects.all():
|
||||||
for defect in qct.defects.all():
|
FtestDefect.objects.create(ftest=ftest, defect=defect)
|
||||||
FtestDefect.objects.create(ftest=ftest, defect=defect)
|
return ftest
|
||||||
return ftest
|
|
||||||
|
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def cal_count_notok(cls, ftestwork: FtestWork):
|
# def cal_count_notok(cls, ftestwork: FtestWork):
|
||||||
|
@ -435,7 +445,7 @@ class Ptest(CommonAModel):
|
||||||
val_tg = models.FloatField("Tg", help_text='℃', null=True, blank=True)
|
val_tg = models.FloatField("Tg", help_text='℃', null=True, blank=True)
|
||||||
val_tf = models.FloatField("Tf", help_text='℃', null=True, blank=True)
|
val_tf = models.FloatField("Tf", help_text='℃', null=True, blank=True)
|
||||||
val_xj = models.CharField(
|
val_xj = models.CharField(
|
||||||
'析晶', max_length=10, default='S', choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS))
|
'析晶', max_length=10, null=True, blank=True, choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS))
|
||||||
val_pzxs = models.FloatField(
|
val_pzxs = models.FloatField(
|
||||||
'膨胀系数', help_text='30-300℃', null=True, blank=True)
|
'膨胀系数', help_text='30-300℃', null=True, blank=True)
|
||||||
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)
|
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)
|
||||||
|
|
|
@ -37,6 +37,7 @@ class DefectSerializer(CustomModelSerializer):
|
||||||
class QctGetSerializer(serializers.Serializer):
|
class QctGetSerializer(serializers.Serializer):
|
||||||
material = serializers.CharField(label="物料ID")
|
material = serializers.CharField(label="物料ID")
|
||||||
tag = serializers.CharField(label="标签")
|
tag = serializers.CharField(label="标签")
|
||||||
|
type = serializers.CharField(label="类型(in/out)", required=False)
|
||||||
|
|
||||||
class TestItemSerializer(CustomModelSerializer):
|
class TestItemSerializer(CustomModelSerializer):
|
||||||
process_name = serializers.CharField(source="process.name", read_only=True)
|
process_name = serializers.CharField(source="process.name", read_only=True)
|
||||||
|
@ -53,6 +54,7 @@ class QctSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class QctTestItemSerializer(CustomModelSerializer):
|
class QctTestItemSerializer(CustomModelSerializer):
|
||||||
testitem_name = serializers.CharField(source='testitem.name', read_only=True)
|
testitem_name = serializers.CharField(source='testitem.name', read_only=True)
|
||||||
|
testitem_type = serializers.CharField(source='testitem.type', read_only=True)
|
||||||
testitem_description = serializers.CharField(source='testitem.description', read_only=True)
|
testitem_description = serializers.CharField(source='testitem.description', read_only=True)
|
||||||
testitem_field_type = serializers.CharField(source='testitem.field_type', read_only=True)
|
testitem_field_type = serializers.CharField(source='testitem.field_type', read_only=True)
|
||||||
testitem_choices = serializers.CharField(source='testitem.choices', read_only=True)
|
testitem_choices = serializers.CharField(source='testitem.choices', read_only=True)
|
||||||
|
@ -61,11 +63,11 @@ class QctTestItemSerializer(CustomModelSerializer):
|
||||||
model = QctTestItem
|
model = QctTestItem
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
def validate(self, attrs):
|
# def validate(self, attrs):
|
||||||
testitem:TestItem = attrs.get("testitem")
|
# testitem:TestItem = attrs.get("testitem")
|
||||||
if testitem.type != TestItem.T_TEST:
|
# if testitem.type != TestItem.T_TEST:
|
||||||
raise ParseError("只可选择检测项")
|
# raise ParseError("只可选择检测项")
|
||||||
return attrs
|
# return attrs
|
||||||
|
|
||||||
class QctDefectSerializer(CustomModelSerializer):
|
class QctDefectSerializer(CustomModelSerializer):
|
||||||
defect_name = serializers.CharField(source='defect.name', read_only=True)
|
defect_name = serializers.CharField(source='defect.name', read_only=True)
|
||||||
|
@ -213,34 +215,33 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
|
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
|
||||||
with transaction.atomic():
|
ins: FtestWork = super().create(validated_data)
|
||||||
ins: FtestWork = super().create(validated_data)
|
for ftestworkdefect in ftestworkdefect:
|
||||||
for ftestworkdefect in ftestworkdefect:
|
if ftestworkdefect['count'] > 0:
|
||||||
if ftestworkdefect['count'] > 0:
|
FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect)
|
||||||
FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect)
|
if ins.qct or ftestworkdefect:
|
||||||
if ins.qct or ftestworkdefect:
|
ins.cal_count()
|
||||||
ins.cal_count()
|
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
|
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
|
||||||
ins:FtestWork = super().update(instance, validated_data)
|
ins:FtestWork = super().update(instance, validated_data)
|
||||||
with transaction.atomic():
|
|
||||||
fd_ids = []
|
fd_ids = []
|
||||||
for item in ftestworkdefect:
|
for item in ftestworkdefect:
|
||||||
if item["count"] > 0:
|
if item["count"] > 0:
|
||||||
try:
|
try:
|
||||||
ins = FtestworkDefect.objects.get(ftestwork=ins, defect=item["defect"])
|
ins = FtestworkDefect.objects.get(ftestwork=ins, defect=item["defect"])
|
||||||
except FtestworkDefect.DoesNotExist:
|
except FtestworkDefect.DoesNotExist:
|
||||||
raise ParseError("新的缺陷项!")
|
raise ParseError("新的缺陷项!")
|
||||||
except FtestworkDefect.MultipleObjectsReturned:
|
except FtestworkDefect.MultipleObjectsReturned:
|
||||||
raise ParseError("缺陷项重复!")
|
raise ParseError("缺陷项重复!")
|
||||||
ins.count = item["count"]
|
ins.count = item["count"]
|
||||||
ins.save()
|
ins.save()
|
||||||
fd_ids.append(ins.id)
|
fd_ids.append(ins.id)
|
||||||
FtestworkDefect.objects.filter(ftestwork=ins).exclude(id__in=fd_ids).delete()
|
FtestworkDefect.objects.filter(ftestwork=ins).exclude(id__in=fd_ids).delete()
|
||||||
if ins.qct or ftestworkdefect:
|
if ins.qct or ftestworkdefect:
|
||||||
ins.cal_count()
|
ins.cal_count()
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
class FtestWorkSerializer(CustomModelSerializer):
|
class FtestWorkSerializer(CustomModelSerializer):
|
||||||
|
@ -290,24 +291,24 @@ class FtestSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ftestitems = validated_data.pop('ftestitems', [])
|
ftestitems = validated_data.pop('ftestitems', [])
|
||||||
with transaction.atomic():
|
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
for item in ftestitems:
|
for item in ftestitems:
|
||||||
FtestItem.objects.create(ftest=instance, **item)
|
FtestItem.objects.create(ftest=instance, **item)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
validated_data.pop('ftest_work', None)
|
validated_data.pop('ftest_work', None)
|
||||||
ftestitems = validated_data.pop('ftestitems', [])
|
ftestitems = validated_data.pop('ftestitems', [])
|
||||||
with transaction.atomic():
|
|
||||||
instance = super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
for item in ftestitems:
|
for item in ftestitems:
|
||||||
id = item.get('id', None)
|
id = item.get('id', None)
|
||||||
if id:
|
if id:
|
||||||
ftestitem = FtestItem.objects.get(id=id)
|
ftestitem = FtestItem.objects.get(id=id)
|
||||||
ftestitem.test_val = item['test_val']
|
ftestitem.test_val = item['test_val']
|
||||||
ftestitem.check_val = item['check_val']
|
ftestitem.check_val = item['check_val']
|
||||||
ftestitem.save()
|
ftestitem.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -362,75 +363,75 @@ class FtestProcessSerializer(CustomModelSerializer):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ftestitems = validated_data.pop('ftestitems', [])
|
ftestitems = validated_data.pop('ftestitems', [])
|
||||||
ftestdefects = validated_data.pop('ftestdefects', [])
|
ftestdefects = validated_data.pop('ftestdefects', [])
|
||||||
with transaction.atomic():
|
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
for item in ftestitems:
|
for item in ftestitems:
|
||||||
FtestItem.objects.create(ftest=instance, **item)
|
FtestItem.objects.create(ftest=instance, **item)
|
||||||
is_ok = True
|
is_ok = True
|
||||||
defect_main = None
|
defect_main = None
|
||||||
has_is_main = False
|
has_is_main = False
|
||||||
for item2 in ftestdefects:
|
for item2 in ftestdefects:
|
||||||
defect:Defect = item2["defect"]
|
defect:Defect = item2["defect"]
|
||||||
if defect.okcate in [Defect.DEFECT_NOTOK] and item2["has"]:
|
if defect.okcate in [Defect.DEFECT_NOTOK] and item2["has"]:
|
||||||
is_ok = False
|
is_ok = False
|
||||||
if not has_is_main:
|
if not has_is_main:
|
||||||
item2["is_main"] = True
|
item2["is_main"] = True
|
||||||
has_is_main = True
|
has_is_main = True
|
||||||
defect_main = defect
|
defect_main = defect
|
||||||
else:
|
else:
|
||||||
item2["is_main"] = False
|
item2["is_main"] = False
|
||||||
FtestDefect.objects.create(ftest=instance, **item2)
|
FtestDefect.objects.create(ftest=instance, **item2)
|
||||||
if not is_ok:
|
if not is_ok:
|
||||||
instance.defect_main = defect_main
|
instance.defect_main = defect_main
|
||||||
else:
|
else:
|
||||||
instance.defect_main = None
|
instance.defect_main = None
|
||||||
instance.is_ok = is_ok
|
instance.is_ok = is_ok
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ftestitems = validated_data.pop('ftestitems', [])
|
ftestitems = validated_data.pop('ftestitems', [])
|
||||||
ftestdefects = validated_data.pop('ftestdefects', [])
|
ftestdefects = validated_data.pop('ftestdefects', [])
|
||||||
with transaction.atomic():
|
|
||||||
instance = super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
for item in ftestitems:
|
for item in ftestitems:
|
||||||
try:
|
try:
|
||||||
ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance)
|
ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance)
|
||||||
except FtestItem.DoesNotExist:
|
except FtestItem.DoesNotExist:
|
||||||
raise ParseError("新的检测项!")
|
ins = FtestItem.objects.create(ftest=instance, **item)
|
||||||
for k, v in item.items():
|
for k, v in item.items():
|
||||||
setattr(ins, k, v)
|
setattr(ins, k, v)
|
||||||
ins.save()
|
ins.save()
|
||||||
is_ok = True
|
is_ok = True
|
||||||
defect_main = None
|
defect_main = None
|
||||||
has_is_main = False
|
has_is_main = False
|
||||||
for item2 in ftestdefects:
|
for item2 in ftestdefects:
|
||||||
try:
|
try:
|
||||||
ins:FtestDefect = FtestDefect.objects.get(ftest=instance, defect=item2["defect"])
|
ins:FtestDefect = FtestDefect.objects.get(ftest=instance, defect=item2["defect"])
|
||||||
except FtestDefect.MultipleObjectsReturned:
|
except FtestDefect.MultipleObjectsReturned:
|
||||||
myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}")
|
myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}")
|
||||||
raise ParseError("获取到重复的缺陷项!")
|
raise ParseError("获取到重复的缺陷项!")
|
||||||
except FtestDefect.DoesNotExist:
|
except FtestDefect.DoesNotExist:
|
||||||
raise ParseError("新的缺陷项!")
|
ins = FtestDefect.objects.create(ftest=instance, **item2)
|
||||||
for k, v in item2.items():
|
for k, v in item2.items():
|
||||||
setattr(ins, k, v)
|
setattr(ins, k, v)
|
||||||
ins.save()
|
ins.save()
|
||||||
if ins.is_main:
|
if ins.is_main:
|
||||||
|
has_is_main = True
|
||||||
|
defect_main = ins.defect
|
||||||
|
if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]:
|
||||||
|
is_ok = False
|
||||||
|
if not has_is_main:
|
||||||
|
ins.is_main = True
|
||||||
has_is_main = True
|
has_is_main = True
|
||||||
defect_main = ins.defect
|
defect_main = ins.defect
|
||||||
if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]:
|
else:
|
||||||
is_ok = False
|
ins.is_main = False
|
||||||
if not has_is_main:
|
ins.save()
|
||||||
ins.is_main = True
|
if not is_ok:
|
||||||
has_is_main = True
|
instance.defect_main = defect_main
|
||||||
defect_main = ins.defect
|
else:
|
||||||
else:
|
instance.defect_main = None
|
||||||
ins.is_main = False
|
instance.is_ok = is_ok
|
||||||
ins.save()
|
instance.save()
|
||||||
if not is_ok:
|
|
||||||
instance.defect_main = defect_main
|
|
||||||
else:
|
|
||||||
instance.defect_main = None
|
|
||||||
instance.is_ok = is_ok
|
|
||||||
instance.save()
|
|
||||||
return instance
|
return instance
|
|
@ -6,6 +6,7 @@ from django.utils import timezone
|
||||||
from apps.wf.models import Ticket
|
from apps.wf.models import Ticket
|
||||||
from apps.qm.models import NotOkOption, Defect
|
from apps.qm.models import NotOkOption, Defect
|
||||||
from apps.wpm.services_2 import ana_batch_thread
|
from apps.wpm.services_2 import ana_batch_thread
|
||||||
|
from apps.inm.models import MaterialBatch
|
||||||
|
|
||||||
def ftestwork_submit_validate(ins: FtestWork):
|
def ftestwork_submit_validate(ins: FtestWork):
|
||||||
wm:WMaterial = ins.wm
|
wm:WMaterial = ins.wm
|
||||||
|
@ -23,8 +24,8 @@ def ftestwork_submit_validate(ins: FtestWork):
|
||||||
def ftestwork_submit(ins:FtestWork, user: User):
|
def ftestwork_submit(ins:FtestWork, user: User):
|
||||||
wm:WMaterial = ins.wm
|
wm:WMaterial = ins.wm
|
||||||
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
|
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
|
||||||
if ins.need_update_wm:
|
if wm and ins.need_update_wm:
|
||||||
if ins.qct is None or not fwd_qs.exists():
|
if ins.qct is None and not fwd_qs.exists():
|
||||||
if wm.state == WMaterial.WM_TEST:
|
if wm.state == WMaterial.WM_TEST:
|
||||||
# 更新对应的车间库存
|
# 更新对应的车间库存
|
||||||
wm.count = wm.count - ins.count
|
wm.count = wm.count - ins.count
|
||||||
|
@ -99,7 +100,7 @@ def ftestwork_submit(ins:FtestWork, user: User):
|
||||||
else:
|
else:
|
||||||
wm:WMaterial = ins.wm
|
wm:WMaterial = ins.wm
|
||||||
# 此时调用了qct表
|
# 此时调用了qct表
|
||||||
for item in FtestworkDefect.objects.filter(ftestwork=ins):
|
for item in fwd_qs:
|
||||||
item:FtestworkDefect = item
|
item:FtestworkDefect = item
|
||||||
if item.count > 0:
|
if item.count > 0:
|
||||||
wm.count = wm.count - item.count
|
wm.count = wm.count - item.count
|
||||||
|
@ -124,9 +125,37 @@ def ftestwork_submit(ins:FtestWork, user: User):
|
||||||
if not new_create:
|
if not new_create:
|
||||||
wmx.count = wmx.count + item.count
|
wmx.count = wmx.count + item.count
|
||||||
wmx.save()
|
wmx.save()
|
||||||
|
|
||||||
|
if ins.mb:
|
||||||
|
mb:MaterialBatch = ins.mb
|
||||||
|
for item in fwd_qs:
|
||||||
|
item:FtestworkDefect = item
|
||||||
|
if item.count > 0:
|
||||||
|
mb.count = mb.count - item.count
|
||||||
|
if mb.count < 0:
|
||||||
|
raise ParseError("数量不足,扣减失败")
|
||||||
|
mb.save()
|
||||||
|
mbstate = WMaterial.WM_OK
|
||||||
|
if item.defect.okcate == Defect.DEFECT_NOTOK:
|
||||||
|
mbstate = WMaterial.WM_NOTOK
|
||||||
|
mbx, new_create = MaterialBatch.objects.get_or_create(
|
||||||
|
material=mb.material,
|
||||||
|
warehouse=mb.warehouse,
|
||||||
|
batch=mb.batch,
|
||||||
|
defect=item.defect,
|
||||||
|
state=mbstate,
|
||||||
|
defaults={
|
||||||
|
'count': item.count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not new_create:
|
||||||
|
mbx.count = mbx.count + item.count
|
||||||
|
mbx.save()
|
||||||
ins.submit_user = user
|
ins.submit_user = user
|
||||||
ins.submit_time = timezone.now()
|
ins.submit_time = timezone.now()
|
||||||
ins.save()
|
ins.save()
|
||||||
|
# 触发批次统计分析
|
||||||
|
ana_batch_thread(xbatchs=[ins.batch])
|
||||||
|
|
||||||
def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
|
def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
ins = FtestWork.objects.get(id=new_ticket_data['t_id'])
|
ins = FtestWork.objects.get(id=new_ticket_data['t_id'])
|
||||||
|
@ -147,5 +176,4 @@ def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
|
||||||
|
|
||||||
def ftestwork_audit_end(ticket: Ticket):
|
def ftestwork_audit_end(ticket: Ticket):
|
||||||
ins = FtestWork.objects.get(id=ticket.ticket_data['t_id'])
|
ins = FtestWork.objects.get(id=ticket.ticket_data['t_id'])
|
||||||
ftestwork_submit(ins, ticket.create_by)
|
ftestwork_submit(ins, ticket.create_by)
|
||||||
ana_batch_thread(xbatchs=[ins.batch])
|
|
|
@ -33,7 +33,6 @@ class DefectViewSet(CustomModelViewSet):
|
||||||
filterset_fields = ["cate", "okcate"]
|
filterset_fields = ["cate", "okcate"]
|
||||||
search_fields = ["name", "code"]
|
search_fields = ["name", "code"]
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
QctDefect.objects.filter(defect=instance).delete()
|
QctDefect.objects.filter(defect=instance).delete()
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
@ -63,8 +62,10 @@ class QctViewSet(CustomModelViewSet):
|
||||||
sr = QctGetSerializer(data=request.data)
|
sr = QctGetSerializer(data=request.data)
|
||||||
sr.is_valid(raise_exception=True)
|
sr.is_valid(raise_exception=True)
|
||||||
vdata = sr.validated_data
|
vdata = sr.validated_data
|
||||||
qct = Qct.get(vdata["material"], vdata["tag"])
|
qct = Qct.get(vdata["material"], vdata["tag"], vdata.get("type", None))
|
||||||
return Response(QctDetailSerializer(instance=qct).data)
|
if qct:
|
||||||
|
return Response(QctDetailSerializer(instance=qct).data)
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class QctTestItemViewSet(CustomModelViewSet):
|
class QctTestItemViewSet(CustomModelViewSet):
|
||||||
|
@ -91,7 +92,6 @@ class QctDefectViewSet(CustomModelViewSet):
|
||||||
filterset_fields = ["qct", "defect"]
|
filterset_fields = ["qct", "defect"]
|
||||||
ordering = ["qct", "sort"]
|
ordering = ["qct", "sort"]
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
ins: QctDefect = serializer.save()
|
ins: QctDefect = serializer.save()
|
||||||
if ins.is_default:
|
if ins.is_default:
|
||||||
|
@ -149,7 +149,6 @@ class TestItemViewSet(CustomModelViewSet):
|
||||||
item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects])
|
item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
QctTestItem.objects.filter(testitem=instance).delete()
|
QctTestItem.objects.filter(testitem=instance).delete()
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
@ -236,19 +235,16 @@ class FtestViewSet(CustomModelViewSet):
|
||||||
ftest_work.count_notok = all_count - ok_count
|
ftest_work.count_notok = all_count - ok_count
|
||||||
ftest_work.save()
|
ftest_work.save()
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
ins: Ftest = serializer.save()
|
ins: Ftest = serializer.save()
|
||||||
if ins.ftest_work:
|
if ins.ftest_work:
|
||||||
self.count_sampling(ins.ftest_work)
|
self.count_sampling(ins.ftest_work)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
ins: Ftest = serializer.save()
|
ins: Ftest = serializer.save()
|
||||||
if ins.ftest_work:
|
if ins.ftest_work:
|
||||||
self.count_sampling(ins.ftest_work)
|
self.count_sampling(ins.ftest_work)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
ftest_work = instance.ftest_work
|
ftest_work = instance.ftest_work
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
@ -282,27 +278,32 @@ class FtestWorkViewSet(CustomModelViewSet):
|
||||||
select_related_fields = ['material', 'mb', 'mb__material']
|
select_related_fields = ['material', 'mb', 'mb__material']
|
||||||
filterset_class = FtestWorkFilter
|
filterset_class = FtestWorkFilter
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
ins:FtestWork = self.get_object()
|
ins:FtestWork = self.get_object()
|
||||||
|
partial = kwargs.pop('partial', False)
|
||||||
if ins.submit_time is not None:
|
if ins.submit_time is not None:
|
||||||
raise ParseError('已提交无法修改')
|
raise ParseError('已提交无法修改')
|
||||||
if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START:
|
if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START:
|
||||||
raise ParseError('审批单已进行,无法修改')
|
raise ParseError('审批单已进行,无法修改')
|
||||||
x = super().update(request, *args, **kwargs)
|
serializer = self.get_serializer(ins, data=request.data, partial=partial)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
self.perform_update(serializer)
|
||||||
# 触发批次统计分析
|
# 触发批次统计分析
|
||||||
ana_batch_thread(xbatchs=[ins.batch])
|
ana_batch_thread(xbatchs=[ins.batch])
|
||||||
return x
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
ins:FtestWork = self.get_object()
|
ins:FtestWork = self.get_object()
|
||||||
if ins.submit_time is not None:
|
if ins.submit_time is not None:
|
||||||
raise ParseError('已提交无法删除')
|
raise ParseError('已提交无法删除')
|
||||||
if ins.ticket:
|
if ins.ticket:
|
||||||
raise ParseError('存在审批, 无法删除')
|
raise ParseError('存在审批, 无法删除')
|
||||||
x = super().destroy(request, *args, **kwargs)
|
self.perform_destroy(ins)
|
||||||
# 触发批次统计分析
|
# 触发批次统计分析
|
||||||
ana_batch_thread(xbatchs=[ins.batch])
|
ana_batch_thread(xbatchs=[ins.batch])
|
||||||
return x
|
return Response(status=204)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
ins = serializer.save()
|
ins = serializer.save()
|
||||||
|
@ -320,8 +321,8 @@ class FtestWorkViewSet(CustomModelViewSet):
|
||||||
ins:FtestWork = self.get_object()
|
ins:FtestWork = self.get_object()
|
||||||
if ins.ticket:
|
if ins.ticket:
|
||||||
raise ParseError('该检验工作存在审批!')
|
raise ParseError('该检验工作存在审批!')
|
||||||
if ins.wm is None:
|
if ins.wm is None and ins.mb is None:
|
||||||
raise ParseError('该检验工作未关联车间库存')
|
raise ParseError('该检验工作未关联库存')
|
||||||
if ins.submit_time is None:
|
if ins.submit_time is None:
|
||||||
ftestwork_submit(ins, request.user)
|
ftestwork_submit(ins, request.user)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -65,7 +65,6 @@ class OrderViewSet(CustomModelViewSet):
|
||||||
"state": ["exact", "in"],
|
"state": ["exact", "in"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
if instance.state != Order.ORDER_CREATE:
|
if instance.state != Order.ORDER_CREATE:
|
||||||
raise ParseError('订单非创建中不可删除')
|
raise ParseError('订单非创建中不可删除')
|
||||||
|
|
|
@ -275,13 +275,11 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
|
||||||
model = Dept
|
model = Dept
|
||||||
exclude = EXCLUDE_FIELDS + ['third_info']
|
exclude = EXCLUDE_FIELDS + ['third_info']
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ins = super().create(validated_data)
|
ins = super().create(validated_data)
|
||||||
sync_dahua_dept(ins)
|
sync_dahua_dept(ins)
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ins = super().update(instance, validated_data)
|
ins = super().update(instance, validated_data)
|
||||||
sync_dahua_dept(ins)
|
sync_dahua_dept(ins)
|
||||||
|
|
|
@ -8,8 +8,7 @@ from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
|
||||||
from django_celery_results.models import TaskResult
|
from django_celery_results.models import TaskResult
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
|
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
|
||||||
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
|
from rest_framework.mixins import RetrieveModelMixin
|
||||||
ListModelMixin, RetrieveModelMixin)
|
|
||||||
from rest_framework.parsers import (JSONParser,
|
from rest_framework.parsers import (JSONParser,
|
||||||
MultiPartParser)
|
MultiPartParser)
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
@ -20,7 +19,7 @@ from apps.hrm.models import Employee
|
||||||
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
|
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
|
||||||
from apps.system.filters import DeptFilterSet, UserFilterSet
|
from apps.system.filters import DeptFilterSet, UserFilterSet
|
||||||
# from django_q.models import Task as QTask, Schedule as QSchedule
|
# from django_q.models import Task as QTask, Schedule as QSchedule
|
||||||
from apps.utils.mixins import (CustomCreateModelMixin, MyLoggingMixin)
|
from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from apps.utils.permission import ALL_PERMS
|
from apps.utils.permission import ALL_PERMS
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
|
@ -229,7 +228,7 @@ class PTaskViewSet(CustomModelViewSet):
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
list:任务执行结果列表
|
list:任务执行结果列表
|
||||||
|
|
||||||
|
@ -373,7 +372,7 @@ class RoleViewSet(CustomModelViewSet):
|
||||||
ordering = ['create_time']
|
ordering = ['create_time']
|
||||||
|
|
||||||
|
|
||||||
class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
|
class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
||||||
"""岗位/角色关系
|
"""岗位/角色关系
|
||||||
|
|
||||||
岗位/角色关系
|
岗位/角色关系
|
||||||
|
@ -385,7 +384,7 @@ class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
|
||||||
filterset_fields = ['post', 'role']
|
filterset_fields = ['post', 'role']
|
||||||
|
|
||||||
|
|
||||||
class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
|
class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
||||||
"""用户/岗位关系
|
"""用户/岗位关系
|
||||||
|
|
||||||
用户/岗位关系
|
用户/岗位关系
|
||||||
|
@ -398,37 +397,13 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
|
||||||
ordering = ['sort', 'create_time']
|
ordering = ['sort', 'create_time']
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
with transaction.atomic():
|
instance = serializer.save()
|
||||||
instance = serializer.save()
|
user = instance.user
|
||||||
user = instance.user
|
up = UserPost.objects.filter(user=user).order_by(
|
||||||
up = UserPost.objects.filter(user=user).order_by(
|
'sort', 'create_time').first()
|
||||||
'sort', 'create_time').first()
|
if up:
|
||||||
if up:
|
user.belong_dept = up.dept
|
||||||
user.belong_dept = up.dept
|
user.post = up.post
|
||||||
user.post = up.post
|
|
||||||
user.update_by = self.request.user
|
|
||||||
user.save()
|
|
||||||
# 更新人员表
|
|
||||||
ep = Employee.objects.get_queryset(
|
|
||||||
all=True).filter(user=user).first()
|
|
||||||
if ep:
|
|
||||||
ep.belong_dept = user.belong_dept
|
|
||||||
ep.post = user.post
|
|
||||||
ep.is_deleted = False
|
|
||||||
ep.save()
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
with transaction.atomic():
|
|
||||||
user = instance.user
|
|
||||||
instance.delete()
|
|
||||||
up = UserPost.objects.filter(user=user).order_by(
|
|
||||||
'sort', 'create_time').first()
|
|
||||||
if up:
|
|
||||||
user.belong_dept = up.dept
|
|
||||||
user.post = up.post
|
|
||||||
else:
|
|
||||||
user.belong_dept = None
|
|
||||||
user.post = None
|
|
||||||
user.update_by = self.request.user
|
user.update_by = self.request.user
|
||||||
user.save()
|
user.save()
|
||||||
# 更新人员表
|
# 更新人员表
|
||||||
|
@ -440,6 +415,28 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
|
||||||
ep.is_deleted = False
|
ep.is_deleted = False
|
||||||
ep.save()
|
ep.save()
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
user = instance.user
|
||||||
|
instance.delete()
|
||||||
|
up = UserPost.objects.filter(user=user).order_by(
|
||||||
|
'sort', 'create_time').first()
|
||||||
|
if up:
|
||||||
|
user.belong_dept = up.dept
|
||||||
|
user.post = up.post
|
||||||
|
else:
|
||||||
|
user.belong_dept = None
|
||||||
|
user.post = None
|
||||||
|
user.update_by = self.request.user
|
||||||
|
user.save()
|
||||||
|
# 更新人员表
|
||||||
|
ep = Employee.objects.get_queryset(
|
||||||
|
all=True).filter(user=user).first()
|
||||||
|
if ep:
|
||||||
|
ep.belong_dept = user.belong_dept
|
||||||
|
ep.post = user.post
|
||||||
|
ep.is_deleted = False
|
||||||
|
ep.save()
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(CustomModelViewSet):
|
class UserViewSet(CustomModelViewSet):
|
||||||
queryset = User.objects.get_queryset(all=True)
|
queryset = User.objects.get_queryset(all=True)
|
||||||
|
@ -610,7 +607,7 @@ class UserViewSet(CustomModelViewSet):
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
|
class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
||||||
"""文件上传
|
"""文件上传
|
||||||
|
|
||||||
list:
|
list:
|
||||||
|
@ -651,7 +648,7 @@ class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, Cu
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet):
|
class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': '*', 'post': 'apk.upload'}
|
perms_map = {'get': '*', 'post': 'apk.upload'}
|
||||||
serializer_class = ApkSerializer
|
serializer_class = ApkSerializer
|
||||||
|
|
||||||
|
@ -692,7 +689,7 @@ class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSe
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||||||
perms_map = {'get': '*', 'post': '*',
|
perms_map = {'get': '*', 'post': '*',
|
||||||
'delete': 'myschedule.delete'}
|
'delete': 'myschedule.delete'}
|
||||||
serializer_class = MyScheduleSerializer
|
serializer_class = MyScheduleSerializer
|
||||||
|
@ -717,7 +714,6 @@ class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, Cus
|
||||||
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
|
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
vdata = serializer.validated_data
|
vdata = serializer.validated_data
|
||||||
vdata['create_by'] = self.request.user # 不可少
|
vdata['create_by'] = self.request.user # 不可少
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
from functools import wraps
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def lock_model_record(model_class, pk):
|
||||||
|
"""
|
||||||
|
Locks a model instance and returns it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instance = model_class.objects.select_for_update().get(pk=pk)
|
||||||
|
yield instance
|
||||||
|
except model_class.DoesNotExist:
|
||||||
|
raise ParseError("该记录不存在或已被删除")
|
||||||
|
|
||||||
|
def lock_model_record_d_func(model_class, pk_attr='id'):
|
||||||
|
"""
|
||||||
|
通用模型锁装饰器(内置事务),用于装饰函数
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
@transaction.atomic
|
||||||
|
def wrapper(old_instance, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
# 获取新鲜记录
|
||||||
|
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
|
||||||
|
# 调用原函数,但传入新鲜记录
|
||||||
|
return func(fresh_record, *args, **kwargs)
|
||||||
|
except model_class.DoesNotExist:
|
||||||
|
raise ParseError('记录不存在或已被删除')
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def lock_model_record_d_method(model_class, pk_attr='id'):
|
||||||
|
"""
|
||||||
|
通用模型锁装饰器(内置事务), 用于装饰类方法
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
@transaction.atomic
|
||||||
|
def wrapper(self, old_instance, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
# 获取新鲜记录
|
||||||
|
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
|
||||||
|
# 调用原函数,但传入新鲜记录
|
||||||
|
return func(self, fresh_record, *args, **kwargs)
|
||||||
|
except model_class.DoesNotExist:
|
||||||
|
raise ParseError('记录不存在或已被删除')
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
|
@ -9,7 +9,6 @@ from django.utils.timezone import now
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
import logging
|
import logging
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db import transaction
|
|
||||||
from rest_framework.exceptions import ParseError, ValidationError
|
from rest_framework.exceptions import ParseError, ValidationError
|
||||||
from apps.utils.errors import PKS_ERROR
|
from apps.utils.errors import PKS_ERROR
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
|
@ -18,6 +17,8 @@ from drf_yasg import openapi
|
||||||
from apps.utils.serializers import PkSerializer
|
from apps.utils.serializers import PkSerializer
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from apps.utils.serializers import ComplexSerializer
|
from apps.utils.serializers import ComplexSerializer
|
||||||
|
from django.db.models import F
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
# 实例化myLogger
|
# 实例化myLogger
|
||||||
myLogger = logging.getLogger('log')
|
myLogger = logging.getLogger('log')
|
||||||
|
@ -80,7 +81,8 @@ class BulkCreateModelMixin(CreateModelMixin):
|
||||||
|
|
||||||
def after_bulk_create(self, objs):
|
def after_bulk_create(self, objs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""创建(支持批量)
|
"""创建(支持批量)
|
||||||
|
|
||||||
|
@ -90,10 +92,9 @@ class BulkCreateModelMixin(CreateModelMixin):
|
||||||
many = False
|
many = False
|
||||||
if isinstance(rdata, list):
|
if isinstance(rdata, list):
|
||||||
many = True
|
many = True
|
||||||
with transaction.atomic():
|
sr = self.get_serializer(data=rdata, many=many)
|
||||||
sr = self.get_serializer(data=rdata, many=many)
|
sr.is_valid(raise_exception=True)
|
||||||
sr.is_valid(raise_exception=True)
|
self.perform_create(sr)
|
||||||
self.perform_create(sr)
|
|
||||||
if many:
|
if many:
|
||||||
self.after_bulk_create(sr.data)
|
self.after_bulk_create(sr.data)
|
||||||
return Response(sr.data, status=201)
|
return Response(sr.data, status=201)
|
||||||
|
@ -104,6 +105,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
def after_bulk_update(self, objs):
|
def after_bulk_update(self, objs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
"""部分更新(支持批量)
|
"""部分更新(支持批量)
|
||||||
|
|
||||||
|
@ -112,6 +114,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
kwargs['partial'] = True
|
kwargs['partial'] = True
|
||||||
return self.update(request, *args, **kwargs)
|
return self.update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
"""更新(支持批量)
|
"""更新(支持批量)
|
||||||
|
|
||||||
|
@ -123,16 +126,15 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
objs = []
|
objs = []
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
with transaction.atomic():
|
for ind, item in enumerate(request.data):
|
||||||
for ind, item in enumerate(request.data):
|
obj = get_object_or_404(queryset, id=item['id'])
|
||||||
obj = get_object_or_404(queryset, id=item['id'])
|
sr = self.get_serializer(obj, data=item, partial=partial)
|
||||||
sr = self.get_serializer(obj, data=item, partial=partial)
|
if not sr.is_valid():
|
||||||
if not sr.is_valid():
|
err_dict = { f'第{ind+1}': sr.errors}
|
||||||
err_dict = { f'第{ind+1}': sr.errors}
|
raise ValidationError(err_dict)
|
||||||
raise ValidationError(err_dict)
|
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
|
||||||
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
|
objs.append(sr.data)
|
||||||
objs.append(sr.data)
|
self.after_bulk_update(objs)
|
||||||
self.after_bulk_update(objs)
|
|
||||||
else:
|
else:
|
||||||
raise ParseError('提交数据非列表')
|
raise ParseError('提交数据非列表')
|
||||||
return Response(objs)
|
return Response(objs)
|
||||||
|
@ -147,6 +149,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
class BulkDestroyModelMixin(DestroyModelMixin):
|
class BulkDestroyModelMixin(DestroyModelMixin):
|
||||||
|
|
||||||
@swagger_auto_schema(request_body=PkSerializer)
|
@swagger_auto_schema(request_body=PkSerializer)
|
||||||
|
@transaction.atomic
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
"""删除(支持批量)
|
"""删除(支持批量)
|
||||||
|
|
||||||
|
@ -192,6 +195,8 @@ class CustomRetrieveModelMixin(RetrieveModelMixin):
|
||||||
|
|
||||||
给dict返回数据添加额外信息
|
给dict返回数据添加额外信息
|
||||||
"""
|
"""
|
||||||
|
if hasattr(self, 'add_info_for_list'):
|
||||||
|
return self.add_info_for_list([data])[0]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class CustomListModelMixin(ListModelMixin):
|
class CustomListModelMixin(ListModelMixin):
|
||||||
|
@ -240,6 +245,8 @@ class ComplexQueryMixin:
|
||||||
vdata = sr.validated_data
|
vdata = sr.validated_data
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
querys = vdata.get('querys', [])
|
querys = vdata.get('querys', [])
|
||||||
|
annotate_field_list = vdata.get('annotate_field_list', [])
|
||||||
|
|
||||||
if not querys:
|
if not querys:
|
||||||
new_qs = queryset
|
new_qs = queryset
|
||||||
else:
|
else:
|
||||||
|
@ -261,13 +268,29 @@ class ComplexQueryMixin:
|
||||||
new_qs = new_qs | one_qs
|
new_qs = new_qs | one_qs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ParseError(str(e))
|
raise ParseError(str(e))
|
||||||
|
|
||||||
|
if annotate_field_list:
|
||||||
|
annotate_dict = getattr(self, "annotate_dict", {})
|
||||||
|
if annotate_dict:
|
||||||
|
filtered_annotate_dict = { key: annotate_dict[key] for key in annotate_field_list if key in annotate_dict}
|
||||||
|
new_qs = new_qs.annotate(**filtered_annotate_dict)
|
||||||
|
|
||||||
ordering = vdata.get('ordering', None)
|
ordering = vdata.get('ordering', None)
|
||||||
if not ordering:
|
if not ordering:
|
||||||
ordering = getattr(self, 'ordering', None)
|
ordering = getattr(self, 'ordering', None)
|
||||||
if isinstance(ordering, str):
|
if isinstance(ordering, str):
|
||||||
|
ordering = ordering.replace('\n', '').replace(' ', '')
|
||||||
ordering = ordering.split(',')
|
ordering = ordering.split(',')
|
||||||
|
order_fields = []
|
||||||
if ordering:
|
if ordering:
|
||||||
new_qs = new_qs.order_by(*ordering)
|
for item in ordering:
|
||||||
|
if item.startswith('-'):
|
||||||
|
# JSONField 排序只能用字符串,不要 F
|
||||||
|
order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item)
|
||||||
|
else:
|
||||||
|
order_fields.append(F(item).asc(nulls_last=True) if '__' not in item else item)
|
||||||
|
new_qs = new_qs.order_by(*order_fields)
|
||||||
|
|
||||||
page = self.paginate_queryset(new_qs)
|
page = self.paginate_queryset(new_qs)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
@ -320,6 +343,7 @@ class MyLoggingMixin(object):
|
||||||
response = super().finalize_response(
|
response = super().finalize_response(
|
||||||
request, response, *args, **kwargs
|
request, response, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
self.log["response_ms"] = self._get_response_ms()
|
||||||
# Ensure backward compatibility for those using _should_log hook
|
# Ensure backward compatibility for those using _should_log hook
|
||||||
should_log = (
|
should_log = (
|
||||||
self._should_log if hasattr(self, "_should_log") else self.should_log
|
self._should_log if hasattr(self, "_should_log") else self.should_log
|
||||||
|
@ -348,7 +372,7 @@ class MyLoggingMixin(object):
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
"query_params": self._clean_data(request.query_params.dict()),
|
"query_params": self._clean_data(request.query_params.dict()),
|
||||||
"user": self._get_user(request),
|
"user": self._get_user(request),
|
||||||
"response_ms": self._get_response_ms(),
|
# "response_ms": self._get_response_ms(),
|
||||||
"response": self._clean_data(rendered_content),
|
"response": self._clean_data(rendered_content),
|
||||||
"status_code": response.status_code,
|
"status_code": response.status_code,
|
||||||
"agent": self._get_agent(request),
|
"agent": self._get_agent(request),
|
||||||
|
@ -442,7 +466,8 @@ class MyLoggingMixin(object):
|
||||||
By default, check if the request method is in logging_methods.
|
By default, check if the request method is in logging_methods.
|
||||||
"""
|
"""
|
||||||
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
|
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
|
||||||
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])
|
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])\
|
||||||
|
or (self.log.get("response_ms", 0) > 2000)
|
||||||
|
|
||||||
def _clean_data(self, data):
|
def _clean_data(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -116,11 +116,26 @@ class BaseModel(models.Model):
|
||||||
@classmethod
|
@classmethod
|
||||||
def safe_get_or_create(cls, defaults=None, **kwargs):
|
def safe_get_or_create(cls, defaults=None, **kwargs):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
lock_data = {**kwargs, **defaults}
|
|
||||||
lock_hash = hashlib.md5(str(lock_data).encode()).hexdigest()
|
for attempt in range(3):
|
||||||
lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}"
|
try:
|
||||||
with cache.lock(lock_key, timeout=10):
|
# 先尝试获取(带锁)
|
||||||
return cls.objects.get_or_create(**kwargs, defaults=defaults)
|
try:
|
||||||
|
obj = cls.objects.select_for_update().get(**kwargs)
|
||||||
|
return obj, False
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
# 不存在则创建
|
||||||
|
obj = cls(**kwargs, **defaults)
|
||||||
|
obj.save()
|
||||||
|
return obj, True
|
||||||
|
except IntegrityError:
|
||||||
|
# 发生唯一约束冲突时重试
|
||||||
|
if attempt == 2:
|
||||||
|
raise
|
||||||
|
time.sleep(0.1 * (attempt + 1))
|
||||||
|
except Exception:
|
||||||
|
# 其他异常直接抛出
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def handle_parent(self):
|
def handle_parent(self):
|
||||||
|
@ -132,32 +147,32 @@ class BaseModel(models.Model):
|
||||||
if not self.id:
|
if not self.id:
|
||||||
is_create = True
|
is_create = True
|
||||||
self.id = idWorker.get_id()
|
self.id = idWorker.get_id()
|
||||||
with transaction.atomic():
|
|
||||||
old_parent = None
|
old_parent = None
|
||||||
need_handle_parent = False
|
need_handle_parent = False
|
||||||
if hasattr(self, "parent"):
|
if hasattr(self, "parent"):
|
||||||
if is_create:
|
if is_create:
|
||||||
|
need_handle_parent = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
old_parent = self.__class__.objects.get(id=self.id).parent
|
||||||
|
except Exception:
|
||||||
|
self.parent = None
|
||||||
need_handle_parent = True
|
need_handle_parent = True
|
||||||
else:
|
if self.parent != old_parent:
|
||||||
try:
|
need_handle_parent = True
|
||||||
old_parent = self.__class__.objects.get(id=self.id).parent
|
try:
|
||||||
except Exception:
|
ins = super().save(*args, **kwargs)
|
||||||
self.parent = None
|
except IntegrityError as e:
|
||||||
need_handle_parent = True
|
if is_create:
|
||||||
if self.parent != old_parent:
|
time.sleep(0.01)
|
||||||
need_handle_parent = True
|
self.id = idWorker.get_id()
|
||||||
try:
|
|
||||||
ins = super().save(*args, **kwargs)
|
ins = super().save(*args, **kwargs)
|
||||||
except IntegrityError as e:
|
raise e
|
||||||
if is_create:
|
# 处理父级
|
||||||
time.sleep(0.01)
|
if need_handle_parent:
|
||||||
self.id = idWorker.get_id()
|
self.handle_parent()
|
||||||
ins = super().save(*args, **kwargs)
|
return ins
|
||||||
raise e
|
|
||||||
# 处理父级
|
|
||||||
if need_handle_parent:
|
|
||||||
self.handle_parent()
|
|
||||||
return ins
|
|
||||||
|
|
||||||
|
|
||||||
class SoftModel(BaseModel):
|
class SoftModel(BaseModel):
|
||||||
|
|
|
@ -75,8 +75,8 @@ class CustomModelSerializer(DynamicFieldsMixin, TreeSerializerMixin, serializers
|
||||||
class QuerySerializer(serializers.Serializer):
|
class QuerySerializer(serializers.Serializer):
|
||||||
field = serializers.CharField(label='字段名')
|
field = serializers.CharField(label='字段名')
|
||||||
compare = serializers.ChoiceField(
|
compare = serializers.ChoiceField(
|
||||||
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains"])
|
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains", "isnull"])
|
||||||
value = serializers.CharField(label='值')
|
value = serializers.JSONField(label='值', allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
class ComplexSerializer(serializers.Serializer):
|
class ComplexSerializer(serializers.Serializer):
|
||||||
|
@ -85,3 +85,4 @@ class ComplexSerializer(serializers.Serializer):
|
||||||
ordering = serializers.CharField(required=False)
|
ordering = serializers.CharField(required=False)
|
||||||
querys = serializers.ListField(child=QuerySerializer(
|
querys = serializers.ListField(child=QuerySerializer(
|
||||||
many=True), label="查询列表", required=False)
|
many=True), label="查询列表", required=False)
|
||||||
|
annotate_field_list = serializers.ListField(child=serializers.CharField(), label="RawSQL字段列表", required=False)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from rest_framework.exceptions import ParseError
|
||||||
myLogger = logging.getLogger('log')
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
|
|
||||||
@auto_log(name='阿里云短信', raise_exception=True, send_mail=True)
|
@auto_log(name='阿里云短信', raise_exception=True, send_mail=False)
|
||||||
def send_sms(phone: str, template_code: int, template_param: dict):
|
def send_sms(phone: str, template_code: int, template_param: dict):
|
||||||
from aliyunsdkcore.client import AcsClient
|
from aliyunsdkcore.client import AcsClient
|
||||||
from aliyunsdkcore.request import CommonRequest
|
from aliyunsdkcore.request import CommonRequest
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def execute_raw_sql(sql: str, params=None):
|
def execute_raw_sql(sql: str, params=None):
|
||||||
"""执行原始sql并返回rows, columns数据
|
"""执行原始sql并返回rows, columns数据
|
||||||
|
@ -23,7 +25,7 @@ def format_sqldata(columns, rows):
|
||||||
return [columns] + rows, [dict(zip(columns, row)) for row in rows]
|
return [columns] + rows, [dict(zip(columns, row)) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def query_all_dict(sql, params=None):
|
def query_all_dict(sql, params=None, with_time_format=False):
|
||||||
'''
|
'''
|
||||||
查询所有结果返回字典类型数据
|
查询所有结果返回字典类型数据
|
||||||
:param sql:
|
:param sql:
|
||||||
|
@ -36,9 +38,19 @@ def query_all_dict(sql, params=None):
|
||||||
else:
|
else:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
columns = [desc[0] for desc in cursor.description]
|
columns = [desc[0] for desc in cursor.description]
|
||||||
|
if with_time_format:
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {}
|
||||||
|
for col, val in zip(columns, row):
|
||||||
|
if isinstance(val, datetime):
|
||||||
|
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
row_dict[col] = val
|
||||||
|
results.append(row_dict)
|
||||||
|
return results
|
||||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||||
|
|
||||||
def query_one_dict(sql, params=None):
|
def query_one_dict(sql, params=None, with_time_format=False):
|
||||||
"""
|
"""
|
||||||
查询一个结果返回字典类型数据
|
查询一个结果返回字典类型数据
|
||||||
:param sql:
|
:param sql:
|
||||||
|
@ -46,13 +58,17 @@ def query_one_dict(sql, params=None):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
if params:
|
cursor.execute(sql, params or ()) # 更简洁的参数处理
|
||||||
cursor.execute(sql, params=params)
|
|
||||||
else:
|
|
||||||
cursor.execute(sql)
|
|
||||||
columns = [desc[0] for desc in cursor.description]
|
columns = [desc[0] for desc in cursor.description]
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
return dict(zip(columns, row))
|
if with_time_format:
|
||||||
|
row_dict = {}
|
||||||
|
for col, val in zip(columns, row):
|
||||||
|
if isinstance(val, datetime):
|
||||||
|
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
row_dict[col] = val
|
||||||
|
return row_dict
|
||||||
|
return dict(zip(columns, row)) if row else None # 安全处理None情况
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http import StreamingHttpResponse
|
from django.http import StreamingHttpResponse, Http404
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.mixins import RetrieveModelMixin
|
from rest_framework.mixins import RetrieveModelMixin
|
||||||
|
@ -18,7 +18,9 @@ from apps.utils.serializers import ComplexSerializer
|
||||||
from rest_framework.throttling import UserRateThrottle
|
from rest_framework.throttling import UserRateThrottle
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
import json
|
import json
|
||||||
|
from django.db import connection
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.utils import NotSupportedError
|
||||||
|
|
||||||
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -89,6 +91,36 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||||
elif hash_v_e:
|
elif hash_v_e:
|
||||||
return Response(hash_v_e)
|
return Response(hash_v_e)
|
||||||
|
|
||||||
|
def get_object(self, force_lock=False):
|
||||||
|
"""
|
||||||
|
智能加锁的get_object
|
||||||
|
- 只读请求:普通查询
|
||||||
|
- 非只读请求且在事务中:加锁查询
|
||||||
|
- 非只读请求但不在事务中:普通查询(带警告)
|
||||||
|
"""
|
||||||
|
# 只读方法列表
|
||||||
|
read_only_methods = ['GET', 'HEAD', 'OPTIONS']
|
||||||
|
|
||||||
|
if self.request.method not in read_only_methods and connection.in_atomic_block:
|
||||||
|
if force_lock:
|
||||||
|
raise ParseError("当前操作需要在事务中进行,请使用事务装饰器")
|
||||||
|
# 非只读请求且在事务中:加锁查询
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||||
|
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = queryset.get(**filter_kwargs)
|
||||||
|
l_obj = queryset.model.objects.select_for_update().get(pk=obj.pk)
|
||||||
|
self.check_object_permissions(self.request, l_obj)
|
||||||
|
return l_obj
|
||||||
|
|
||||||
|
except queryset.model.DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
else:
|
||||||
|
# 其他情况:普通查询
|
||||||
|
return super().get_object()
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
action_serializer_name = f"{self.action}_serializer_class"
|
action_serializer_name = f"{self.action}_serializer_class"
|
||||||
action_serializer_class = getattr(self, action_serializer_name, None)
|
action_serializer_class = getattr(self, action_serializer_name, None)
|
||||||
|
@ -193,5 +225,4 @@ class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListM
|
||||||
CustomRetrieveModelMixin, BulkDestroyModelMixin, ComplexQueryMixin, CustomGenericViewSet):
|
CustomRetrieveModelMixin, BulkDestroyModelMixin, ComplexQueryMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
增强的ModelViewSet
|
增强的ModelViewSet
|
||||||
"""
|
"""
|
||||||
pass
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-09-19 01:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0002_alter_state_filter_dept'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflow',
|
||||||
|
name='view_path',
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-10-16 08:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wf', '0003_workflow_view_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflow',
|
||||||
|
name='view_path2',
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径2'),
|
||||||
|
),
|
||||||
|
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue