Compare commits
55 Commits
3.0.202512
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
d29fcce935 | |
|
|
a534bde086 | |
|
|
63002f27c8 | |
|
|
4bbae8b7df | |
|
|
dc26c7cc46 | |
|
|
0d80e182cd | |
|
|
2759114ede | |
|
|
80f832aa85 | |
|
|
70e49eb27e | |
|
|
e99b2ecbbc | |
|
|
146e842642 | |
|
|
47b1887c4b | |
|
|
1ffbe0cc44 | |
|
|
3e173f7a72 | |
|
|
fce66da1d9 | |
|
|
feb8bd6770 | |
|
|
43f5f11ca8 | |
|
|
d5ea72a021 | |
|
|
143d9cb719 | |
|
|
cf6633592a | |
|
|
b39b0e7923 | |
|
|
70563a6c02 | |
|
|
def22f6b18 | |
|
|
f9eee5a523 | |
|
|
2ecaeadff7 | |
|
|
6eee0e1e53 | |
|
|
3417515e72 | |
|
|
43abcbaa48 | |
|
|
e2a92b6faa | |
|
|
02e3265133 | |
|
|
65cdeb0e7c | |
|
|
238d1dd074 | |
|
|
f7a78431c5 | |
|
|
5b60318bfb | |
|
|
0127e2a149 | |
|
|
f5b1b13a63 | |
|
|
f7b09ab1df | |
|
|
b362fc3b89 | |
|
|
8f791ac8de | |
|
|
afa3b8b9ad | |
|
|
3d6fcfac8a | |
|
|
db910f0804 | |
|
|
7dc7a78695 | |
|
|
952cdb1bc7 | |
|
|
9ed78f8d32 | |
|
|
52ebac68a0 | |
|
|
81770e89fa | |
|
|
6ac7c020bd | |
|
|
e385a558e9 | |
|
|
c37e71d60f | |
|
|
c3c7675ac5 | |
|
|
29f4e2f76a | |
|
|
ec13b8b166 | |
|
|
c56f908b42 | |
|
|
a8ae8ee32a |
|
|
@ -11,7 +11,7 @@ router.register('question', QuestionViewSet, basename='question')
|
||||||
router.register('paper', PaperViewSet, basename='paper')
|
router.register('paper', PaperViewSet, basename='paper')
|
||||||
router.register('exam', ExamViewSet, basename='exam')
|
router.register('exam', ExamViewSet, basename='exam')
|
||||||
router.register('examrecord', ExamRecordViewSet, basename='examrecord')
|
router.register('examrecord', ExamRecordViewSet, basename='examrecord')
|
||||||
router.register('training', TrainRecordViewSet, basename='examrecord')
|
router.register('training', TrainRecordViewSet, basename='training')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
path(API_BASE_URL, include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Generated by Django 3.2.12 on 2026-01-07 01:18
|
||||||
|
|
||||||
|
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', '0006_auto_20251215_1645'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('hrm', '0024_emppersoninfo_post'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Leave',
|
||||||
|
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_date', models.DateTimeField(verbose_name='开始日期')),
|
||||||
|
('end_date', models.DateTimeField(verbose_name='结束日期')),
|
||||||
|
('leave_type', models.PositiveSmallIntegerField(blank=True, choices=[(10, '事假'), (20, '病假'), (30, '婚假'), (40, '丧假'), (50, '公假'), (60, '工伤'), (70, '产假'), (80, '护理假'), (90, '其他')], null=True, verbose_name='请假类型')),
|
||||||
|
('reason', models.TextField(verbose_name='请假事由')),
|
||||||
|
('hour', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='请假时长')),
|
||||||
|
('file', models.TextField(blank=True, null=True, verbose_name='证明')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='leave_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hrm.employee', verbose_name='员工')),
|
||||||
|
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='leave_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -246,3 +246,28 @@ class EmpPersonInfo(CommonADModel):
|
||||||
post = models.CharField('岗位', max_length=20, null=True, blank=True)
|
post = models.CharField('岗位', max_length=20, null=True, blank=True)
|
||||||
note = models.TextField('备注', null=True, blank=True)
|
note = models.TextField('备注', null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Leave(CommonADModel):
|
||||||
|
"""
|
||||||
|
TN:员工请假
|
||||||
|
"""
|
||||||
|
E_TYPE_CHOISE = (
|
||||||
|
(10, '事假'),
|
||||||
|
(20, '病假'),
|
||||||
|
(30, '婚假'),
|
||||||
|
(40, '丧假'),
|
||||||
|
(50, '公假'),
|
||||||
|
(60, '工伤'),
|
||||||
|
(70, '产假'),
|
||||||
|
(80, '护理假'),
|
||||||
|
(90, '其他'),
|
||||||
|
)
|
||||||
|
employee = models.ForeignKey(Employee, verbose_name='员工', on_delete=models.CASCADE)
|
||||||
|
start_date = models.DateTimeField('开始日期')
|
||||||
|
end_date = models.DateTimeField('结束日期')
|
||||||
|
leave_type = models.PositiveSmallIntegerField('请假类型', choices=E_TYPE_CHOISE, null=True, blank=True)
|
||||||
|
reason = models.TextField('请假事由')
|
||||||
|
hour = models.PositiveSmallIntegerField('请假时长', null=True, blank=True)
|
||||||
|
file = models.TextField('证明', null=True, blank=True)
|
||||||
|
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.CASCADE, related_name='leave_ticket', null=True, blank=True)
|
||||||
|
|
@ -9,7 +9,7 @@ from django.utils import timezone
|
||||||
from apps.utils.serializers import CustomModelSerializer
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
from apps.utils.constants import EXCLUDE_FIELDS
|
from apps.utils.constants import EXCLUDE_FIELDS
|
||||||
from apps.hrm.models import (Certificate, ClockRecord, Employee,
|
from apps.hrm.models import (Certificate, ClockRecord, Employee,
|
||||||
NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo)
|
NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo, Leave)
|
||||||
from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer
|
from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
@ -368,3 +368,12 @@ class EmpPersonInfoSerializer(CustomModelSerializer):
|
||||||
'note',
|
'note',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class LeaveSerializer(CustomModelSerializer):
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
employee_name = serializers.CharField(source='employee.name', read_only=True)
|
||||||
|
post_name = serializers.CharField(source="employee.post.name", read_only=True)
|
||||||
|
belong_dept_name = serializers.CharField(source='employee.belong_dept.name', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Leave
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet,
|
from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet,
|
||||||
AttendanceViewSet, ResignationViewSet, EmpJoinViewSet)
|
AttendanceViewSet, ResignationViewSet, EmpJoinViewSet, LeaveViewSet)
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ router.register('attendance', AttendanceViewSet, basename='attendance')
|
||||||
router.register('resignation', ResignationViewSet, basename='resignation')
|
router.register('resignation', ResignationViewSet, basename='resignation')
|
||||||
router.register('empneed', EmpNeedViewSet, basename='empneed')
|
router.register('empneed', EmpNeedViewSet, basename='empneed')
|
||||||
router.register('empjoin', EmpJoinViewSet, basename='empjoin')
|
router.register('empjoin', EmpJoinViewSet, basename='empjoin')
|
||||||
|
router.register('leave', LeaveViewSet, basename='leave')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
path(API_BASE_URL, include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from rest_framework.response import Response
|
||||||
from apps.hrm.errors import NO_NEED_LEVEL_REMARK
|
from apps.hrm.errors import NO_NEED_LEVEL_REMARK
|
||||||
from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet,
|
from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet,
|
||||||
NotWorkRemarkFilterSet)
|
NotWorkRemarkFilterSet)
|
||||||
from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin
|
from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, Leave
|
||||||
from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer,
|
from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer,
|
||||||
ClockRecordListSerializer,
|
ClockRecordListSerializer,
|
||||||
EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer,
|
EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer,
|
||||||
|
|
@ -20,7 +20,7 @@ from apps.hrm.serializers import (CertificateCreateUpdateSerializer, Certificate
|
||||||
EmployeeSerializer,
|
EmployeeSerializer,
|
||||||
ClockRecordSimpleSerializer, ClockRecordCreateSerializer,
|
ClockRecordSimpleSerializer, ClockRecordCreateSerializer,
|
||||||
NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer,
|
NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer,
|
||||||
ResignationSerializer, EmpNeedSerializer)
|
ResignationSerializer, EmpNeedSerializer, LeaveSerializer)
|
||||||
from apps.hrm.services import HrmService
|
from apps.hrm.services import HrmService
|
||||||
|
|
||||||
from apps.third.dahua import dhClient
|
from apps.third.dahua import dhClient
|
||||||
|
|
@ -453,3 +453,19 @@ class EmpJoinViewSet(TicketMixin, EuModelViewSet):
|
||||||
serializer = EmpPersonInfoSerializer(data=person, many=True)
|
serializer = EmpPersonInfoSerializer(data=person, many=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
|
class LeaveViewSet(TicketMixin, EuModelViewSet):
|
||||||
|
select_related_fields = [
|
||||||
|
'employee',
|
||||||
|
'employee__belong_dept',
|
||||||
|
'ticket',
|
||||||
|
]
|
||||||
|
queryset = Leave.objects.all()
|
||||||
|
serializer_class = LeaveSerializer
|
||||||
|
filterset_fields = ['leave_type', 'employee__belong_dept']
|
||||||
|
search_fields = ["employee__name", "leave_type"]
|
||||||
|
workflow_key = "wf_leave"
|
||||||
|
|
||||||
|
def gen_other_ticket_data(self, instance):
|
||||||
|
return {"hour": instance.hour if instance.hour else None}
|
||||||
|
|
@ -247,6 +247,15 @@ class MIOItemAListSerializer(CustomModelSerializer):
|
||||||
read_only_fields = EXCLUDE_FIELDS_BASE
|
read_only_fields = EXCLUDE_FIELDS_BASE
|
||||||
|
|
||||||
|
|
||||||
|
class MIOItemListSimpleSerializer(CustomModelSerializer):
|
||||||
|
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||||
|
material_name = serializers.StringRelatedField(
|
||||||
|
source='material', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MIOItem
|
||||||
|
fields = ["id", "mio", "material", "warehouse", "material_name", "warehouse_name", "batch", "count", "test_date", "count_notok"]
|
||||||
|
|
||||||
class MIOItemSerializer(CustomModelSerializer):
|
class MIOItemSerializer(CustomModelSerializer):
|
||||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||||
material_ = MaterialSerializer(source='material', read_only=True)
|
material_ = MaterialSerializer(source='material', read_only=True)
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ router.register('warehouse', WarehouseVIewSet, basename='warehouse')
|
||||||
router.register('materialbatch', MaterialBatchViewSet,
|
router.register('materialbatch', MaterialBatchViewSet,
|
||||||
basename='materialbatch')
|
basename='materialbatch')
|
||||||
router.register('mio', MIOViewSet, basename='mio')
|
router.register('mio', MIOViewSet, basename='mio')
|
||||||
router.register('mio/do', MioDoViewSet)
|
router.register('mio/do', MioDoViewSet, basename='mio_do')
|
||||||
router.register('mio/sale', MioSaleViewSet)
|
router.register('mio/sale', MioSaleViewSet, basename='mio_sale')
|
||||||
router.register('mio/pur', MioPurViewSet)
|
router.register('mio/pur', MioPurViewSet, basename='mio_pur')
|
||||||
router.register('mio/other', MioOtherViewSet)
|
router.register('mio/other', MioOtherViewSet, basename='mio_other')
|
||||||
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')
|
# router.register('pack', PackViewSet, basename='pack')
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ 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, MioItemDetailSerializer, PackSerializer, PackMioSerializer)
|
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer, MIOItemListSimpleSerializer)
|
||||||
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
|
||||||
|
|
@ -163,20 +163,39 @@ class MIOViewSet(CustomModelViewSet):
|
||||||
return mio
|
return mio
|
||||||
|
|
||||||
def add_info_for_list(self, data):
|
def add_info_for_list(self, data):
|
||||||
# 获取检验状态
|
# 1. 收集所有mio的ID
|
||||||
mio_dict = {}
|
mio_ids = [item['id'] for item in data]
|
||||||
|
|
||||||
|
# 2. 预初始化mio字典和items列表
|
||||||
|
mio_dict = {item['id']: {
|
||||||
|
**item,
|
||||||
|
'is_tested': False, # 默认值设为False
|
||||||
|
'mioitems': []
|
||||||
|
} for item in data}
|
||||||
|
|
||||||
|
# 3. 批量查询MIOItem数据
|
||||||
|
if mio_ids: # 避免空查询
|
||||||
|
mioitems = MIOItemListSimpleSerializer(
|
||||||
|
instance=MIOItem.objects.filter(
|
||||||
|
mio__id__in=mio_ids
|
||||||
|
).select_related("warehouse", "material"),
|
||||||
|
many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
# 4. 单次循环处理所有item
|
||||||
|
for item in mioitems:
|
||||||
|
mio_id = item['mio']
|
||||||
|
if mio_id in mio_dict:
|
||||||
|
mio_dict[mio_id]['mioitems'].append(item)
|
||||||
|
# 更新is_tested状态(只要有一个item有test_date就为True)
|
||||||
|
if item.get('test_date'):
|
||||||
|
mio_dict[mio_id]['is_tested'] = True
|
||||||
|
|
||||||
|
# 5. 直接返回原始data列表,避免额外转换
|
||||||
for item in data:
|
for item in data:
|
||||||
item['is_tested'] = None
|
item.update(mio_dict[item['id']])
|
||||||
mio_dict[item['id']] = item
|
|
||||||
mioitems = list(MIOItem.objects.filter(mio__id__in=mio_dict.keys()).values_list("mio__id", "test_date"))
|
return data
|
||||||
for item in mioitems:
|
|
||||||
mioId, test_date = item
|
|
||||||
is_tested = False
|
|
||||||
if test_date:
|
|
||||||
is_tested = True
|
|
||||||
mio_dict[mioId]['is_tested'] = is_tested
|
|
||||||
datax = [mio_dict[key] for key in mio_dict.keys()]
|
|
||||||
return datax
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action in ['create', 'update', 'partial_update']:
|
if self.action in ['create', 'update', 'partial_update']:
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ class Mgroup(CommonBModel):
|
||||||
# 如果当前时间在结束时间之前,属于前一天
|
# 如果当前时间在结束时间之前,属于前一天
|
||||||
else:
|
else:
|
||||||
return (w_s_time - timedelta(days=1)).date(), shift
|
return (w_s_time - timedelta(days=1)).date(), shift
|
||||||
# return w_s_time.date(), None
|
raise ParseError(f"工段{self.name}的班次规则未覆盖时间点{w_s_time.strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(BaseModel):
|
class TeamMember(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.12 on 2026-01-04 06:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ofm', '0004_auto_20251218_1636'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vehicleuse',
|
||||||
|
name='end_km',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='归还公里数'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -60,7 +60,7 @@ class VehicleUse(CommonBDModel):
|
||||||
via = 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)
|
destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
|
||||||
start_km = models.PositiveIntegerField('出发公里数', null=True, blank=True)
|
start_km = models.PositiveIntegerField('出发公里数', null=True, blank=True)
|
||||||
end_km = models.PositiveIntegerField('归还公里数')
|
end_km = models.PositiveIntegerField('归还公里数', default=0)
|
||||||
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
|
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
|
||||||
is_city = models.BooleanField('是否市内用车', default=True)
|
is_city = models.BooleanField('是否市内用车', default=True)
|
||||||
reason = models.CharField('用车事由', max_length=100)
|
reason = models.CharField('用车事由', max_length=100)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 3.2.12 on 2025-12-26 07: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):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('wf', '0006_auto_20251215_1645'),
|
||||||
|
('pum', '0009_supplieraudit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuotationApply',
|
||||||
|
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='删除标记')),
|
||||||
|
('customer_name', models.CharField(max_length=50, verbose_name='客户名称')),
|
||||||
|
('product_name', models.CharField(max_length=50, verbose_name='产品名称')),
|
||||||
|
('contact_person', models.CharField(max_length=50, verbose_name='联系人')),
|
||||||
|
('contact_phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')),
|
||||||
|
('receive_address', models.CharField(blank=True, max_length=100, null=True, verbose_name='收件地址')),
|
||||||
|
('product_spec_quantity', models.TextField(blank=True, null=True, verbose_name='产品规格/数量')),
|
||||||
|
('quotation_basis', models.TextField(blank=True, null=True, verbose_name='报价依据')),
|
||||||
|
('suggested_price_calc', models.TextField(blank=True, null=True, verbose_name='建议价格及计算方式')),
|
||||||
|
('quotation_range', models.CharField(blank=True, max_length=100, null=True, verbose_name='报价区间')),
|
||||||
|
('quoter', models.CharField(blank=True, max_length=50, null=True, verbose_name='报价人')),
|
||||||
|
('apply_date', models.DateField(auto_now_add=True, verbose_name='申请日期')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotationapply_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quo_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotationapply_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -107,3 +107,22 @@ class PuPlanItem(CommonBDModel):
|
||||||
null=True, blank=True, related_name='item_puplan')
|
null=True, blank=True, related_name='item_puplan')
|
||||||
pu_order = models.ForeignKey(PuOrder, verbose_name='关联采购订单',
|
pu_order = models.ForeignKey(PuOrder, verbose_name='关联采购订单',
|
||||||
on_delete=models.SET_NULL, null=True, blank=True, related_name='puplan_item_puorder')
|
on_delete=models.SET_NULL, null=True, blank=True, related_name='puplan_item_puorder')
|
||||||
|
|
||||||
|
|
||||||
|
class QuotationApply(CommonADModel):
|
||||||
|
"""
|
||||||
|
TN:报价申请表
|
||||||
|
"""
|
||||||
|
customer_name = models.CharField(max_length=50,verbose_name="客户名称")
|
||||||
|
product_name = models.CharField(max_length=50,verbose_name="产品名称")
|
||||||
|
contact_person = models.CharField(max_length=50,verbose_name="联系人")
|
||||||
|
contact_phone = models.CharField(max_length=20,verbose_name="联系电话", null=True, blank=True)
|
||||||
|
receive_address = models.CharField(max_length=100,verbose_name="收件地址", null=True, blank=True)
|
||||||
|
product_spec_quantity = models.TextField(verbose_name="产品规格/数量", null=True, blank=True)
|
||||||
|
quotation_basis = models.TextField(verbose_name="报价依据", null=True, blank=True)
|
||||||
|
suggested_price_calc = models.TextField(verbose_name="建议价格及计算方式", null=True, blank=True)
|
||||||
|
quotation_range = models.CharField(max_length=100,verbose_name="报价区间", null=True, blank=True)
|
||||||
|
quoter = models.CharField(max_length=50,verbose_name="报价人", null=True, blank=True)
|
||||||
|
apply_date = models.DateField(verbose_name="申请日期",auto_now_add=True)
|
||||||
|
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
|
||||||
|
on_delete=models.CASCADE, related_name='quo_ticket', null=True, blank=True)
|
||||||
|
|
@ -3,7 +3,7 @@ from apps.utils.serializers import CustomModelSerializer
|
||||||
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
|
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
|
||||||
from rest_framework.exceptions import ValidationError, ParseError
|
from rest_framework.exceptions import ValidationError, ParseError
|
||||||
|
|
||||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit
|
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||||
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
|
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from .services import PumService
|
from .services import PumService
|
||||||
|
|
@ -158,3 +158,10 @@ class SupplierAuditSerializer(CustomModelSerializer):
|
||||||
if Supplier.objects.filter(name=name).exists():
|
if Supplier.objects.filter(name=name).exists():
|
||||||
raise ParseError('供应商名称已存在')
|
raise ParseError('供应商名称已存在')
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
class QuotationApplySerializer(CustomModelSerializer):
|
||||||
|
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = QuotationApply
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = EXCLUDE_FIELDS
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet)
|
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet)
|
||||||
|
# from apps.pum.views import SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet
|
||||||
|
|
||||||
API_BASE_URL = 'api/pum/'
|
API_BASE_URL = 'api/pum/'
|
||||||
HTML_BASE_URL = 'dhtml/pum/'
|
HTML_BASE_URL = 'dhtml/pum/'
|
||||||
|
|
@ -12,6 +13,7 @@ router.register('pu_plan', PuPlanViewSet, basename='pu_plan')
|
||||||
router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem')
|
router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem')
|
||||||
router.register('pu_order', PuOrderViewSet, basename='pu_order')
|
router.register('pu_order', PuOrderViewSet, basename='pu_order')
|
||||||
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
|
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
|
||||||
|
router.register('quotation', QuotationApplyViewSet, basename='quotation')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(API_BASE_URL, include(router.urls)),
|
path(API_BASE_URL, include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit
|
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
||||||
from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer,
|
from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer, QuotationApplySerializer,
|
||||||
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer)
|
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer)
|
||||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
@ -210,3 +210,16 @@ class PuOrderItemViewSet(CustomModelViewSet):
|
||||||
item.pu_order = puorder
|
item.pu_order = puorder
|
||||||
item.save()
|
item.save()
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
class QuotationApplyViewSet(TicketMixin, CustomModelViewSet):
|
||||||
|
"""
|
||||||
|
list: 报价申请
|
||||||
|
|
||||||
|
报价申请
|
||||||
|
"""
|
||||||
|
queryset = QuotationApply.objects.all()
|
||||||
|
serializer_class = QuotationApplySerializer
|
||||||
|
filterset_fields = ['product_name', 'customer_name','apply_date', 'quoter']
|
||||||
|
search_fields = ['product_name', 'customer_name','contact_person']
|
||||||
|
ordering = ['create_time']
|
||||||
|
workflow_key = "wf_quotation"
|
||||||
|
|
@ -157,6 +157,43 @@ def ftestwork_submit(ins:FtestWork, user: User):
|
||||||
# 触发批次统计分析
|
# 触发批次统计分析
|
||||||
ana_batch_thread(xbatchs=[ins.batch])
|
ana_batch_thread(xbatchs=[ins.batch])
|
||||||
|
|
||||||
|
|
||||||
|
def ftestwork_revert(ins: FtestWork):
|
||||||
|
wm:WMaterial = ins.wm
|
||||||
|
if wm and ins.need_update_wm:
|
||||||
|
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
|
||||||
|
for item in fwd_qs:
|
||||||
|
item:FtestworkDefect = item
|
||||||
|
if item.count > 0:
|
||||||
|
wm.count = wm.count + item.count
|
||||||
|
wm.save()
|
||||||
|
wmstate = WMaterial.WM_OK
|
||||||
|
if item.defect.okcate == Defect.DEFECT_NOTOK:
|
||||||
|
wmstate = WMaterial.WM_NOTOK
|
||||||
|
try:
|
||||||
|
wmx = WMaterial.objects.get(
|
||||||
|
material=wm.material,
|
||||||
|
batch=wm.batch,
|
||||||
|
mgroup=wm.mgroup,
|
||||||
|
belong_dept=wm.belong_dept,
|
||||||
|
state=wmstate,
|
||||||
|
notok_sign=None,
|
||||||
|
defect=item.defect,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ParseError(f'找不到{item.defect.name}的车间库存: {str(e)}')
|
||||||
|
wmx.count = wmx.count - item.count
|
||||||
|
if wmx.count < 0:
|
||||||
|
raise ParseError("数量不足,撤回失败")
|
||||||
|
wmx.save()
|
||||||
|
else:
|
||||||
|
raise ParseError("该检验工作不支持撤回")
|
||||||
|
ins.submit_user = None
|
||||||
|
ins.submit_time = None
|
||||||
|
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'])
|
||||||
if ins.submit_time is not None:
|
if ins.submit_time is not None:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
from apps.wpm.models import SfLog
|
from apps.wpm.models import SfLog
|
||||||
from apps.qm.filters import QuaStatFilter, TestItemFilter, FtestWorkFilter, QctFilter, FtestFilter
|
from apps.qm.filters import QuaStatFilter, TestItemFilter, FtestWorkFilter, QctFilter, FtestFilter
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from apps.qm.services import ftestwork_submit
|
from apps.qm.services import ftestwork_submit, ftestwork_revert
|
||||||
from apps.wpm.services_2 import ana_batch_thread
|
from apps.wpm.services_2 import ana_batch_thread
|
||||||
from apps.wf.models import State
|
from apps.wf.models import State
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
@ -328,3 +328,19 @@ class FtestWorkViewSet(CustomModelViewSet):
|
||||||
else:
|
else:
|
||||||
raise ParseError('该检验工作已提交')
|
raise ParseError('该检验工作已提交')
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=True, perms_map={'post': 'ftestwork.submit'}, serializer_class=Serializer)
|
||||||
|
@transaction.atomic
|
||||||
|
def revert(self, request, *args, **kwargs):
|
||||||
|
"""撤回检验工作
|
||||||
|
|
||||||
|
撤回检验工作
|
||||||
|
"""
|
||||||
|
ins:FtestWork = self.get_object()
|
||||||
|
if ins.submit_time:
|
||||||
|
if self.request.user != ins.submit_user:
|
||||||
|
raise ParseError('只能由提交人撤回')
|
||||||
|
ftestwork_revert(ins)
|
||||||
|
else:
|
||||||
|
raise ParseError('该检验工作未提交')
|
||||||
|
return Response()
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RemConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.rem'
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 3.2.12 on 2026-01-06 01:49
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Project',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.TextField(verbose_name='项目名称')),
|
||||||
|
('description', models.TextField(verbose_name='项目介绍')),
|
||||||
|
('start_date', models.DateField(verbose_name='开始日期')),
|
||||||
|
('end_date', models.DateField(verbose_name='结束日期')),
|
||||||
|
('participants', models.TextField(blank=True, null=True, verbose_name='项目成员')),
|
||||||
|
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('files', models.ManyToManyField(blank=True, to='system.File', verbose_name='附件')),
|
||||||
|
('leader', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 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='project_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from django.db import models
|
||||||
|
from apps.utils.models import CommonADModel
|
||||||
|
from apps.system.models import User, File
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class Project(CommonADModel):
|
||||||
|
name = models.TextField("项目名称")
|
||||||
|
description = models.TextField("项目介绍")
|
||||||
|
start_date = models.DateField("开始日期")
|
||||||
|
end_date = models.DateField("结束日期")
|
||||||
|
leader = models.ForeignKey(User, verbose_name="项目负责人", on_delete=models.CASCADE)
|
||||||
|
participants = models.TextField("项目成员", blank=True, null=True)
|
||||||
|
files = models.ManyToManyField(File, verbose_name="附件", blank=True)
|
||||||
|
note = models.TextField("备注", blank=True, null=True)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from apps.rem.models import Project
|
||||||
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
|
from apps.system.serializers import FileSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSerializer(CustomModelSerializer):
|
||||||
|
leader_name = serializers.CharField(source="leader.name", read_only=True)
|
||||||
|
files_ = FileSerializer(source="files", many=True, read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ProjectUpdateSerializer(CustomModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
fields = ["id", "participants", "files", "note"]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import ProjectViewSet
|
||||||
|
|
||||||
|
API_BASE_URL = 'api/rem/'
|
||||||
|
HTML_BASE_URL = 'dhtml/rem/'
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register('project', ProjectViewSet, basename='research_project')
|
||||||
|
urlpatterns = [
|
||||||
|
path(API_BASE_URL, include(router.urls)),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from apps.utils.viewsets import CustomModelViewSet
|
||||||
|
from apps.rem.models import Project
|
||||||
|
from apps.rem.serializers import ProjectSerializer, ProjectUpdateSerializer
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
class ProjectViewSet(CustomModelViewSet):
|
||||||
|
queryset = Project.objects.all()
|
||||||
|
serializer_class = ProjectSerializer
|
||||||
|
update_serializer_class = ProjectUpdateSerializer
|
||||||
|
select_related_fields = ['leader']
|
||||||
|
search_fields = ["name", "description", "leader__name"]
|
||||||
|
|
@ -7,6 +7,7 @@ from rest_framework.exceptions import ParseError
|
||||||
class UserFilterSet(filters.FilterSet):
|
class UserFilterSet(filters.FilterSet):
|
||||||
ubelong_dept__name = filters.CharFilter(label='归属于该部门及以下(按名称)', method='filter_ubelong_dept__name')
|
ubelong_dept__name = filters.CharFilter(label='归属于该部门及以下(按名称)', method='filter_ubelong_dept__name')
|
||||||
ubelong_dept = filters.CharFilter(label='归属于该部门及以下', method='filter_ubelong_dept')
|
ubelong_dept = filters.CharFilter(label='归属于该部门及以下', method='filter_ubelong_dept')
|
||||||
|
has_perm = filters.CharFilter(label='拥有指定权限标识', method='filter_has_perm')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
@ -38,6 +39,9 @@ class UserFilterSet(filters.FilterSet):
|
||||||
raise ParseError(f"部门ID错误: {value} {str(e)}")
|
raise ParseError(f"部门ID错误: {value} {str(e)}")
|
||||||
return queryset.filter(belong_dept__in=depts)
|
return queryset.filter(belong_dept__in=depts)
|
||||||
|
|
||||||
|
def filter_has_perm(self, queryset, name, value):
|
||||||
|
return queryset.filter(up_user__post__pr_post__role__perms__codes__contains=value)
|
||||||
|
|
||||||
|
|
||||||
class DeptFilterSet(filters.FilterSet):
|
class DeptFilterSet(filters.FilterSet):
|
||||||
|
|
||||||
|
|
@ -45,5 +49,6 @@ class DeptFilterSet(filters.FilterSet):
|
||||||
model = Dept
|
model = Dept
|
||||||
fields = {
|
fields = {
|
||||||
'type': ['exact', 'in'],
|
'type': ['exact', 'in'],
|
||||||
'name': ['exact', 'in', 'contains']
|
'name': ['exact', 'in', 'contains'],
|
||||||
|
"parent": ['exact', 'isnull'],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Generated by Django 4.2.27 on 2026-01-16 06:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0006_auto_20241213_1249'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dept',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dept',
|
||||||
|
name='third_info',
|
||||||
|
field=models.JSONField(blank=True, default=dict, verbose_name='三方系统信息'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dept',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dictionary',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dictionary',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dicttype',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dicttype',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='file',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='file',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='myschedule',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='myschedule',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='post',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='post',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='postrole',
|
||||||
|
name='post',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pr_post', to='system.post', verbose_name='关联岗位'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='postrole',
|
||||||
|
name='role',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pr_role', to='system.role', verbose_name='关联角色'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='belong_dept',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='roles',
|
||||||
|
field=models.ManyToManyField(blank=True, to='system.role', verbose_name='关联角色'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -54,7 +54,7 @@ class Dept(ParentModel, CommonAModel):
|
||||||
name = models.CharField('名称', max_length=60)
|
name = models.CharField('名称', max_length=60)
|
||||||
type = models.CharField('类型', max_length=20, default='dept')
|
type = models.CharField('类型', max_length=20, default='dept')
|
||||||
sort = models.PositiveSmallIntegerField('排序标记', default=1)
|
sort = models.PositiveSmallIntegerField('排序标记', default=1)
|
||||||
third_info = models.JSONField('三方系统信息', default=dict)
|
third_info = models.JSONField('三方系统信息', default=dict, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '部门'
|
verbose_name = '部门'
|
||||||
|
|
@ -107,9 +107,9 @@ class PostRole(BaseModel):
|
||||||
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices,
|
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices,
|
||||||
default=DataFilter.THISLEVEL_AND_BELOW)
|
default=DataFilter.THISLEVEL_AND_BELOW)
|
||||||
post = models.ForeignKey(Post, verbose_name='关联岗位',
|
post = models.ForeignKey(Post, verbose_name='关联岗位',
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE, related_name="pr_post")
|
||||||
role = models.ForeignKey(Role, verbose_name='关联角色',
|
role = models.ForeignKey(Role, verbose_name='关联角色',
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE, related_name='pr_role')
|
||||||
|
|
||||||
|
|
||||||
class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager):
|
class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager):
|
||||||
|
|
@ -132,7 +132,7 @@ class User(AbstractUser, CommonBModel):
|
||||||
posts = models.ManyToManyField(
|
posts = models.ManyToManyField(
|
||||||
Post, through='system.userpost', related_name='user_posts')
|
Post, through='system.userpost', related_name='user_posts')
|
||||||
depts = models.ManyToManyField(Dept, through='system.userpost')
|
depts = models.ManyToManyField(Dept, through='system.userpost')
|
||||||
roles = models.ManyToManyField(Role, verbose_name='关联角色')
|
roles = models.ManyToManyField(Role, verbose_name='关联角色', blank=True)
|
||||||
|
|
||||||
# 关联账号
|
# 关联账号
|
||||||
secret = models.CharField('密钥', max_length=100, null=True, blank=True)
|
secret = models.CharField('密钥', max_length=100, null=True, blank=True)
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ def phone_exist(phone):
|
||||||
|
|
||||||
|
|
||||||
def user_exist(username):
|
def user_exist(username):
|
||||||
if User.objects.filter(username=username).exists():
|
if User.objects.get_queryset(all=True).filter(username=username).exists():
|
||||||
raise serializers.ValidationError(**USERNAME_EXIST)
|
raise serializers.ValidationError(**USERNAME_EXIST)
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -300,9 +300,15 @@ class ComplexQueryMixin:
|
||||||
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)
|
||||||
return self.get_paginated_response(serializer.data)
|
rdata = serializer.data
|
||||||
|
if hasattr(self, 'add_info_for_list'):
|
||||||
|
rdata = self.add_info_for_list(rdata)
|
||||||
|
return self.get_paginated_response(rdata)
|
||||||
serializer = self.get_serializer(new_qs, many=True)
|
serializer = self.get_serializer(new_qs, many=True)
|
||||||
return Response(serializer.data)
|
rdata = serializer.data
|
||||||
|
if hasattr(self, 'add_info_for_list'):
|
||||||
|
rdata = self.add_info_for_list(rdata)
|
||||||
|
return Response(rdata)
|
||||||
|
|
||||||
class MyLoggingMixin(object):
|
class MyLoggingMixin(object):
|
||||||
"""Mixin to log requests"""
|
"""Mixin to log requests"""
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,32 @@ class BaseModel(models.Model):
|
||||||
time.sleep(0.1 * (attempt + 1))
|
time.sleep(0.1 * (attempt + 1))
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def locked_get_or_create(cls, defaults: dict, **kwargs):
|
||||||
|
"""
|
||||||
|
仅用于事务内
|
||||||
|
并发安全的 get_or_create
|
||||||
|
"""
|
||||||
|
if not connection.in_atomic_block:
|
||||||
|
raise RuntimeError("locked_get_or_create 必须在事务中调用")
|
||||||
|
|
||||||
|
defaults = defaults or {}
|
||||||
|
|
||||||
|
qs = cls.objects.select_for_update().filter(**kwargs)
|
||||||
|
|
||||||
|
cnt = qs.count()
|
||||||
|
if cnt > 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{cls.__name__} 数据异常:定位条件 {kwargs} 命中 {cnt} 条"
|
||||||
|
)
|
||||||
|
|
||||||
|
if cnt == 1:
|
||||||
|
return qs.get(), False
|
||||||
|
|
||||||
|
params = {**kwargs, **defaults}
|
||||||
|
obj = cls.objects.create(**params)
|
||||||
|
return obj, True
|
||||||
|
|
||||||
def handle_parent(self):
|
def handle_parent(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
from apps.wpm.models import (SfLog, StLog, WMaterial, Mlog, Mlogbw,
|
from apps.wpm.models import (SfLog, StLog, WMaterial, Mlog, Mlogbw,
|
||||||
Handover, Mgroup, Mlogb, Mtask, BatchSt)
|
Handover, Mgroup, Mlogb, Mtask, BatchSt, Handoverb)
|
||||||
from apps.mtm.models import Route, Material
|
from apps.mtm.models import Route, Material
|
||||||
from django.db.models import Q
|
from django.db.models import Q, Exists, OuterRef
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
@ -43,6 +43,7 @@ class WMaterialFilter(filters.FilterSet):
|
||||||
material__process__exclude = filters.CharFilter(field_name="material__process", lookup_expr="exact", exclude=True)
|
material__process__exclude = filters.CharFilter(field_name="material__process", lookup_expr="exact", exclude=True)
|
||||||
mlog_date_start = filters.DateFilter(label="产出开始", method="filter_mlog_date_start")
|
mlog_date_start = filters.DateFilter(label="产出开始", method="filter_mlog_date_start")
|
||||||
mlog_date_end = filters.DateFilter(label="产出结束", method="filter_mlog_date_end")
|
mlog_date_end = filters.DateFilter(label="产出结束", method="filter_mlog_date_end")
|
||||||
|
current_merged = filters.BooleanFilter(label="是否本工段新合成的批", method="filter_current_merged")
|
||||||
|
|
||||||
def filter_mlog_date_start(self, queryset, name, value):
|
def filter_mlog_date_start(self, queryset, name, value):
|
||||||
mgroupId = self.data.get("mgroup", None)
|
mgroupId = self.data.get("mgroup", None)
|
||||||
|
|
@ -101,6 +102,18 @@ class WMaterialFilter(filters.FilterSet):
|
||||||
raise ParseError('生产路线不存在!')
|
raise ParseError('生产路线不存在!')
|
||||||
return queryset.filter(material=route.material_in)|queryset.filter(material__in=route.materials.all())
|
return queryset.filter(material=route.material_in)|queryset.filter(material__in=route.materials.all())
|
||||||
|
|
||||||
|
def filter_current_merged(self, queryset, name, value):
|
||||||
|
sub_qs = Handoverb.objects.filter(
|
||||||
|
wm_to=OuterRef("pk"),
|
||||||
|
handover__mtype=Handover.H_MERGE,
|
||||||
|
handover__submit_time__isnull=False
|
||||||
|
)
|
||||||
|
if value is True:
|
||||||
|
return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=True)
|
||||||
|
elif value is False:
|
||||||
|
return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=False)
|
||||||
|
return queryset
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WMaterial
|
model = WMaterial
|
||||||
fields = {
|
fields = {
|
||||||
|
|
@ -154,11 +167,17 @@ class MlogFilter(filters.FilterSet):
|
||||||
class HandoverFilter(filters.FilterSet):
|
class HandoverFilter(filters.FilterSet):
|
||||||
cbatch = filters.CharFilter(label='批次号', method='filter_cbatch')
|
cbatch = filters.CharFilter(label='批次号', method='filter_cbatch')
|
||||||
mgroup = filters.CharFilter(label='MgroupId', method='filter_mgroup')
|
mgroup = filters.CharFilter(label='MgroupId', method='filter_mgroup')
|
||||||
|
mgroupx = filters.CharFilter(label='MgroupId', method='filter_mgroupx')
|
||||||
dept = filters.CharFilter(label='DeptId', method='filter_dept')
|
dept = filters.CharFilter(label='DeptId', method='filter_dept')
|
||||||
|
|
||||||
def filter_mgroup(self, queryset, name, value):
|
def filter_mgroup(self, queryset, name, value):
|
||||||
return queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value)
|
return queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value)
|
||||||
|
|
||||||
|
def filter_mgroupx(self, queryset, name, value):
|
||||||
|
dept = Mgroup.objects.get(id=value).belong_dept
|
||||||
|
return (queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value)|
|
||||||
|
queryset.filter(send_dept=dept, send_mgroup__isnull=True)|queryset.filter(recive_dept=dept, recive_mgroup__isnull=True))
|
||||||
|
|
||||||
def filter_dept(self, queryset, name, value):
|
def filter_dept(self, queryset, name, value):
|
||||||
return queryset.filter(send_dept__id=value)|queryset.filter(recive_dept__id=value)
|
return queryset.filter(send_dept__id=value)|queryset.filter(recive_dept__id=value)
|
||||||
|
|
||||||
|
|
@ -223,6 +242,10 @@ class MlogbFilter(filters.FilterSet):
|
||||||
class BatchStFilter(filters.FilterSet):
|
class BatchStFilter(filters.FilterSet):
|
||||||
batch__startswith__in = filters.CharFilter(method='filter_batch')
|
batch__startswith__in = filters.CharFilter(method='filter_batch')
|
||||||
data__has_key = filters.CharFilter(method='filter_data')
|
data__has_key = filters.CharFilter(method='filter_data')
|
||||||
|
with_source_near = filters.CharFilter(label='来源', method='filter_source_near')
|
||||||
|
|
||||||
|
def filter_source_near(self, queryset, name, value):
|
||||||
|
return queryset
|
||||||
|
|
||||||
def filter_data(self, queryset, name, value):
|
def filter_data(self, queryset, name, value):
|
||||||
return queryset.filter(data__has_key=value)
|
return queryset.filter(data__has_key=value)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Generated by Django 4.2.27 on 2026-01-16 07:29
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
|
||||||
|
('wpm', '0126_auto_20251208_1337'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='handoverb',
|
||||||
|
name='oinfo_json',
|
||||||
|
field=models.JSONField(blank=True, default=dict, verbose_name='其他信息'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='attlog',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='attlog',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fmlog',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fmlog',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='handover',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='handover',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlog',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mlog',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='otherlog',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='otherlog',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sflog',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sflog',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sflogexp',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sflogexp',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stlog',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stlog',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wmaterial',
|
||||||
|
name='belong_dept',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wmaterial',
|
||||||
|
name='create_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wmaterial',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -14,9 +14,11 @@ from django.db.models import Count
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
import re
|
import re
|
||||||
from django.db.models import Q
|
from django.db.models import Q, F
|
||||||
import django.utils.timezone as timezone
|
import django.utils.timezone as timezone
|
||||||
from apps.utils.sql import query_all_dict
|
from apps.utils.sql import query_all_dict
|
||||||
|
import logging
|
||||||
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
class SfLog(CommonADModel):
|
class SfLog(CommonADModel):
|
||||||
|
|
@ -125,6 +127,10 @@ class WMaterial(CommonBDModel):
|
||||||
material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom')
|
material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom')
|
||||||
number_from = models.TextField("来源于个号", null=True, blank=True)
|
number_from = models.TextField("来源于个号", null=True, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def belong_dept_or_mgroup_id(self):
|
||||||
|
return self.mgroup.id if self.mgroup else self.belong_dept.id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def count_working(self):
|
def count_working(self):
|
||||||
return Mlogb.objects.filter(wm_in=self, mlog__submit_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0
|
return Mlogb.objects.filter(wm_in=self, mlog__submit_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0
|
||||||
|
|
@ -162,6 +168,30 @@ class WMaterial(CommonBDModel):
|
||||||
state__in=[WMaterial.WM_OK, WMaterial.WM_REPAIR]
|
state__in=[WMaterial.WM_OK, WMaterial.WM_REPAIR]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def increase(cls, wm_id: str, user:User, count, count_eweight=None):
|
||||||
|
updates = {}
|
||||||
|
if count:
|
||||||
|
updates['count'] = F('count') + count
|
||||||
|
if count_eweight:
|
||||||
|
updates['count_eweight'] = count_eweight
|
||||||
|
if not updates:
|
||||||
|
return 0
|
||||||
|
updates["update_by"] = user
|
||||||
|
updates['update_time'] = timezone.now()
|
||||||
|
return cls.objects.filter(id=wm_id).update(**updates)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decrease(cls, wm_id: str, user:User, count):
|
||||||
|
if not count:
|
||||||
|
return 0
|
||||||
|
updated = cls.objects.filter(id=wm_id, count__gte= count).update(
|
||||||
|
count=F('count') - count, update_by=user, update_time=timezone.now())
|
||||||
|
if updated == 0:
|
||||||
|
batch = WMaterial.objects.get(id=wm_id).batch
|
||||||
|
raise ParseError(f'{batch}_库存不足,无法完成扣减')
|
||||||
|
return updated
|
||||||
|
|
||||||
class Fmlog(CommonADModel):
|
class Fmlog(CommonADModel):
|
||||||
"""TN: 父级生产日志
|
"""TN: 父级生产日志
|
||||||
"""
|
"""
|
||||||
|
|
@ -639,6 +669,7 @@ class Handoverb(BaseModel):
|
||||||
wm_to = models.ForeignKey(WMaterial, verbose_name='所到车间库存', on_delete=models.SET_NULL,
|
wm_to = models.ForeignKey(WMaterial, verbose_name='所到车间库存', on_delete=models.SET_NULL,
|
||||||
null=True, blank=True, related_name='handoverb_wm_to')
|
null=True, blank=True, related_name='handoverb_wm_to')
|
||||||
count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1)
|
count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1)
|
||||||
|
oinfo_json = models.JSONField('其他信息', default=dict, blank=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handoverbw(self):
|
def handoverbw(self):
|
||||||
|
|
@ -837,41 +868,46 @@ class BatchLog(BaseModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def batches_to(cls, batch:str):
|
def batches_to(cls, batch:str):
|
||||||
|
|
||||||
# query = """
|
|
||||||
# SELECT batch FROM wpm_batchst
|
|
||||||
# WHERE batch ~ %s
|
|
||||||
# """
|
|
||||||
query = """
|
query = """
|
||||||
SELECT batch
|
SELECT
|
||||||
FROM wpm_batchst
|
batch,
|
||||||
WHERE batch ~ %s
|
CAST(substring(batch FROM LENGTH(%s) + 2) AS INTEGER) AS batch_num
|
||||||
ORDER BY
|
FROM wpm_batchst
|
||||||
-- 先按前缀部分排序(例如 'A')
|
WHERE batch LIKE %s AND translate(
|
||||||
SUBSTRING(batch FROM '^(.*)-') DESC,
|
substring(batch FROM LENGTH(%s) + 2),
|
||||||
-- 再按后缀的数值部分排序(将 '2', '11' 转为整数)
|
'0123456789',
|
||||||
CAST(SUBSTRING(batch FROM '-([0-9]+)$') AS INTEGER) DESC
|
''
|
||||||
""" # 排序可在sql层处理
|
) = ''
|
||||||
query_ = """SELECT batch FROM wpm_batchst WHERE batch ~ %s"""
|
ORDER BY batch_num DESC
|
||||||
pattern = f'^{batch}-[0-9]+$'
|
"""
|
||||||
|
|
||||||
"""可以用如下方法直接查询
|
prefix = batch
|
||||||
"""
|
params = (
|
||||||
# batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct()
|
prefix,
|
||||||
# batches = sorted(list(batches), key=custom_key)
|
f"{prefix}-%",
|
||||||
batches_r = query_all_dict(query_, params=(pattern,))
|
prefix
|
||||||
batches = [b["batch"] for b in batches_r]
|
)
|
||||||
batches = sorted(list(batches), key=custom_key)
|
|
||||||
last_batch_num = None
|
try:
|
||||||
if batches:
|
rows = query_all_dict(query, params=params)
|
||||||
last_batch = batches[-1]
|
except Exception as e:
|
||||||
last_batch_list = last_batch.split("-")
|
myLogger.error(f"BatchLog.batches_to error: {(str(e), query, params)}")
|
||||||
if last_batch_list:
|
raise
|
||||||
try:
|
|
||||||
last_batch_num = int(last_batch_list[-1])
|
if not rows:
|
||||||
except Exception:
|
return {
|
||||||
pass
|
"batches": [],
|
||||||
return {"batches": batches, "last_batch_num": last_batch_num, "last_batch": last_batch}
|
"last_batch_num": None,
|
||||||
return {"batches": [], "last_batch_num": None, "last_batch": None}
|
"last_batch": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
batches = [r["batch"] for r in rows]
|
||||||
|
last = rows[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"batches": batches,
|
||||||
|
"last_batch_num": last["batch_num"],
|
||||||
|
"last_batch": last["batch"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from apps.wpm.models import BatchSt
|
from apps.wpm.models import BatchSt
|
||||||
import logging
|
import logging
|
||||||
from apps.qm.models import Defect
|
from apps.qm.models import Defect, FtestWork, FtestworkDefect
|
||||||
from apps.wpm.models import Mlogb, MlogbDefect
|
from apps.wpm.models import Mlogb, MlogbDefect
|
||||||
from apps.mtm.models import Mgroup
|
from apps.mtm.models import Mgroup
|
||||||
import decimal
|
import decimal
|
||||||
|
|
@ -117,25 +117,54 @@ def main(batch: str, mgroup_obj):
|
||||||
data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"]
|
data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"]
|
||||||
data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2)
|
data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2)
|
||||||
|
|
||||||
|
# 车间库存抽检
|
||||||
|
ft_qs = FtestWork.objects.filter(type2=FtestWork.TYPE2_SOME, wm__mgroup__name="外观检验", batch=batch, submit_time__isnull=False)
|
||||||
|
if ft_qs.exists():
|
||||||
|
data["外观检验_车间库存抽检_日期"] = []
|
||||||
|
data["外观检验_车间库存抽检_操作人"] = []
|
||||||
|
data["外观检验_车间库存抽检_count_notok"] = 0
|
||||||
|
for item in ft_qs:
|
||||||
|
if item.test_user:
|
||||||
|
data["外观检验_车间库存抽检_操作人"].append(item.test_user)
|
||||||
|
if item.test_date:
|
||||||
|
data["外观检验_车间库存抽检_日期"].append(item.test_date)
|
||||||
|
data["外观检验_车间库存抽检_count_notok"] += item.count_notok if item.count_notok else 0
|
||||||
|
|
||||||
|
data["外观检验_车间库存抽检_日期"] = list(set(data["外观检验_车间库存抽检_日期"]))
|
||||||
|
data["外观检验_车间库存抽检_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["外观检验_车间库存抽检_日期"]])
|
||||||
|
data["外观检验_车间库存抽检_操作人"] = list(set(data["外观检验_车间库存抽检_操作人"]))
|
||||||
|
data["外观检验_车间库存抽检_操作人"] = ";".join([item.name for item in data["外观检验_车间库存抽检_操作人"]])
|
||||||
|
|
||||||
|
# 车间库存抽检缺陷
|
||||||
|
ftd_qs = FtestworkDefect.objects.filter(ftestwork__in=ft_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
||||||
|
for item in ftd_qs:
|
||||||
|
data[f"外观检验_车间库存抽检_缺陷_{item['defect__name']}"] = item["total"]
|
||||||
|
|
||||||
if "外观检验_count_ok" in data:
|
if "外观检验_count_ok" in data:
|
||||||
data["外观检验_总合格数"] = data["外观检验_count_ok"] + data["外观检验_返修_count_ok"] if "外观检验_返修_count_ok" in data else 0
|
data["外观检验_总合格数"] = data["外观检验_count_ok"] + data.get("外观检验_返修_count_ok", 0)
|
||||||
try:
|
try:
|
||||||
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
|
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
|
||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
data["外观检验_总合格率"] = 0
|
data["外观检验_总合格率"] = 0
|
||||||
|
|
||||||
data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + data["外观检验_返修_count_ok_full"] if "外观检验_返修_count_ok_full" in data else 0
|
data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + data.get("外观检验_返修_count_ok_full", 0)
|
||||||
try:
|
try:
|
||||||
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
|
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
|
||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
data["外观检验_完全总合格率"] = 0
|
data["外观检验_完全总合格率"] = 0
|
||||||
|
|
||||||
|
data["外观检验_直通合格数"] = data["外观检验_总合格数"] - data.get("外观检验_车间库存抽检_count_notok", 0)
|
||||||
if "尺寸检验_合格率" in data:
|
if "尺寸检验_合格率" in data:
|
||||||
try:
|
try:
|
||||||
data["外观检验_直通合格率"] = round((data["外观检验_总合格率"]* data["尺寸检验_合格率"])/100, 2)
|
data["外观检验_直通合格率"] = round((data["外观检验_总合格率"]* data["尺寸检验_合格率"])/100, 2)
|
||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
data["外观检验_直通合格率"] = 0
|
data["外观检验_直通合格率"] = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["外观检验_直通合格率2"] = round((data["外观检验_直通合格数"]/data["尺寸检验_count_use"])*100, 2)
|
||||||
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||||
|
data["外观检验_直通合格率2"] = 0
|
||||||
|
|
||||||
if "尺寸检验_完全合格率" in data:
|
if "尺寸检验_完全合格率" in data:
|
||||||
try:
|
try:
|
||||||
data["外观检验_完全直通合格率"] = round((data["外观检验_完全总合格率"]* data["尺寸检验_完全合格率"])/100, 2)
|
data["外观检验_完全直通合格率"] = round((data["外观检验_完全总合格率"]* data["尺寸检验_完全合格率"])/100, 2)
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ def main(batch: str, mgroup_obj=None):
|
||||||
data["六车间交接领料_接料人"] = ";".join([item.name for item in data["六车间交接领料_接料人"]])
|
data["六车间交接领料_接料人"] = ";".join([item.name for item in data["六车间交接领料_接料人"]])
|
||||||
|
|
||||||
# 六车间工段生产数据
|
# 六车间工段生产数据
|
||||||
mgroup_list = ["平头", "粘铁头", "粗中细磨", "平磨", "掏管", "抛光", "开槽", "倒角"]
|
mgroup_list = ["平头", "粘铁头", "粗中细磨", "平磨", "掏管", "抛光", "开槽", "倒角", "加工前检验", "中检"]
|
||||||
for mgroup_name in mgroup_list:
|
for mgroup_name in mgroup_list:
|
||||||
if mgroup_name == '粗中细磨':
|
if mgroup_name == '粗中细磨':
|
||||||
mgroups = Mgroup.objects.filter(name__in=['粗磨', '粗中磨', '粗中细磨'])
|
mgroups = Mgroup.objects.filter(name__in=['粗磨', '粗中磨', '粗中细磨'])
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,67 @@
|
||||||
from apps.wpmw.models import Wpr
|
from apps.wpmw.models import Wpr
|
||||||
from apps.wpm.models import Mlogbw
|
from apps.wpm.models import Mlogbw, Mlog, MlogUser
|
||||||
from apps.qm.models import Ftest, FtestDefect, FtestItem
|
from apps.qm.models import Ftest, FtestDefect, FtestItem, TestItem
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from apps.mtm.models import Mgroup
|
from apps.mtm.models import Mgroup
|
||||||
|
|
||||||
def main(wprId, mgroup:Mgroup):
|
def main(wprId, mgroup:Mgroup=None):
|
||||||
wpr = Wpr.objects.get(id=wprId)
|
wpr = Wpr.objects.get(id=wprId)
|
||||||
|
if mgroup is None:
|
||||||
|
mgroup_ids = Mlogbw.objects.filter(
|
||||||
|
wpr=wpr,
|
||||||
|
mlogb__mlog__submit_time__isnull=False,
|
||||||
|
mlogb__mlog__is_fix=False
|
||||||
|
).values_list(
|
||||||
|
'mlogb__mlog__mgroup',
|
||||||
|
flat=True
|
||||||
|
).distinct()
|
||||||
|
mgroups = Mgroup.objects.filter(id__in=mgroup_ids)
|
||||||
|
else:
|
||||||
|
mgroups = [mgroup]
|
||||||
data = {}
|
data = {}
|
||||||
mgroup_name = mgroup.name
|
for mgroup in mgroups:
|
||||||
mlogbw = Mlogbw.objects.filter(wpr=wpr, mlogb__mlog__mgroup=mgroup, mlogb__mlog__submit_time__isnull=False).order_by("-update_time").first()
|
mgroup_name = mgroup.name
|
||||||
if mlogbw:
|
mlogbw = Mlogbw.objects.filter(wpr=wpr,
|
||||||
data[f"{mgroup_name}_批次号"] = mlogbw.mlogb.batch
|
mlogb__mlog__mgroup=mgroup,
|
||||||
data[f"{mgroup_name}_日期"] = mlogbw.mlogb.mlog.handle_date.strftime("%Y-%m-%d")
|
mlogb__mlog__submit_time__isnull=False, mlogb__mlog__is_fix=False).order_by("-update_time").first()
|
||||||
ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
|
if mlogbw:
|
||||||
|
mlog:Mlog = mlogbw.mlogb.mlog
|
||||||
|
data[f"{mgroup_name}_批次号"] = mlogbw.mlogb.batch
|
||||||
|
data[f"{mgroup_name}_设备编号"] = mlog.equipment.number if mlog.equipment else None
|
||||||
|
data[f"{mgroup_name}_操作人"] = mlog.handle_user.name if mlog.handle_user else None
|
||||||
|
data[f"{mgroup_name}_日期"] = mlog.handle_date.strftime("%Y-%m-%d")
|
||||||
|
# 日志操作数据
|
||||||
|
if mlog.oinfo_json:
|
||||||
|
oinfo_keys = list(mlog.oinfo_json.keys())
|
||||||
|
oinfo_keys_qs = TestItem.objects.filter(id__in=oinfo_keys)
|
||||||
|
for item in oinfo_keys_qs:
|
||||||
|
data[f"{mgroup_name}_操作项_{item.name}"] = mlog.oinfo_json[item.id]
|
||||||
|
# 子工序操作人和日期
|
||||||
|
mlogusers = MlogUser.objects.filter(mlog=mlog)
|
||||||
|
if mlogusers.exists():
|
||||||
|
datab = mlogusers.values("handle_user__name", "process__name", "handle_date")
|
||||||
|
for ind, item in enumerate(datab):
|
||||||
|
data[f"{mgroup_name}_{item['process__name']}_操作人"] = item["handle_user__name"]
|
||||||
|
data[f"{mgroup_name}_{item['process__name']}_日期"] = item["handle_date"].strftime("%Y-%m-%d")
|
||||||
|
# 检测数据
|
||||||
|
ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
|
||||||
|
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,
|
||||||
|
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False,
|
||||||
|
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False)
|
||||||
|
for ftestitem in ftestitems:
|
||||||
|
data[f"{mgroup_name}_检测项_{ftestitem.testitem.name}"] = ftestitem.test_val_json
|
||||||
|
|
||||||
|
ftestdefects = FtestDefect.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
|
||||||
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,
|
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,
|
||||||
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False,
|
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False,
|
||||||
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False)
|
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False)
|
||||||
for ftestitem in ftestitems:
|
for ftestdefect in ftestdefects:
|
||||||
data[f"{mgroup_name}_检测项_{ftestitem.testitem.name}"] = ftestitem.test_val_json
|
data[f"{mgroup_name}_缺陷项_{ftestdefect.defect.name}"] = 1 if ftestdefect.has is True else 0
|
||||||
|
old_data:dict = wpr.data
|
||||||
ftestdefects = FtestDefect.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
|
if old_data:
|
||||||
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,
|
for item in list(old_data.keys()):
|
||||||
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False,
|
if f'{mgroup_name}_' in item:
|
||||||
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False)
|
del old_data[item]
|
||||||
for ftestdefect in ftestdefects:
|
old_data.update(data)
|
||||||
data[f"{mgroup_name}_缺陷项_{ftestdefect.defect.name}"] = 1 if ftestdefect.has is True else 0
|
wpr.data = old_data
|
||||||
|
wpr.save(update_fields=["data"])
|
||||||
old_data:dict = wpr.data
|
|
||||||
if old_data:
|
|
||||||
for item in list(old_data.keys()):
|
|
||||||
if f'{mgroup_name}_' in item:
|
|
||||||
del old_data[item]
|
|
||||||
old_data.update(data)
|
|
||||||
wpr.data = old_data
|
|
||||||
wpr.save(update_fields=["data"])
|
|
||||||
|
|
@ -1049,6 +1049,11 @@ class MlogbwStartTestSerializer(serializers.Serializer):
|
||||||
test_equip=test_equip
|
test_equip=test_equip
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class MlogbOutPatchUpdateSerializer(CustomModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Mlogb
|
||||||
|
fields = ["batch"]
|
||||||
|
|
||||||
class MlogbOutUpdateSerializer(CustomModelSerializer):
|
class MlogbOutUpdateSerializer(CustomModelSerializer):
|
||||||
mlogbdefect = MlogbDefectSerializer(many=True, required=False)
|
mlogbdefect = MlogbDefectSerializer(many=True, required=False)
|
||||||
count_json = CountJsonSerializer(required=False, many=True)
|
count_json = CountJsonSerializer(required=False, many=True)
|
||||||
|
|
@ -1256,12 +1261,18 @@ class HandoverSerializer(CustomModelSerializer):
|
||||||
next_mat = new_wm.material
|
next_mat = new_wm.material
|
||||||
next_state = new_wm.state
|
next_state = new_wm.state
|
||||||
next_defect = new_wm.defect
|
next_defect = new_wm.defect
|
||||||
|
deptOrmgroupId = None
|
||||||
for ind, item in enumerate(attrs['handoverb']):
|
for ind, item in enumerate(attrs['handoverb']):
|
||||||
if item["count"] > 0:
|
if item["count"] > 0:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ParseError(f'第{ind+1}行-交接数量必须大于0')
|
raise ParseError(f'第{ind+1}行-交接数量必须大于0')
|
||||||
wm = item["wm"]
|
wm: WMaterial = item["wm"]
|
||||||
|
current_mdept_id = wm.belong_dept_or_mgroup_id
|
||||||
|
if deptOrmgroupId is None:
|
||||||
|
deptOrmgroupId = current_mdept_id
|
||||||
|
elif deptOrmgroupId != current_mdept_id:
|
||||||
|
raise ParseError(f'第{ind+1}行-交接物料所属工段/车间不一致')
|
||||||
if mtype == Handover.H_MERGE:
|
if mtype == Handover.H_MERGE:
|
||||||
if next_mat is None:
|
if next_mat is None:
|
||||||
next_mat = wm.material
|
next_mat = wm.material
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from ..qm.models import Defect, Ftest
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from apps.utils.tasks import ctask_run
|
from apps.utils.tasks import ctask_run
|
||||||
from apps.mtm.models import Process
|
from apps.mtm.models import Process
|
||||||
from apps.utils.lock import lock_model_record_d_func
|
from django.db.models import F
|
||||||
|
|
||||||
myLogger = logging.getLogger('log')
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
|
|
@ -150,11 +150,12 @@ def get_pcoal_heat(year_s: int, month_s: int, day_s: int):
|
||||||
myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True)
|
myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True)
|
||||||
return 25000
|
return 25000
|
||||||
|
|
||||||
# @lock_model_record_d_func(Mlog)
|
|
||||||
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
"""
|
"""
|
||||||
生产日志提交后需要执行的操作
|
生产日志提交后需要执行的操作
|
||||||
"""
|
"""
|
||||||
|
mlog = Mlog.objects.select_for_update().get(id=mlog.id)
|
||||||
if mlog.work_start_time and mlog.work_start_time > timezone.now():
|
if mlog.work_start_time and mlog.work_start_time > timezone.now():
|
||||||
raise ParseError('操作开始时间不能晚于当前时间')
|
raise ParseError('操作开始时间不能晚于当前时间')
|
||||||
if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time:
|
if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time:
|
||||||
|
|
@ -172,13 +173,15 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
|
|
||||||
mgroup = mlog.mgroup
|
mgroup = mlog.mgroup
|
||||||
process = mgroup.process
|
process = mgroup.process
|
||||||
into_wm_mgroup = process.into_wm_mgroup
|
stored_mgroup = process.into_wm_mgroup
|
||||||
need_store_notok = process.store_notok
|
stored_notok = process.store_notok
|
||||||
belong_dept = mgroup.belong_dept
|
belong_dept = mgroup.belong_dept
|
||||||
material_out: Material = mlog.material_out
|
material_out: Material = mlog.material_out
|
||||||
material_in: Material = mlog.material_in
|
material_in: Material = mlog.material_in
|
||||||
supplier = mlog.supplier # 外协
|
supplier = mlog.supplier # 外协
|
||||||
is_fix = mlog.is_fix
|
is_fix = mlog.is_fix
|
||||||
|
if is_fix: # 如果是返工,直接放到工段下
|
||||||
|
stored_mgroup = True
|
||||||
m_ins_list = []
|
m_ins_list = []
|
||||||
m_ins_bl_list = []
|
m_ins_bl_list = []
|
||||||
|
|
||||||
|
|
@ -221,21 +224,21 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
# 需要判断领用数是否合理
|
# 需要判断领用数是否合理
|
||||||
# 优先使用工段库存
|
# 优先使用工段库存
|
||||||
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in:
|
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in:
|
||||||
wm_qs = WMaterial.objects.filter(id=mlog_or_b.wm_in.id)
|
wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id)
|
||||||
else:
|
else:
|
||||||
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK)
|
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK)
|
||||||
if not wm_qs.exists():
|
if not wm_qs.exists():
|
||||||
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma,
|
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma,
|
||||||
belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK)
|
belong_dept=belong_dept, mgroup=None, 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 = WMaterial.objects.select_for_update().get(id=wm_qs.first().id)
|
||||||
elif count_x == 0:
|
elif count_x == 0:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
f'{str(mi_ma)}-{mi_batch}-批次库存不存在!')
|
f'{str(mi_ma)}-{mi_batch}-批次库存不存在!')
|
||||||
else:
|
else:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!')
|
f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!')
|
||||||
|
|
||||||
if mi_count > wm.count:
|
if mi_count > wm.count:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
|
|
@ -252,13 +255,13 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
Wpr.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest)
|
Wpr.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest)
|
||||||
|
|
||||||
# 针对加工前不良的暂时额外处理
|
# 针对加工前不良的暂时额外处理
|
||||||
if need_store_notok:
|
if stored_notok:
|
||||||
for item in m_ins_bl_list:
|
for item in m_ins_bl_list:
|
||||||
material, batch, count, defect, mi_ = item
|
material, batch, count, defect, mi_ = item
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
raise ParseError('存在非正数!')
|
raise ParseError('存在非正数!')
|
||||||
lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'defect': defect, 'state': WMaterial.WM_NOTOK}
|
lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'defect': defect, 'state': WMaterial.WM_NOTOK}
|
||||||
wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={"belong_dept": belong_dept})
|
wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
|
||||||
wm.count = wm.count + count
|
wm.count = wm.count + count
|
||||||
if is_create:
|
if is_create:
|
||||||
wm.create_by = user
|
wm.create_by = user
|
||||||
|
|
@ -275,12 +278,10 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
|
|
||||||
|
|
||||||
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
|
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
|
||||||
stored_mgroup = into_wm_mgroup
|
|
||||||
stored_notok = need_store_notok
|
|
||||||
if mlogb_out_qs.exists():
|
if mlogb_out_qs.exists():
|
||||||
mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
|
mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
|
||||||
m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
|
m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
|
||||||
if need_store_notok:
|
if stored_notok:
|
||||||
for item in mlogb_out_qs:
|
for item in mlogb_out_qs:
|
||||||
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
|
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
|
||||||
if item.qct is not None or mbd_qs.exists():
|
if item.qct is not None or mbd_qs.exists():
|
||||||
|
|
@ -309,7 +310,6 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if 'count_n_' in f.name and getattr(item, f.name) > 0:
|
if 'count_n_' in f.name and getattr(item, f.name) > 0:
|
||||||
notok_sign = f.name.replace('count_n_', '')
|
notok_sign = f.name.replace('count_n_', '')
|
||||||
m_outs_list.append( (item.material_out, item.batch if item.batch else mlog.batch, getattr(item, f.name), mlog.count_real_eweight, notok_sign, item))
|
m_outs_list.append( (item.material_out, item.batch if item.batch else mlog.batch, getattr(item, f.name), mlog.count_real_eweight, notok_sign, item))
|
||||||
stored_notok = True
|
|
||||||
# 这里有一个漏洞,在产出物为兄弟件的情况下,不合格品的数量是记录在mlog上的,
|
# 这里有一个漏洞,在产出物为兄弟件的情况下,不合格品的数量是记录在mlog上的,
|
||||||
# 而不是mlogb上,以上的额外处理就没有效果了, 不过光子不记录不合格品
|
# 而不是mlogb上,以上的额外处理就没有效果了, 不过光子不记录不合格品
|
||||||
else:
|
else:
|
||||||
|
|
@ -339,12 +339,12 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
lookup['defect'] = notok_sign_or_defect
|
lookup['defect'] = notok_sign_or_defect
|
||||||
elif notok_sign_or_defect is not None:
|
elif notok_sign_or_defect is not None:
|
||||||
lookup['notok_sign'] = notok_sign_or_defect
|
lookup['notok_sign'] = notok_sign_or_defect
|
||||||
if into_wm_mgroup:
|
if stored_mgroup:
|
||||||
lookup['mgroup'] = mgroup
|
lookup['mgroup'] = mgroup
|
||||||
else:
|
else:
|
||||||
lookup['belong_dept'] = belong_dept
|
lookup['belong_dept'] = belong_dept
|
||||||
|
|
||||||
wm, is_create2 = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept})
|
wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
|
||||||
wm.count = wm.count + mo_count
|
wm.count = wm.count + mo_count
|
||||||
wm.count_eweight = mo_count_eweight
|
wm.count_eweight = mo_count_eweight
|
||||||
wm.update_by = user
|
wm.update_by = user
|
||||||
|
|
@ -402,16 +402,17 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup)
|
ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup)
|
||||||
|
|
||||||
# 触发单个统计
|
# 触发单个统计
|
||||||
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True))
|
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True))
|
||||||
if wprIds:
|
if wprIds:
|
||||||
ana_wpr_thread(wprIds, mlog.mgroup)
|
ana_wpr_thread(wprIds, mlog.mgroup)
|
||||||
|
|
||||||
# @lock_model_record_d_func(Mlog)
|
|
||||||
def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
"""日志撤回
|
"""日志撤回
|
||||||
"""
|
"""
|
||||||
# if mlog.submit_time is None:
|
# if mlog.submit_time is None:
|
||||||
# return
|
# return
|
||||||
|
mlog = Mlog.objects.select_for_update().get(id=mlog.id)
|
||||||
if now is None:
|
if now is None:
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
|
|
@ -507,6 +508,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
else:
|
else:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
f'{str(mo_ma)}-{mo_batch}-存在多个相同批次!')
|
f'{str(mo_ma)}-{mo_batch}-存在多个相同批次!')
|
||||||
|
|
||||||
|
wm = WMaterial.objects.select_for_update().get(id=wm.id)
|
||||||
wm.count = wm.count - mo_count
|
wm.count = wm.count - mo_count
|
||||||
if wm.count < 0:
|
if wm.count < 0:
|
||||||
raise ParseError(f'{wm.batch} 车间库存不足, 产物无法回退')
|
raise ParseError(f'{wm.batch} 车间库存不足, 产物无法回退')
|
||||||
|
|
@ -529,7 +532,6 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
# 再生成消耗
|
# 再生成消耗
|
||||||
m_ins_list = []
|
m_ins_list = []
|
||||||
m_ins_bl_list = []
|
m_ins_bl_list = []
|
||||||
into_wm_mgroup = process.into_wm_mgroup
|
|
||||||
m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False)
|
m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False)
|
||||||
if m_ins.exists():
|
if m_ins.exists():
|
||||||
m_ins = m_ins.filter(need_inout=True)
|
m_ins = m_ins.filter(need_inout=True)
|
||||||
|
|
@ -549,17 +551,17 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if mi_count <= 0:
|
if mi_count <= 0:
|
||||||
raise ParseError('存在非正数!')
|
raise ParseError('存在非正数!')
|
||||||
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in:
|
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in:
|
||||||
wm = WMaterial.objects.get(id=mlog_or_b.wm_in.id)
|
wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id)
|
||||||
else:
|
else:
|
||||||
# 针对光子的情况,实际上必须需要wm_in
|
# 针对光子的情况,实际上必须需要wm_in
|
||||||
lookup = {'batch': mi_batch, 'material': mi_ma, 'mgroup': None, 'state': WMaterial.WM_OK}
|
lookup = {'batch': mi_batch, 'material': mi_ma, 'mgroup': None, 'state': WMaterial.WM_OK}
|
||||||
if into_wm_mgroup:
|
if stored_mgroup:
|
||||||
# 退回到本工段
|
# 退回到本工段
|
||||||
lookup['mgroup'] = mgroup
|
lookup['mgroup'] = mgroup
|
||||||
else:
|
else:
|
||||||
lookup['belong_dept'] = belong_dept
|
lookup['belong_dept'] = belong_dept
|
||||||
|
|
||||||
wm, _ = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept})
|
wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
|
||||||
wm.count = wm.count + mi_count
|
wm.count = wm.count + mi_count
|
||||||
wm.update_by = user
|
wm.update_by = user
|
||||||
wm.save()
|
wm.save()
|
||||||
|
|
@ -581,7 +583,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
lookup['mgroup'] = mgroup
|
lookup['mgroup'] = mgroup
|
||||||
else:
|
else:
|
||||||
lookup['belong_dept'] = belong_dept
|
lookup['belong_dept'] = belong_dept
|
||||||
wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept})
|
wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
|
||||||
wm.count = wm.count - count
|
wm.count = wm.count - count
|
||||||
if wm.count < 0:
|
if wm.count < 0:
|
||||||
raise ParseError('加工前不良数量大于库存量')
|
raise ParseError('加工前不良数量大于库存量')
|
||||||
|
|
@ -621,7 +623,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
ana_batch_thread(xbatches, mgroup=mlog.mgroup)
|
ana_batch_thread(xbatches, mgroup=mlog.mgroup)
|
||||||
|
|
||||||
# 触发单个统计
|
# 触发单个统计
|
||||||
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True))
|
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True))
|
||||||
if wprIds:
|
if wprIds:
|
||||||
ana_wpr_thread(wprIds, mlog.mgroup)
|
ana_wpr_thread(wprIds, mlog.mgroup)
|
||||||
|
|
||||||
|
|
@ -698,11 +700,15 @@ def update_mtask(mtask: Mtask, fill_way: int = 10):
|
||||||
# utask.state = Utask.UTASK_SUBMIT
|
# utask.state = Utask.UTASK_SUBMIT
|
||||||
utask.save()
|
utask.save()
|
||||||
|
|
||||||
@lock_model_record_d_func(Handover)
|
|
||||||
def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]):
|
def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]):
|
||||||
"""
|
"""
|
||||||
交接提交后需要执行的操作
|
交接提交后需要执行的操作
|
||||||
"""
|
"""
|
||||||
|
handover = (
|
||||||
|
Handover.objects
|
||||||
|
.select_for_update()
|
||||||
|
.get(pk=handover.pk)
|
||||||
|
)
|
||||||
if handover.submit_time is not None:
|
if handover.submit_time is not None:
|
||||||
return
|
return
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
@ -746,7 +752,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
wmId, xcount, handover_or_b = item
|
wmId, xcount, handover_or_b = item
|
||||||
if xcount <= 0:
|
if xcount <= 0:
|
||||||
raise ParseError("存在非正数!")
|
raise ParseError("存在非正数!")
|
||||||
wm_from = WMaterial.objects.get(id=wmId)
|
wm_from = (
|
||||||
|
WMaterial.objects
|
||||||
|
.select_for_update()
|
||||||
|
.get(id=wmId)
|
||||||
|
)
|
||||||
mids.append(wm_from.material.id)
|
mids.append(wm_from.material.id)
|
||||||
|
|
||||||
# 合并为新批
|
# 合并为新批
|
||||||
|
|
@ -770,25 +780,17 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
batch = wm_from.batch
|
batch = wm_from.batch
|
||||||
batches.append(batch)
|
batches.append(batch)
|
||||||
|
|
||||||
if wm_from is None:
|
WMaterial.decrease(wm_id=wm_from.id, user=user, count=xcount)
|
||||||
raise ParseError(f'{wm_from.batch} 找不到车间库存')
|
|
||||||
|
|
||||||
count_x = wm_from.count - xcount
|
|
||||||
if count_x < 0:
|
|
||||||
raise ParseError(f'{wm_from.batch} 车间库存不足!')
|
|
||||||
else:
|
|
||||||
wm_from.count = count_x
|
|
||||||
wm_from.save()
|
|
||||||
|
|
||||||
if need_add:
|
if need_add:
|
||||||
# 开始变动
|
# 开始变动
|
||||||
if handover.type == Handover.H_NORMAL:
|
if handover.type == Handover.H_NORMAL:
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = handover.new_wm
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if wm_to.state != WMaterial.WM_OK or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
if wm_to.state != wm_from.state or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
||||||
raise ParseError("正常合并到的车间库存状态或物料异常")
|
raise ParseError("正常合并到的车间库存状态或物料异常")
|
||||||
else:
|
else:
|
||||||
wm_to, _ = WMaterial.objects.get_or_create(
|
wm_to, _ = WMaterial.locked_get_or_create(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
material=material,
|
material=material,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
|
|
@ -808,11 +810,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
recive_mgroup = handover.recive_mgroup
|
recive_mgroup = handover.recive_mgroup
|
||||||
wm_state = WMaterial.WM_REPAIR
|
wm_state = WMaterial.WM_REPAIR
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = handover.new_wm
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
||||||
raise ParseError("返修合并到的车间库存状态或物料异常")
|
raise ParseError("返修合并到的车间库存状态或物料异常")
|
||||||
elif recive_mgroup:
|
elif recive_mgroup:
|
||||||
wm_to, _ = WMaterial.objects.get_or_create(
|
wm_to, _ = WMaterial.locked_get_or_create(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
material=material,
|
material=material,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
|
|
@ -830,28 +832,13 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ParseError("返工交接必须指定接收工段")
|
raise ParseError("返工交接必须指定接收工段")
|
||||||
elif handover.type == Handover.H_TEST:
|
|
||||||
raise ParseError("检验交接已废弃")
|
|
||||||
wm_to, _ = WMaterial.objects.get_or_create(
|
|
||||||
batch=batch,
|
|
||||||
material=material,
|
|
||||||
mgroup=recive_mgroup,
|
|
||||||
state=WMaterial.WM_TEST,
|
|
||||||
belong_dept=recive_dept,
|
|
||||||
defaults={
|
|
||||||
"count_xtest": 0,
|
|
||||||
"batch_ofrom": wm_from.batch_ofrom,
|
|
||||||
"material_ofrom": wm_from.material_ofrom,
|
|
||||||
"create_by": user
|
|
||||||
},
|
|
||||||
)
|
|
||||||
elif handover.type == Handover.H_SCRAP:
|
elif handover.type == Handover.H_SCRAP:
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = handover.new_wm
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
||||||
raise ParseError("报废合并到的车间库存状态或物料异常")
|
raise ParseError("报废合并到的车间库存状态或物料异常")
|
||||||
elif recive_mgroup:
|
elif recive_mgroup:
|
||||||
wm_to, _ = WMaterial.objects.get_or_create(
|
wm_to, _ = WMaterial.locked_get_or_create(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
material=material,
|
material=material,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
|
|
@ -870,11 +857,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
raise ParseError("不支持非工段报废")
|
raise ParseError("不支持非工段报废")
|
||||||
elif handover.type == Handover.H_CHANGE:
|
elif handover.type == Handover.H_CHANGE:
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = handover.new_wm
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed:
|
if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed:
|
||||||
raise ParseError("改版合并到的车间库存状态或物料异常")
|
raise ParseError("改版合并到的车间库存状态或物料异常")
|
||||||
elif handover.recive_mgroup:
|
elif handover.recive_mgroup:
|
||||||
wm_to, _ = WMaterial.objects.get_or_create(
|
wm_to, _ = WMaterial.locked_get_or_create(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
material=handover.material_changed,
|
material=handover.material_changed,
|
||||||
state=handover.state_changed,
|
state=handover.state_changed,
|
||||||
|
|
@ -897,9 +884,9 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
if wm_from and wm_from.state != WMaterial.WM_OK:
|
if wm_from and wm_from.state != WMaterial.WM_OK:
|
||||||
raise ParseError("仅合格品支持退回")
|
raise ParseError("仅合格品支持退回")
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = handover.new_wm
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
else:
|
else:
|
||||||
wm_to, _ = WMaterial.objects.get_or_create(
|
wm_to, _ = WMaterial.locked_get_or_create(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
material=material,
|
material=material,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
|
|
@ -917,9 +904,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
else:
|
else:
|
||||||
raise ParseError("不支持该交接类型")
|
raise ParseError("不支持该交接类型")
|
||||||
|
|
||||||
wm_to.count = wm_to.count + xcount
|
WMaterial.increase(wm_id=wm_to.id, user=user,count=xcount, count_eweight=handover.count_eweight if handover.count_eweight else None)
|
||||||
wm_to.count_eweight = handover.count_eweight # 这行代码有隐患
|
|
||||||
wm_to.save()
|
|
||||||
handover_or_b.wm_to = wm_to
|
handover_or_b.wm_to = wm_to
|
||||||
handover_or_b.save()
|
handover_or_b.save()
|
||||||
if material.tracking == Material.MA_TRACKING_SINGLE:
|
if material.tracking == Material.MA_TRACKING_SINGLE:
|
||||||
|
|
@ -934,7 +919,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
for item in handoverbws:
|
for item in handoverbws:
|
||||||
wpr:Wpr = item.wpr
|
wpr:Wpr = item.wpr
|
||||||
Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
|
Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
|
||||||
if wm_to.count != Wpr.objects.filter(wm=wm_to).count():
|
db_count = WMaterial.objects.filter(id=wm_to.id).values_list("count", flat=True).get()
|
||||||
|
if db_count != Wpr.objects.filter(wm=wm_to).count():
|
||||||
raise ParseError("交接与明细数量不一致2,操作失败")
|
raise ParseError("交接与明细数量不一致2,操作失败")
|
||||||
|
|
||||||
handover.submit_user = user
|
handover.submit_user = user
|
||||||
|
|
@ -945,14 +931,14 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
|
|
||||||
ana_batch_thread(xbatchs=batches)
|
ana_batch_thread(xbatchs=batches)
|
||||||
|
|
||||||
@lock_model_record_d_func(Handover)
|
|
||||||
def handover_revert(handover:Handover, handler:User=None):
|
def handover_revert(handover:Handover, handler:User=None):
|
||||||
|
handover = Handover.objects.select_for_update().get(id=handover.id)
|
||||||
if handover.submit_time is None:
|
if handover.submit_time is None:
|
||||||
raise ParseError('该交接单未提交!')
|
raise ParseError('该交接单未提交!')
|
||||||
ticket:Ticket = handover.ticket
|
ticket:Ticket = handover.ticket
|
||||||
if ticket:
|
if ticket:
|
||||||
# 首先把ticket改回开始状态
|
# 首先把ticket改回开始状态
|
||||||
WfService.retreat(ticket=ticket, suggestion="撤销交接单", handler=handler, next_handler=handover.create_by)
|
WfService.retreat(ticket=ticket, suggestion="撤回交接单", handler=handler, next_handler=handover.create_by)
|
||||||
mids = []
|
mids = []
|
||||||
# handover_type = handover.type
|
# handover_type = handover.type
|
||||||
# handover_mtype = handover.mtype
|
# handover_mtype = handover.mtype
|
||||||
|
|
@ -973,21 +959,18 @@ def handover_revert(handover:Handover, handler:User=None):
|
||||||
wm = item.wm
|
wm = item.wm
|
||||||
wm_to = item.wm_to
|
wm_to = item.wm_to
|
||||||
if wm is None or wm_to is None:
|
if wm is None or wm_to is None:
|
||||||
raise ParseError('该交接单不支持撤销2!')
|
raise ParseError('该交接单不支持撤回2!')
|
||||||
if wm == wm_to:
|
if wm == wm_to:
|
||||||
# 此时是自己交给自己,不需要做任何操作
|
# 此时是自己交给自己,不需要做任何操作
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
wm.count = wm.count + item.count
|
WMaterial.increase(wm_id=wm.id, user=handler, count=item.count)
|
||||||
wm.save()
|
WMaterial.decrease(wm_id=wm_to.id, user=handler, count=item.count)
|
||||||
wm_to.count = wm_to.count - item.count
|
|
||||||
if wm_to.count < 0:
|
|
||||||
raise ParseError('库存不足无法撤回!')
|
|
||||||
wm_to.save()
|
|
||||||
if material.tracking == Material.MA_TRACKING_SINGLE:
|
if material.tracking == Material.MA_TRACKING_SINGLE:
|
||||||
handoverbws = Handoverbw.objects.filter(handoverb=item)
|
handoverbws = Handoverbw.objects.filter(handoverb=item)
|
||||||
if handoverbws.count() != item.count:
|
if handoverbws.count() != item.count:
|
||||||
raise ParseError("交接与明细数量不一致,操作失败")
|
raise ParseError("交接与明细数量不一致,操作失败")
|
||||||
|
wm = WMaterial.objects.get(id=wm.id)
|
||||||
for item in handoverbws:
|
for item in handoverbws:
|
||||||
wpr:Wpr = item.wpr
|
wpr:Wpr = item.wpr
|
||||||
Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False)
|
Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False)
|
||||||
|
|
@ -1064,6 +1047,7 @@ def get_batch_dag(batch_number: str, method="full"):
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == "full":
|
if method == "full":
|
||||||
|
raise ParseError("不支持获取全局关系链条")
|
||||||
# 完整DAG模式 - 收集所有相关批次和边(原逻辑)
|
# 完整DAG模式 - 收集所有相关批次和边(原逻辑)
|
||||||
nodes_set = {batch_ins.id}
|
nodes_set = {batch_ins.id}
|
||||||
edges = []
|
edges = []
|
||||||
|
|
@ -1120,33 +1104,33 @@ def get_batch_dag(batch_number: str, method="full"):
|
||||||
})
|
})
|
||||||
|
|
||||||
# 查询作为source的其他关系
|
# 查询作为source的其他关系
|
||||||
leftLogs = BatchLog.objects.filter(source_id__in=left_source_ids).exclude(id__in=exist_log_ids)
|
# leftLogs = BatchLog.objects.filter(source_id__in=left_source_ids).exclude(id__in=exist_log_ids)
|
||||||
for log in leftLogs:
|
# for log in leftLogs:
|
||||||
source = log.source.id
|
# source = log.source.id
|
||||||
target = log.target.id
|
# target = log.target.id
|
||||||
nodes_set.add(log.target.id)
|
# nodes_set.add(log.target.id)
|
||||||
edges.append({
|
# edges.append({
|
||||||
'id': log.id,
|
# 'id': log.id,
|
||||||
'source': source,
|
# 'source': source,
|
||||||
'target': target,
|
# 'target': target,
|
||||||
"handover": log.handover.id if log.handover else None,
|
# "handover": log.handover.id if log.handover else None,
|
||||||
"mlog": log.mlog.id if log.mlog else None,
|
# "mlog": log.mlog.id if log.mlog else None,
|
||||||
'label': r_dict.get(log.relation_type, ""),
|
# 'label': r_dict.get(log.relation_type, ""),
|
||||||
})
|
# })
|
||||||
|
|
||||||
rightLogs = BatchLog.objects.filter(target_id__in=right_target_ids).exclude(id__in=exist_log_ids)
|
# rightLogs = BatchLog.objects.filter(target_id__in=right_target_ids).exclude(id__in=exist_log_ids)
|
||||||
for log in rightLogs:
|
# for log in rightLogs:
|
||||||
source = log.source.id
|
# source = log.source.id
|
||||||
target = log.target.id
|
# target = log.target.id
|
||||||
nodes_set.add(log.source.id)
|
# nodes_set.add(log.source.id)
|
||||||
edges.append({
|
# edges.append({
|
||||||
'id': log.id,
|
# 'id': log.id,
|
||||||
'source': source,
|
# 'source': source,
|
||||||
'target': target,
|
# 'target': target,
|
||||||
"handover": log.handover.id if log.handover else None,
|
# "handover": log.handover.id if log.handover else None,
|
||||||
"mlog": log.mlog.id if log.mlog else None,
|
# "mlog": log.mlog.id if log.mlog else None,
|
||||||
'label': r_dict.get(log.relation_type, ""),
|
# 'label': r_dict.get(log.relation_type, ""),
|
||||||
})
|
# })
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ParseError("不支持的查询方法,请使用'full'或'direct'")
|
raise ParseError("不支持的查询方法,请使用'full'或'direct'")
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ router.register('sflogexp', SfLogExpViewSet, basename='sflogexp')
|
||||||
router.register('wmaterial', WMaterialViewSet, basename='wmaterial')
|
router.register('wmaterial', WMaterialViewSet, basename='wmaterial')
|
||||||
router.register('fmlog', FmlogViewSet, basename='fmlog')
|
router.register('fmlog', FmlogViewSet, basename='fmlog')
|
||||||
router.register('mlog', MlogViewSet, basename='mlog')
|
router.register('mlog', MlogViewSet, basename='mlog')
|
||||||
router.register('mlogb', MlogbViewSet)
|
router.register('mlogb', MlogbViewSet, basename='mlogb')
|
||||||
router.register('mlogb/in', MlogbInViewSet)
|
router.register('mlogb/in', MlogbInViewSet, basename='mlogb_in')
|
||||||
router.register('mlogb/out', MlogbOutViewSet)
|
router.register('mlogb/out', MlogbOutViewSet, basename='mlogb_out')
|
||||||
router.register('handover', HandoverViewSet, basename='handover')
|
router.register('handover', HandoverViewSet, basename='handover')
|
||||||
router.register('attlog', AttlogViewSet, basename='attlog')
|
router.register('attlog', AttlogViewSet, basename='attlog')
|
||||||
router.register('otherlog', OtherLogViewSet, basename='otherlog')
|
router.register('otherlog', OtherLogViewSet, basename='otherlog')
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ from .serializers import (
|
||||||
MlogQuickSerializer,
|
MlogQuickSerializer,
|
||||||
MlogbwStartTestSerializer,
|
MlogbwStartTestSerializer,
|
||||||
HandoverListSerializer,
|
HandoverListSerializer,
|
||||||
BatchChangeSerializer
|
BatchChangeSerializer,
|
||||||
|
MlogbOutPatchUpdateSerializer
|
||||||
)
|
)
|
||||||
from .services import mlog_submit, handover_submit, mlog_revert, get_batch_dag, handover_revert
|
from .services import mlog_submit, handover_submit, mlog_revert, get_batch_dag, handover_revert
|
||||||
from apps.wpm.services import mlog_submit_validate, generate_new_batch
|
from apps.wpm.services import mlog_submit_validate, generate_new_batch
|
||||||
|
|
@ -446,9 +447,9 @@ class MlogViewSet(CustomModelViewSet):
|
||||||
raise ParseError("该日志存在审批!")
|
raise ParseError("该日志存在审批!")
|
||||||
user = request.user
|
user = request.user
|
||||||
if ins.submit_time is None:
|
if ins.submit_time is None:
|
||||||
raise ParseError("日志未提交不可撤销")
|
raise ParseError("日志未提交不可撤回")
|
||||||
if user != ins.submit_user:
|
if user != ins.submit_user:
|
||||||
raise ParseError("非提交人不可撤销!")
|
raise ParseError("非提交人不可撤回!")
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
mlog_revert(ins, user, now)
|
mlog_revert(ins, user, now)
|
||||||
return Response(MlogSerializer(instance=ins).data)
|
return Response(MlogSerializer(instance=ins).data)
|
||||||
|
|
@ -836,7 +837,7 @@ class MlogbInViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyMode
|
||||||
"batch": mlogbin.batch,
|
"batch": mlogbin.batch,
|
||||||
"batch_ofrom": wm_in.batch_ofrom,
|
"batch_ofrom": wm_in.batch_ofrom,
|
||||||
"material_ofrom": wm_in.material_ofrom,
|
"material_ofrom": wm_in.material_ofrom,
|
||||||
"qct": Qct.get(material_out, "process", "out"),
|
"qct": Qct.get(material_out, "fix" if mlog.is_fix else "process", "out"),
|
||||||
}
|
}
|
||||||
if mtype == Process.PRO_DIV and material_in.tracking == Material.MA_TRACKING_SINGLE:
|
if mtype == Process.PRO_DIV and material_in.tracking == Material.MA_TRACKING_SINGLE:
|
||||||
pass
|
pass
|
||||||
|
|
@ -1020,18 +1021,22 @@ class MlogbInViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyMode
|
||||||
|
|
||||||
|
|
||||||
class MlogbOutViewSet(BulkUpdateModelMixin, CustomGenericViewSet):
|
class MlogbOutViewSet(BulkUpdateModelMixin, CustomGenericViewSet):
|
||||||
perms_map = {"put": "mlog.update"}
|
perms_map = {"put": "mlog.update", "patch": "mlog.update"}
|
||||||
queryset = Mlogb.objects.filter(material_out__isnull=False)
|
queryset = Mlogb.objects.filter(material_out__isnull=False)
|
||||||
serializer_class = MlogbOutUpdateSerializer
|
serializer_class = MlogbOutUpdateSerializer
|
||||||
|
partial_update_serializer_class = MlogbOutPatchUpdateSerializer
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
ins: Mlogb = serializer.instance
|
if self.request.method == "PATCH":
|
||||||
mlog = MlogViewSet.lock_and_check_can_update(ins.mlog)
|
serializer.save()
|
||||||
material_out = serializer.validated_data.get("material_out")
|
else:
|
||||||
if material_out and material_out.tracking == Material.MA_TRACKING_SINGLE:
|
ins: Mlogb = serializer.instance
|
||||||
raise ParseError("单件产品不支持直接修改")
|
mlog = MlogViewSet.lock_and_check_can_update(ins.mlog)
|
||||||
ins: Mlogb = serializer.save()
|
material_out = serializer.validated_data.get("material_out")
|
||||||
mlog.cal_mlog_count_from_mlogb()
|
if material_out and material_out.tracking == Material.MA_TRACKING_SINGLE:
|
||||||
|
raise ParseError("单件产品不支持直接修改")
|
||||||
|
ins: Mlogb = serializer.save()
|
||||||
|
mlog.cal_mlog_count_from_mlogb()
|
||||||
|
|
||||||
|
|
||||||
class FmlogViewSet(CustomModelViewSet):
|
class FmlogViewSet(CustomModelViewSet):
|
||||||
|
|
@ -1072,6 +1077,16 @@ class BatchStViewSet(CustomListModelMixin, ComplexQueryMixin, CustomGenericViewS
|
||||||
ordering = ["batch"]
|
ordering = ["batch"]
|
||||||
filterset_class = BatchStFilter
|
filterset_class = BatchStFilter
|
||||||
|
|
||||||
|
def add_info_for_list(self, data):
|
||||||
|
if (self.request.query_params.get("with_source_near", None) == "yes" or
|
||||||
|
self.request.data.get("with_source_near", None) == "yes"):
|
||||||
|
batchstIds = [ins["id"] for ins in data]
|
||||||
|
batchlog_qs = BatchLog.objects.filter(target__id__in=batchstIds).values("id", "source", "target")
|
||||||
|
source_data = BatchStSerializer(instance=BatchSt.objects.filter(id__in=[ins["source"] for ins in batchlog_qs]), many=True).data
|
||||||
|
source_data_dict = {ins["id"]: ins for ins in source_data}
|
||||||
|
for item in data:
|
||||||
|
item["source_near"] = [source_data_dict[ins["source"]] for ins in batchlog_qs if ins["target"] == item["id"]]
|
||||||
|
return data
|
||||||
|
|
||||||
class MlogbwViewSet(CustomModelViewSet):
|
class MlogbwViewSet(CustomModelViewSet):
|
||||||
perms_map = {"get": "*", "post": "mlog.update", "put": "mlog.update", "delete": "mlog.update", "patch": "mlog.update"}
|
perms_map = {"get": "*", "post": "mlog.update", "put": "mlog.update", "delete": "mlog.update", "patch": "mlog.update"}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ class WprFilter(filters.FilterSet):
|
||||||
"mb": ["exact", "isnull"],
|
"mb": ["exact", "isnull"],
|
||||||
"wm": ["exact", "isnull"],
|
"wm": ["exact", "isnull"],
|
||||||
"material__process": ["exact"],
|
"material__process": ["exact"],
|
||||||
|
"material__name": ["exact", "contains"],
|
||||||
|
"wpr_from": ["exact", "isnull"],
|
||||||
"state": ["exact"],
|
"state": ["exact"],
|
||||||
"defects": ["exact"],
|
"defects": ["exact"],
|
||||||
"number": ["exact"]
|
"number": ["exact"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
|
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
|
||||||
from apps.utils.mixins import CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin
|
from apps.utils.mixins import CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin
|
||||||
|
|
||||||
from apps.wpmw.models import Wpr, WprDefect
|
from apps.wpmw.models import Wpr, WprDefect
|
||||||
from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer
|
from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer
|
||||||
|
|
@ -13,7 +13,7 @@ from apps.utils.sql import query_one_dict
|
||||||
from django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
|
|
||||||
|
|
||||||
class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet):
|
class WprViewSet(CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet):
|
||||||
"""动态产品
|
"""动态产品
|
||||||
|
|
||||||
动态产品
|
动态产品
|
||||||
|
|
@ -30,10 +30,20 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
|
||||||
ordering_fields = ["number", "create_time", "update_time"]
|
ordering_fields = ["number", "create_time", "update_time"]
|
||||||
search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"]
|
search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"]
|
||||||
annotate_dict = {
|
annotate_dict = {
|
||||||
"number_prefix": RawSQL("regexp_replace(number, '(\\d+)$', '')", []),
|
"number_prefix": RawSQL("regexp_replace(wpmw_wpr.number, '(\\d+)$', '')", []),
|
||||||
"number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []),
|
"number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(wpmw_wpr.number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def add_info_for_list(self, data):
|
||||||
|
parent_ids = [item["wpr_from"] for item in data if item.get("wpr_from", False)]
|
||||||
|
if parent_ids:
|
||||||
|
parent_data = Wpr.objects.filter(id__in=parent_ids).values("id", "number", "data")
|
||||||
|
parent_map = {item["id"]: item for item in parent_data}
|
||||||
|
for item in data:
|
||||||
|
if item["wpr_from"]:
|
||||||
|
item["wpr_from_"] = parent_map[item["wpr_from"]]
|
||||||
|
return data
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
qs = super().filter_queryset(queryset)
|
qs = super().filter_queryset(queryset)
|
||||||
if "mb__isnull" in self.request.query_params or "wm__isnull" in self.request.query_params:
|
if "mb__isnull" in self.request.query_params or "wm__isnull" in self.request.query_params:
|
||||||
|
|
@ -92,11 +102,20 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
|
||||||
# 使用原始sql
|
# 使用原始sql
|
||||||
query = """
|
query = """
|
||||||
SELECT id, number_out FROM wpmw_wpr
|
SELECT id, number_out FROM wpmw_wpr
|
||||||
WHERE number_out ~ %s order by number_out desc limit 1
|
WHERE number_out LIKE %s
|
||||||
|
AND translate(
|
||||||
|
substring(number_out FROM LENGTH(%s) + 2),
|
||||||
|
'0123456789',
|
||||||
|
''
|
||||||
|
) = ''
|
||||||
|
order by number_out desc limit 1
|
||||||
"""
|
"""
|
||||||
pattern = f"^{prefix}[0-9]+$"
|
params = (
|
||||||
|
f"{prefix}-%",
|
||||||
|
prefix
|
||||||
|
)
|
||||||
number_outs = []
|
number_outs = []
|
||||||
wpr_qs_last = query_one_dict(query, [pattern])
|
wpr_qs_last = query_one_dict(query, [params])
|
||||||
if wpr_qs_last:
|
if wpr_qs_last:
|
||||||
number_outs.append(wpr_qs_last["number_out"])
|
number_outs.append(wpr_qs_last["number_out"])
|
||||||
# 查找未出库的记录
|
# 查找未出库的记录
|
||||||
|
|
@ -106,9 +125,15 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
|
||||||
query2 = """
|
query2 = """
|
||||||
select mioitemw.id, mioitemw.number_out from inm_mioitemw mioitemw left join inm_mioitem mioitem on mioitem.id = mioitemw.mioitem_id
|
select mioitemw.id, mioitemw.number_out from inm_mioitemw mioitemw left join inm_mioitem mioitem on mioitem.id = mioitemw.mioitem_id
|
||||||
left join inm_mio mio on mio.id = mioitem.mio_id
|
left join inm_mio mio on mio.id = mioitem.mio_id
|
||||||
where mio.submit_time is null and mioitemw.number_out ~ %s order by mioitemw.number_out desc limit 1
|
where mio.submit_time is null and mioitemw.number_out LIKE %s
|
||||||
|
AND translate(
|
||||||
|
substring(mioitemw.number_out FROM LENGTH(%s) + 2),
|
||||||
|
'0123456789',
|
||||||
|
''
|
||||||
|
) = ''
|
||||||
|
order by mioitemw.number_out desc limit 1
|
||||||
"""
|
"""
|
||||||
mioitemw_last = query_one_dict(query2, [pattern])
|
mioitemw_last = query_one_dict(query2, [params])
|
||||||
if mioitemw_last:
|
if mioitemw_last:
|
||||||
number_outs.append(mioitemw_last["number_out"])
|
number_outs.append(mioitemw_last["number_out"])
|
||||||
if number_outs:
|
if number_outs:
|
||||||
|
|
|
||||||
20
changelog.md
20
changelog.md
|
|
@ -1,3 +1,23 @@
|
||||||
|
## 3.0.2026010716
|
||||||
|
- feat: 新增功能
|
||||||
|
- get_shift需要报错 [caoqianming]
|
||||||
|
- 添加rem模块 [caoqianming]
|
||||||
|
- 捕获除0异常 [caoqianming]
|
||||||
|
- 按需求修改光芯批次统计分析 [caoqianming]
|
||||||
|
- 批次统计数据支持返回source_near [caoqianming]
|
||||||
|
- 光芯OA 审批系统新增报价单审核 [TianyangZhang]
|
||||||
|
- get_batch_dag还是只返回直接前后级别 [caoqianming]
|
||||||
|
- mlogbbpatch修改批次号 [caoqianming]
|
||||||
|
- 出入库记录返回子表部分信息 [caoqianming]
|
||||||
|
- 统一撤回和撤销的表述 [caoqianming]
|
||||||
|
- 车间库存检验支持撤回 [caoqianming]
|
||||||
|
- wpr添加material_name查询条件 [caoqianming]
|
||||||
|
- 优化mlog_submit 返工后产品放在本工段下 [caoqianming]
|
||||||
|
- mlogbin qct 可依据fix选择 [caoqianming]
|
||||||
|
- base dept filter支持parent isnull查询 [caoqianming]
|
||||||
|
- fix: 问题修复
|
||||||
|
- wpr list annotate明确number指向 [caoqianming]
|
||||||
|
- 正常交接支持new_wm且支持不合格品 [caoqianming]
|
||||||
## 3.0.2025122514
|
## 3.0.2025122514
|
||||||
- feat: 新增功能
|
- feat: 新增功能
|
||||||
- mlogbw patch权限 [caoqianming]
|
- mlogbw patch权限 [caoqianming]
|
||||||
|
|
|
||||||
101
requirements.txt
101
requirements.txt
|
|
@ -1,37 +1,82 @@
|
||||||
celery==5.2.3
|
# =======================
|
||||||
Django==3.2.12
|
# Core
|
||||||
django-celery-beat==2.3.0
|
# =======================
|
||||||
django-celery-results==2.4.0
|
Django==4.2.27
|
||||||
django-cors-headers==3.11.0
|
|
||||||
django-filter==21.1
|
djangorestframework==3.16.1
|
||||||
djangorestframework==3.13.1
|
django-filter==23.5
|
||||||
djangorestframework-simplejwt==5.1.0
|
django-cors-headers==4.9.0
|
||||||
drf-yasg==1.21.7
|
|
||||||
psutil==5.9.0
|
djangorestframework-simplejwt==5.5.1
|
||||||
pillow==9.0.1
|
|
||||||
opencv-python==4.5.5.62
|
|
||||||
redis==4.4.0
|
|
||||||
django-redis==5.2.0
|
|
||||||
user-agents==2.2.0
|
|
||||||
daphne==4.0.0
|
|
||||||
channels-redis==4.0.0
|
|
||||||
django-restql==0.15.2
|
django-restql==0.15.2
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# Celery
|
||||||
|
# =======================
|
||||||
|
celery==5.6.2
|
||||||
|
django-celery-beat==2.8.1
|
||||||
|
django-celery-results==2.6.0
|
||||||
|
redis==7.1.0
|
||||||
|
django-redis==6.0.0
|
||||||
|
cron-descriptor==1.2.35
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# Channels / ASGI
|
||||||
|
# =======================
|
||||||
|
channels==4.3.2
|
||||||
|
daphne==4.0.0
|
||||||
|
channels-redis==4.3.0
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# API Docs
|
||||||
|
# =======================
|
||||||
|
drf-yasg==1.21.7
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# Auth / Utils
|
||||||
|
# =======================
|
||||||
|
user-agents==2.2.0
|
||||||
|
psutil==5.9.0
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# Media / Image / CV
|
||||||
|
# =======================
|
||||||
|
pillow==9.5.0
|
||||||
|
opencv-python==4.5.5.62
|
||||||
shapely==1.8.3
|
shapely==1.8.3
|
||||||
aliyun-python-sdk-core==2.13.36
|
|
||||||
baidu-aip==4.16.6
|
# =======================
|
||||||
chardet==5.0.0
|
# Network / RPC
|
||||||
requests==2.28.1
|
# =======================
|
||||||
|
requests==2.32.5
|
||||||
grpcio==1.47.0
|
grpcio==1.47.0
|
||||||
grpcio-tools==1.47.0
|
grpcio-tools==1.47.0
|
||||||
protobuf==3.20.1
|
protobuf==3.20.1
|
||||||
pycryptodome==3.15.0
|
|
||||||
|
# =======================
|
||||||
|
# Cloud SDK
|
||||||
|
# =======================
|
||||||
aliyun-python-sdk-core==2.13.36
|
aliyun-python-sdk-core==2.13.36
|
||||||
|
baidu-aip==4.16.6
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# Crypto
|
||||||
|
# =======================
|
||||||
|
pycryptodome==3.15.0
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# Excel / Docs
|
||||||
|
# =======================
|
||||||
xlwt==1.3.0
|
xlwt==1.3.0
|
||||||
openpyxl==3.1.0
|
openpyxl==3.1.5
|
||||||
cron-descriptor==1.2.35
|
|
||||||
pymysql==1.0.3
|
|
||||||
# face-recognition==1.3.0
|
|
||||||
docxtpl==0.16.7
|
docxtpl==0.16.7
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# DB
|
||||||
|
# =======================
|
||||||
|
pymysql==1.0.3
|
||||||
|
|
||||||
|
# =======================
|
||||||
|
# IoT / MQTT
|
||||||
|
# =======================
|
||||||
paho-mqtt==2.0.0
|
paho-mqtt==2.0.0
|
||||||
# deepface==0.0.79
|
|
||||||
# edge-tts==6.1.12
|
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,16 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import django
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
from apps.utils.middlewares import TokenAuthMiddleware
|
from apps.utils.middlewares import TokenAuthMiddleware
|
||||||
import apps.ws.routing
|
import apps.ws.routing
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter({
|
application = ProtocolTypeRouter({
|
||||||
"http": get_asgi_application(),
|
"http": get_asgi_application(),
|
||||||
"websocket": TokenAuthMiddleware(
|
"websocket": TokenAuthMiddleware(
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
SYS_NAME = '星途工厂综合管理系统'
|
SYS_NAME = '星途工厂综合管理系统'
|
||||||
SYS_VERSION = '3.0.2025122514'
|
SYS_VERSION = '3.0.2026010716'
|
||||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
@ -88,6 +88,7 @@ INSTALLED_APPS = [
|
||||||
'apps.ofm',
|
'apps.ofm',
|
||||||
'apps.srm',
|
'apps.srm',
|
||||||
'apps.asm',
|
'apps.asm',
|
||||||
|
'apps.rem'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ urlpatterns = [
|
||||||
path('', include('apps.ofm.urls')),
|
path('', include('apps.ofm.urls')),
|
||||||
path('', include('apps.srm.urls')),
|
path('', include('apps.srm.urls')),
|
||||||
path('', include('apps.asm.urls')),
|
path('', include('apps.asm.urls')),
|
||||||
|
path('', include('apps.rem.urls')),
|
||||||
|
|
||||||
# 前端页面入口
|
# 前端页面入口
|
||||||
path('', TemplateView.as_view(template_name="index.html")),
|
path('', TemplateView.as_view(template_name="index.html")),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue