diff --git a/apps/bi/views.py b/apps/bi/views.py index 411657bc..6321fe40 100644 --- a/apps/bi/views.py +++ b/apps/bi/views.py @@ -12,6 +12,8 @@ from apps.bi.services import check_sql_safe, format_json_with_placeholders from rest_framework.exceptions import ParseError from rest_framework.generics import get_object_or_404 from apps.utils.mixins import ListModelMixin +import logging +myLogger = logging.getLogger('log') # Create your views here. @@ -101,6 +103,7 @@ class DatasetViewSet(CustomModelViewSet): results[name], results2[name] = format_sqldata( res[0], res[1]) except Exception as e: + myLogger.error(f'bi查询异常:{str(e)}-{dt.code}--{sql_f}') if raise_exception: raise ParseError(f'查询异常:{str(e)}') else: diff --git a/apps/enm/migrations/0044_xscript_change_data.py b/apps/enm/migrations/0044_xscript_change_data.py new file mode 100644 index 00000000..0760d372 --- /dev/null +++ b/apps/enm/migrations/0044_xscript_change_data.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2024-08-28 08:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('enm', '0043_xscript'), + ] + + operations = [ + migrations.AddField( + model_name='xscript', + name='change_data', + field=models.JSONField(blank=True, default=dict, null=True, verbose_name='变更数据'), + ), + ] diff --git a/apps/enm/models.py b/apps/enm/models.py index 6b323ca9..593acadd 100644 --- a/apps/enm/models.py +++ b/apps/enm/models.py @@ -8,6 +8,7 @@ class Xscript(BaseModel): name = models.CharField("脚本名称", max_length=50) code = models.TextField("脚本内容", null=True, blank=True) base_data = models.JSONField("基础数据", default=dict, null=True, blank=True) + change_data = models.JSONField("变更数据", default=dict, null=True, blank=True) myschedule = models.ForeignKey('system.myschedule', verbose_name='周期', on_delete=models.SET_NULL, null=True, blank=True) periodictask = models.ForeignKey(PeriodicTask, verbose_name='关联定时任务', on_delete=models.CASCADE, related_name='xscript_periodictask', null=True, blank=True) diff --git a/apps/enm/serializers.py b/apps/enm/serializers.py index 581c90ba..c3e6f9fc 100644 --- a/apps/enm/serializers.py +++ b/apps/enm/serializers.py @@ -210,7 +210,7 @@ class XscriptSerializer(CustomModelSerializer): class Meta: model = Xscript fields = "__all__" - read_only_fields = EXCLUDE_FIELDS_BASE + ['periodictask'] + read_only_fields = EXCLUDE_FIELDS_BASE + ['periodictask', 'change_data'] def validate(self, attrs): code = attrs['code'] diff --git a/apps/enm/services.py b/apps/enm/services.py index 1fddba09..86fdff79 100644 --- a/apps/enm/services.py +++ b/apps/enm/services.py @@ -21,12 +21,9 @@ myLogger = logging.getLogger("log") def db_insert_mplogx_batch(rows): for row in rows: _, tag_val, tag_code, tag_update = row - # if '散装' in tag_code or '袋装' in tag_code: - # myLogger.info(f"db_ins_mplogx tag_val: {tag_val} tag_code: {tag_code}-------{str(type(tag_val))}") - # mpoint = Mpoint.objects.get(code=tag_code) - # tag_val = float(tag_val) - # myLogger.info(f"db_ins_mpoint_id: {mpoint.id}-------db_ins_float_val: {tag_val}") - insert_mplogx_item(tag_code, tag_val, timezone.make_aware(tag_update), {}) + if tag_update.tzinfo is None: + tag_update = timezone.make_aware(tag_update) + insert_mplogx_item(tag_code, tag_val, tag_update, {}) def translate_eval_formula(exp_str: str, year: int, month: int, day: int, hour: int): """ diff --git a/apps/enm/tasks.py b/apps/enm/tasks.py index adb5b7b7..9fd25b36 100644 --- a/apps/enm/tasks.py +++ b/apps/enm/tasks.py @@ -27,6 +27,7 @@ from apps.wpm.tasks import get_total_sec_now, cal_exp_duration_sec from apps.utils.sql import DbConnection from apps.enm.services import db_insert_mplogx_batch from apps.enm.xscript import main +from django.core.exceptions import ObjectDoesNotExist myLogger = logging.getLogger("log") @@ -130,8 +131,7 @@ def cal_mpointstat_hour(mpointId: str, year: int, month: int, day: int, hour: in dt = datetime.datetime(year=year, month=month, day=day, hour=hour, minute=0, second=0, tzinfo=mytz) # 整点时间 dt_hour_p= dt - datetime.timedelta(hours=1) # 上个整点 dt_hour_n= dt + datetime.timedelta(hours=1) # 下个整点 - - if mpoint.material and mpoint.val_type in ['float', 'int']: # 如果计量的是物料 # 累计量 有的会清零,需要额外处理(还未做) + if (mpoint.material or mpoint.type == Mpoint.MT_COMPUTE) and mpoint.val_type in ['float', 'int']: # 如果计量的是物料 # 累计量 有的会清零,需要额外处理(还未做) params = {"mpoint": mpoint, "type": "hour"} params["year"], params["month"], params["day"], params["hour"] = year, month, day, hour val = 0 @@ -141,7 +141,6 @@ def cal_mpointstat_hour(mpointId: str, year: int, month: int, day: int, hour: in val = MpLogx.objects.filter(mpoint=mpoint, timex__gte=dt, timex__lt=dt_hour_n).aggregate(sum=Sum(f'val_{mpoint.val_type}'))["sum"] if val is None: val = 0 - myLogger.info(str(val), "-------val") else: mrs0 = MpLogx.objects.filter(mpoint=mpoint, timex__gte=dt_hour_p, timex__lte=dt).order_by("timex") mrs = MpLogx.objects.filter(mpoint=mpoint, timex__gte=dt, timex__lte=dt_hour_n).order_by("timex") @@ -281,7 +280,7 @@ def cal_mpointstats(is_now=1, year=None, month=None, day=None, hour=None, m_code cal_mpointstat_hour(item.id, year, month, day, hour) # 再统计计算测点 - mpoints_compute = Mpoint.objects.filter(type=Mpoint.MT_COMPUTE, enabled=True, material__isnull=False).exclude(formula="").order_by('report_sortstr', 'create_time') + mpoints_compute = Mpoint.objects.filter(type=Mpoint.MT_COMPUTE, enabled=True).exclude(formula="").order_by('report_sortstr', 'create_time') # mpoints_other_group = [] for item in mpoints_compute: # mpoints_other_group.append(cal_mpointstat_hour.s(item.id, year, month, day, hour)) @@ -688,34 +687,42 @@ def cal_enstat2(type: str, year_s: int, month_s: int, day_s: int, cascade=True): elif type == "day_s": enstat2, _ = EnStat2.objects.get_or_create(type="day_s", year_s=year_s, month_s=month_s, day_s=day_s, defaults={"year_s": year_s, "month_s": month_s, "day_s": day_s, "type": "day_s"}) # enstat2 = EnStat2.objects.select_for_update().get(id=enstat2.id) # 加锁 - material_bulk_clinker = Material.objects.get(code="bulk_clinker") # 散装熟料 - material_bulk_cement = Material.objects.get(code="bulk_cement") # 散装水泥 - material_bag_cement = Material.objects.get(code="bag_cement") # 袋装水泥 + try: + material_bulk_clinker = Material.objects.get(code="bulk_clinker") # 散装熟料 + enstat2.bulk_clinker_price = get_price_unit(material_bulk_clinker, year_s, month_s) + except ObjectDoesNotExist: + enstat2.bulk_clinker_price = 0 + try: + material_bulk_cement = Material.objects.get(code="bulk_cement") # 散装水泥 + enstat2.bulk_cement_price = get_price_unit(material_bulk_cement, year_s, month_s) + except ObjectDoesNotExist: + enstat2.bulk_cement_price = 0 + try: + material_bag_cement = Material.objects.get(code="bag_cement") # 袋装水泥 + enstat2.bag_cement_price = get_price_unit(material_bag_cement, year_s, month_s) + except ObjectDoesNotExist: + enstat2.bag_cement_price = 0 - enstat2.bulk_cement_price = get_price_unit(material_bulk_cement, year_s, month_s) - enstat2.bulk_clinker_price = get_price_unit(material_bulk_clinker, year_s, month_s) - enstat2.bag_cement_price = get_price_unit(material_bag_cement, year_s, month_s) - if type == "month_s": - enstat2.bulk_cement_val = MpointStat.objects.filter(type="month_s", mpoint__material=material_bulk_cement, year_s=year_s, month_s=month_s).aggregate(sum=Sum("val"))["sum"] + enstat2.bulk_cement_val = MpointStat.objects.filter(type="month_s", mpoint__material__code="bulk_cement", year_s=year_s, month_s=month_s).aggregate(sum=Sum("val"))["sum"] elif type == "day_s": - enstat2.bulk_cement_val = MpointStat.objects.filter(type="day_s", mpoint__material=material_bulk_cement, year_s=year_s, month_s=month_s, day_s=day_s).aggregate(sum=Sum("val"))["sum"] + enstat2.bulk_cement_val = MpointStat.objects.filter(type="day_s", mpoint__material__code="bulk_cement", year_s=year_s, month_s=month_s, day_s=day_s).aggregate(sum=Sum("val"))["sum"] if enstat2.bulk_cement_val is None: enstat2.bulk_cement_val = 0 if type == "month_s": - enstat2.bag_cement_val = MpointStat.objects.filter(type="month_s", mpoint__material=material_bag_cement, year_s=year_s, month_s=month_s).aggregate(sum=Sum("val"))["sum"] + enstat2.bag_cement_val = MpointStat.objects.filter(type="month_s", mpoint__material__code='bag_cement', year_s=year_s, month_s=month_s).aggregate(sum=Sum("val"))["sum"] elif type == "day_s": - enstat2.bag_cement_val = MpointStat.objects.filter(type="day_s", mpoint__material=material_bag_cement, year_s=year_s, month_s=month_s, day_s=day_s).aggregate(sum=Sum("val"))["sum"] + enstat2.bag_cement_val = MpointStat.objects.filter(type="day_s", mpoint__material__code='bag_cement', year_s=year_s, month_s=month_s, day_s=day_s).aggregate(sum=Sum("val"))["sum"] if enstat2.bag_cement_val is None: enstat2.bag_cement_val = 0 if type == "month_s": - enstat2.bulk_clinker_val = MpointStat.objects.filter(type="month_s", mpoint__material=material_bulk_clinker, year_s=year_s, month_s=month_s).aggregate(sum=Sum("val"))["sum"] + enstat2.bulk_clinker_val = MpointStat.objects.filter(type="month_s", mpoint__material__code='bulk_clinker', year_s=year_s, month_s=month_s).aggregate(sum=Sum("val"))["sum"] elif type == "day_s": - enstat2.bulk_clinker_val = MpointStat.objects.filter(type="day_s", mpoint__material=material_bulk_clinker, year_s=year_s, month_s=month_s, day_s=day_s).aggregate(sum=Sum("val"))["sum"] + enstat2.bulk_clinker_val = MpointStat.objects.filter(type="day_s", mpoint__material__code='bulk_clinker', year_s=year_s, month_s=month_s, day_s=day_s).aggregate(sum=Sum("val"))["sum"] if enstat2.bulk_clinker_val is None: enstat2.bulk_clinker_val = 0 diff --git a/apps/enm/views.py b/apps/enm/views.py index 6552d645..9b8a8de5 100644 --- a/apps/enm/views.py +++ b/apps/enm/views.py @@ -92,6 +92,17 @@ class XscriptViewSet(CustomModelViewSet): periodictask.save() return Response() + @action(methods=['put'], detail=True, perms_map={'put': 'xscript.update'}) + def change_data(self, request, pk=None): + """修改变动数据 + + 修改变动数据 + """ + obj: Xscript = self.get_object() + obj.change_data = request.data.get('change_data', {}) + obj.save(update_fields=['change_data']) + return Response() + @transaction.atomic def perform_destroy(self, instance): periodictask = instance.periodictask diff --git a/apps/enm/xscript.py b/apps/enm/xscript.py index 613fbf9c..c78bd851 100644 --- a/apps/enm/xscript.py +++ b/apps/enm/xscript.py @@ -3,6 +3,8 @@ from apps.utils.sql import DbConnection from datetime import datetime, timedelta from apps.enm.services import db_insert_mplogx_batch import requests +import logging +myLogger = logging.getLogger("log") def main(xscript, mcodes_list): exec(xscript.code) diff --git a/apps/inm/serializers.py b/apps/inm/serializers.py index 9f497659..866c337b 100644 --- a/apps/inm/serializers.py +++ b/apps/inm/serializers.py @@ -10,6 +10,7 @@ from apps.utils.serializers import CustomModelSerializer from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA from django.db import transaction +from server.settings import get_sysconfig class WareHourseSerializer(CustomModelSerializer): @@ -104,7 +105,9 @@ class MIOItemCreateSerializer(CustomModelSerializer): if mio.state != MIO.MIO_CREATE: raise ValidationError('出入库记录非创建中不可新增') # 生产领料要校验是否进行检验 - if mio.type == MIO.MIO_TYPE_DO_OUT: + # 某些客户此处无需校验 + check_test_when_do_out = get_sysconfig('mes.check_test_when_do_out', True) + if check_test_when_do_out and mio.type == MIO.MIO_TYPE_DO_OUT: mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN]) if mis.exists() and (not mis.exclude(test_date=None).exists()): raise ValidationError('该批次的物料未经检验') @@ -165,6 +168,7 @@ class MIODoSerializer(CustomModelSerializer): model = MIO fields = ['id', 'number', 'note', 'do_user', 'belong_dept', 'type', 'inout_date', 'mgroup'] + extra_kwargs = {'inout_date': {'required': True}, 'do_user': {'required': True}} def validate(self, attrs): if 'mgroup' in attrs and attrs['mgroup']: @@ -190,6 +194,7 @@ class MIOSaleSerializer(CustomModelSerializer): class Meta: model = MIO fields = ['id', 'number', 'note', 'order', 'inout_date', 'customer'] + extra_kwargs = {'inout_date': {'required': True}} def create(self, validated_data): validated_data['type'] = MIO.MIO_TYPE_SALE_OUT @@ -216,6 +221,7 @@ class MIOPurSerializer(CustomModelSerializer): class Meta: model = MIO fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier'] + extra_kwargs = {'inout_date': {'required': True}} def create(self, validated_data): validated_data['type'] = MIO.MIO_TYPE_PUR_IN @@ -241,6 +247,7 @@ class MIOOtherSerializer(CustomModelSerializer): model = MIO fields = ['id', 'number', 'note', 'supplier', 'customer', 'type', 'inout_date'] + extra_kwargs = {'inout_date': {'required': True}} def create(self, validated_data): if validated_data['type'] not in [MIO.MIO_TYPE_OTHER_OUT, MIO.MIO_TYPE_OTHER_IN]: diff --git a/apps/mtm/admin.py b/apps/mtm/admin.py index 2aec3530..5de10265 100644 --- a/apps/mtm/admin.py +++ b/apps/mtm/admin.py @@ -5,7 +5,7 @@ from apps.mtm.models import Material, Shift, Mgroup, Process @admin.register(Process) class ProcessAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'cate', 'sort', 'into_wm_mgroup', 'store_notok', 'batch_append_equip') + list_display = ('id', 'name', 'cate', 'sort', 'into_wm_mgroup', 'store_notok', 'batch_append_equip', 'mlog_need_ticket') @admin.register(Material) diff --git a/apps/mtm/migrations/0040_process_mlog_need_ticket.py b/apps/mtm/migrations/0040_process_mlog_need_ticket.py new file mode 100644 index 00000000..1549d30a --- /dev/null +++ b/apps/mtm/migrations/0040_process_mlog_need_ticket.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2024-09-02 09:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0039_alter_material_unit_price'), + ] + + operations = [ + migrations.AddField( + model_name='process', + name='mlog_need_ticket', + field=models.BooleanField(default=False, verbose_name='日志提交是否需要审批'), + ), + ] diff --git a/apps/mtm/migrations/0041_process_mstate_json.py b/apps/mtm/migrations/0041_process_mstate_json.py new file mode 100644 index 00000000..84f21c3b --- /dev/null +++ b/apps/mtm/migrations/0041_process_mstate_json.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2024-09-03 02:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0040_process_mlog_need_ticket'), + ] + + operations = [ + migrations.AddField( + model_name='process', + name='mstate_json', + field=models.JSONField(blank=True, default=list, verbose_name='中间状态'), + ), + ] diff --git a/apps/mtm/models.py b/apps/mtm/models.py index 37b47f7e..5998d83a 100644 --- a/apps/mtm/models.py +++ b/apps/mtm/models.py @@ -18,6 +18,8 @@ class Process(CommonBModel): into_wm_mgroup = models.BooleanField('交接到工段', default=False) store_notok = models.BooleanField('不合格品是否入库', default=False) batch_append_equip = models.BooleanField('批号追加设备', default=False) + mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False) + mstate_json = models.JSONField('中间状态', default=list, blank=True) class Meta: verbose_name = '工序' diff --git a/apps/mtm/services.py b/apps/mtm/services.py index 4f2eb38a..dafbd031 100644 --- a/apps/mtm/services.py +++ b/apps/mtm/services.py @@ -153,10 +153,14 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict): raise ParseError('重复创建工单') if not Route.objects.filter(routepack=routepack).exists(): raise ParseError('缺少步骤') - first_route = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time').first() + r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time') + first_route = r_qs.first() + last_route = r_qs.last() if first_route.batch_bind: first_route.batch_bind = False first_route.save(update_fields=['batch_bind']) + if last_route.material_out != routepack.material: + raise ParseError('最后一步产出与工艺包不一致') ticket_data = ticket.ticket_data ticket_data.update({ 't_model': 'routepack', diff --git a/apps/pm/migrations/0020_mtaskb.py b/apps/pm/migrations/0020_mtaskb.py new file mode 100644 index 00000000..bd319ecb --- /dev/null +++ b/apps/pm/migrations/0020_mtaskb.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.12 on 2024-09-03 08:38 + +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), + ('pm', '0019_auto_20240703_1618'), + ] + + operations = [ + migrations.CreateModel( + name='Mtaskb', + 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='删除标记')), + ('count', models.PositiveIntegerField(default=0, verbose_name='任务数')), + ('handle_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mtaskb_handle_user', to=settings.AUTH_USER_MODEL, verbose_name='操作人')), + ('mtask', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='b_mtask', to='pm.mtask', verbose_name='关联任务')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/pm/models.py b/apps/pm/models.py index 391191e2..82f1d699 100644 --- a/apps/pm/models.py +++ b/apps/pm/models.py @@ -1,5 +1,5 @@ from django.db import models -from apps.utils.models import CommonADModel, CommonBDModel +from apps.utils.models import CommonADModel, CommonBDModel, BaseModel from apps.mtm.models import Material, Mgroup, RoutePack, Route # Create your models here. @@ -111,3 +111,14 @@ class Mtask(CommonADModel): def mlogs(self): from apps.wpm.models import Mlog return Mlog.objects.filter(mtask=self) + + @property + def mtaskb(self): + return Mtaskb.objects.filter(mtask=self) + + +class Mtaskb(BaseModel): + mtask = models.ForeignKey(Mtask, verbose_name='关联任务', on_delete=models.CASCADE, related_name='b_mtask') + handle_user = models.ForeignKey( + 'system.user', verbose_name='操作人', on_delete=models.CASCADE, related_name='mtaskb_handle_user') + count = models.PositiveIntegerField('任务数', default=0) \ No newline at end of file diff --git a/apps/pm/serializers.py b/apps/pm/serializers.py index c0245fe7..5bea0059 100644 --- a/apps/pm/serializers.py +++ b/apps/pm/serializers.py @@ -3,11 +3,12 @@ from rest_framework.exceptions import ValidationError, ParseError import math from apps.mtm.serializers import MaterialSimpleSerializer -from apps.pm.models import Mtask, Utask, SCHEDULE_TYPE +from apps.pm.models import Mtask, Utask, SCHEDULE_TYPE, Mtaskb from apps.sam.models import OrderItem from apps.utils.serializers import CustomModelSerializer from apps.system.models import Dept from apps.wpm.models import Mlog +from apps.utils.constants import EXCLUDE_FIELDS_BASE class UtaskSerializer(CustomModelSerializer): @@ -54,6 +55,25 @@ class MlogSimpleSerializer(CustomModelSerializer): fields = ['id', 'shift_name', 'count_use', 'count_ok', 'count_real', 'submit_time'] +class MtaskbAddSerializer(CustomModelSerializer): + handle_user_name = serializers.StringRelatedField( + source='handle_user.name', read_only=True) + class Meta: + model = Mtaskb + fields = ['id', 'handle_user', 'handle_user_name', 'count'] + +class MtaskbSerializer(CustomModelSerializer): + handle_user_name = serializers.StringRelatedField( + source='handle_user.name', read_only=True) + class Meta: + model = Mtaskb + fields = '__all__' + read_only_fields = EXCLUDE_FIELDS_BASE + +class MtaskbUpdateSerializer(CustomModelSerializer): + class Meta: + model = Mtaskb + fields = ['id', 'count'] class MtaskSerializer(CustomModelSerializer): material_out_ = MaterialSimpleSerializer( @@ -66,6 +86,7 @@ class MtaskSerializer(CustomModelSerializer): source='submit_user.name', read_only=True) mgroup_name = serializers.CharField(source='mgroup.name', read_only=True) mlogs = MlogSimpleSerializer(label='日志信息', many=True, required=False) + mtaskb = MtaskbSerializer(label='子任务信息', many=True, required=False, read_only=True) class Meta: model = Mtask @@ -109,4 +130,4 @@ class MtaskDaySerializer(serializers.Serializer): class MtaskAddInfoSerializer(CustomModelSerializer): class Meta: model = Mtask - fields = ['peifen_kg'] + fields = ['peifen_kg'] \ No newline at end of file diff --git a/apps/pm/services.py b/apps/pm/services.py index a049102c..fabdfc30 100644 --- a/apps/pm/services.py +++ b/apps/pm/services.py @@ -311,6 +311,8 @@ class PmService: now = timezone.now() if mtask.state == Mtask.MTASK_ASSGINED: mlogs = Mlog.objects.filter(mtask=mtask)|Mlog.objects.filter(b_mlog__mtask=mtask) + if not mlogs.exists(): + raise ParseError('该任务没有日志') if mlogs.filter(submit_time__isnull=True).exists(): raise ParseError('存在未提交的日志') mtask.state = Mtask.MTASK_SUBMIT diff --git a/apps/pm/urls.py b/apps/pm/urls.py index b75e9f9e..082f7d3d 100644 --- a/apps/pm/urls.py +++ b/apps/pm/urls.py @@ -1,12 +1,13 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from apps.pm.views import (MtaskViewSet, UtaskViewSet) +from apps.pm.views import (MtaskViewSet, UtaskViewSet, MtaskbViewSet) API_BASE_URL = 'api/pm/' HTML_BASE_URL = 'pm/' router = DefaultRouter() router.register('mtask', MtaskViewSet, basename='mtask') +router.register('mtaskb', MtaskbViewSet, basename='mtaskb') router.register('utask', UtaskViewSet, basename='utask') urlpatterns = [ path(API_BASE_URL, include(router.urls)), diff --git a/apps/pm/views.py b/apps/pm/views.py index eadc1cc8..391d7f90 100644 --- a/apps/pm/views.py +++ b/apps/pm/views.py @@ -8,9 +8,11 @@ from apps.utils.serializers import PkSerializer from apps.utils.viewsets import CustomModelViewSet from .filters import MtaskFilter, UtaskFilter -from .models import Mtask, Utask +from .models import Mtask, Utask, Mtaskb from .serializers import (MtaskSerializer, SchedueSerializer, UtaskSerializer, - MtaskDaySerializer, MtaskAddInfoSerializer, SchedueMtasksSerializer) + MtaskDaySerializer, MtaskAddInfoSerializer, + SchedueMtasksSerializer, MtaskbSerializer, + MtaskbUpdateSerializer, MtaskbAddSerializer) from .services import PmService from django.utils import timezone @@ -129,7 +131,7 @@ class MtaskViewSet(CustomModelViewSet): serializer_class = MtaskSerializer filterset_class = MtaskFilter select_related_fields = ['material_in', 'material_out', 'mgroup'] - prefetch_related_fields = ['mlog_mtask'] + prefetch_related_fields = ['mlog_mtask', 'b_mtask'] ordering_fields = ['start_date', 'mgroup__process__sort', 'create_time'] ordering = ['-start_date', 'mgroup__process__sort', '-create_time'] @@ -204,3 +206,38 @@ class MtaskViewSet(CustomModelViewSet): sr.is_valid(raise_exception=True) sr.save() return Response() + + @action(methods=['post'], detail=True, perms_map={'post': 'mtaskb.create'}, serializer_class=MtaskbAddSerializer) + @transaction.atomic + def add_mtaskb(self, request, *args, **kwargs): + mtask = self.get_object() + if mtask.state != Mtask.MTASK_ASSGINED: + raise ParseError('该任务不可分配') + sr = MtaskbAddSerializer(data=request.data, many=True) + sr.is_valid(raise_exception=True) + vdata = sr.validated_data + total = 0 + userIds = [] + for v in vdata: + total += v['count'] + userIds.append(v['handle_user'].id) + if total != mtask.count: + raise ParseError('任务数量与分配数量不匹配') + for v in vdata: + mtaskb, _ = Mtaskb.objects.get_or_create(mtask=mtask, handle_user=v['handle_user']) + mtaskb.count = v['count'] + mtaskb.save() + dobjs = Mtaskb.objects.filter(mtask=mtask).exclude(handle_user__in=userIds) + from apps.wpm.models import Mlog + if Mlog.objects.filter(mtaskb__in=dobjs).exists(): + raise ParseError('已存在工作日志,无法修改分配') + else: + dobjs.delete() + return Response() + +class MtaskbViewSet(CustomModelViewSet): + queryset = Mtaskb.objects.all() + serializer_class = MtaskbSerializer + update_serializer_class = MtaskbUpdateSerializer + filterset_fields = {"mtask": ["exact"], "handle_user": ["exact"], "mtask__mgroup": ["exact"]} + ordering = ['id'] \ No newline at end of file diff --git a/apps/qm/filters.py b/apps/qm/filters.py index 018cf323..162c793e 100644 --- a/apps/qm/filters.py +++ b/apps/qm/filters.py @@ -30,7 +30,10 @@ class FtestWorkFilter(filters.FilterSet): fields = { "material__process__name": ["exact", "contains"], "material": ["exact"], + "wm": ["exact"], + "mb": ["exact"], "batch": ["exact"], "type": ["exact"], "type2": ["exact"], + "shift": ["exact"] } \ No newline at end of file diff --git a/apps/qm/migrations/0022_ftestwork_count_sampling_ok.py b/apps/qm/migrations/0022_ftestwork_count_sampling_ok.py new file mode 100644 index 00000000..8f9bac26 --- /dev/null +++ b/apps/qm/migrations/0022_ftestwork_count_sampling_ok.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2024-08-30 05:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('qm', '0021_ftestwork_mb'), + ] + + operations = [ + migrations.AddField( + model_name='ftestwork', + name='count_sampling_ok', + field=models.IntegerField(default=0, verbose_name='抽检合格数量'), + ), + ] diff --git a/apps/qm/migrations/0023_ftestwork_need_update_wm.py b/apps/qm/migrations/0023_ftestwork_need_update_wm.py new file mode 100644 index 00000000..db68613e --- /dev/null +++ b/apps/qm/migrations/0023_ftestwork_need_update_wm.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2024-09-02 07:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('qm', '0022_ftestwork_count_sampling_ok'), + ] + + operations = [ + migrations.AddField( + model_name='ftestwork', + name='need_update_wm', + field=models.BooleanField(default=True, verbose_name='是否更新车间库存'), + ), + ] diff --git a/apps/qm/migrations/0024_ftestwork_shift.py b/apps/qm/migrations/0024_ftestwork_shift.py new file mode 100644 index 00000000..7743caaa --- /dev/null +++ b/apps/qm/migrations/0024_ftestwork_shift.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2024-09-11 08:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0041_process_mstate_json'), + ('qm', '0023_ftestwork_need_update_wm'), + ] + + operations = [ + migrations.AddField( + model_name='ftestwork', + name='shift', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mtm.shift', verbose_name='班次'), + ), + ] diff --git a/apps/qm/models.py b/apps/qm/models.py index 1d0944f9..d69b2854 100644 --- a/apps/qm/models.py +++ b/apps/qm/models.py @@ -1,7 +1,7 @@ from django.db import models from apps.system.models import CommonAModel, CommonADModel, User from apps.utils.models import CommonBDModel, BaseModel -from apps.mtm.models import Material, Mgroup, Team +from apps.mtm.models import Material, Mgroup, Team, Shift from apps.wpm.models import SfLog, WMaterial from django.utils.translation import gettext_lazy as _ @@ -56,6 +56,8 @@ class NotOkOption(models.TextChoices): z = "z", _("脏") zhg = "zhg", _("准合格") yz = "yz", _("圆准") + + jgqbl = "jgqbl", _("加工前不良") qt = "qt", _("其它") @@ -109,6 +111,7 @@ class FtestWork(CommonBDModel): """ type = models.CharField('检验类型', max_length=20, choices=FTEST_TYPE_CHOICES, default='prod') type2 = models.PositiveSmallIntegerField('检验类型2', choices=((10, '抽检'), (20, '全检')), default=10) + shift = models.ForeignKey(Shift, verbose_name='班次', on_delete=models.SET_NULL, null=True, blank=True) wm = models.ForeignKey(WMaterial, verbose_name='关联车间库存', on_delete=models.SET_NULL, null=True, blank=True) mb = models.ForeignKey('inm.materialbatch', verbose_name='关联仓库', on_delete=models.SET_NULL, null=True, blank=True) test_date = models.DateField('检验日期') @@ -117,8 +120,10 @@ class FtestWork(CommonBDModel): batch = models.CharField('生产批次', max_length=50) count = models.IntegerField('检验数量') count_sampling = models.IntegerField('抽检数量', default=0) + count_sampling_ok = models.IntegerField('抽检合格数量', default=0) count_ok = models.IntegerField('合格数量', default=0) count_notok = models.IntegerField('不合格数量', default=0) + need_update_wm = models.BooleanField('是否更新车间库存', default=True) count_notok_json = models.JSONField('不合格项数量统计', default=dict, null=False, blank=True) test_user = models.ForeignKey( User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftestwork_test_user', null=True, blank=True) diff --git a/apps/qm/serializers.py b/apps/qm/serializers.py index bcfb7862..29c56cf0 100644 --- a/apps/qm/serializers.py +++ b/apps/qm/serializers.py @@ -66,7 +66,7 @@ class QuaStatUpdateSerializer(CustomModelSerializer): class FtestWorkCreateUpdateSerializer(CustomModelSerializer): class Meta: model = FtestWork - fields = ['id', 'wm', 'mb', 'type', 'type2', 'test_date', 'count', 'count_sampling', 'count_ok', 'count_notok', 'count_notok_json', 'test_user'] + fields = ['id', 'shift', 'wm', 'mb', 'type', 'type2', 'test_date', 'count', 'count_sampling', 'count_sampling_ok', 'count_ok', 'count_notok', 'count_notok_json', 'test_user', 'need_update_wm'] extra_kwargs = {'test_user': {'required': True}, 'type': {'required': True}} def validate(self, attrs): @@ -99,6 +99,8 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer): attrs['batch'] = attrs['mb'].batch else: raise ValidationError('请选择车间/仓库库存') + if attrs['test_user']: + attrs['belong_dept'] = attrs['test_user'].belong_dept return attrs @@ -107,6 +109,7 @@ class FtestWorkSerializer(CustomModelSerializer): source='material', read_only=True) material_cate = serializers.CharField(source='material.cate', read_only=True) mb_ = MaterialBatchDetailSerializer(source='mb', read_only=True) + test_user_name = serializers.CharField(source='test_user.name', read_only=True) class Meta: model = FtestWork diff --git a/apps/qm/services.py b/apps/qm/services.py index 2a107090..e984855b 100644 --- a/apps/qm/services.py +++ b/apps/qm/services.py @@ -7,67 +7,68 @@ from django.utils import timezone def ftestwork_submit(ins:FtestWork, user: User): wm:WMaterial = ins.wm - if wm.state == WMaterial.WM_TEST: - # 更新对应的车间库存 - wm.count = wm.count - ins.count - if wm.count >= 0: - # 已检测的数量 - wm.count_xtest = wm.count_xtest + ins.count - wm.save() - else: - raise ParseError("超过待检数量") - # 生成合格的 - count_ok = ins.count_ok - if count_ok > 0: - wm, new_create = WMaterial.objects.get_or_create( - material=wm.material, - batch=wm.batch, - mgroup=wm.mgroup, - belong_dept=wm.belong_dept, - state=WMaterial.WM_OK, - defaults={ - 'count': count_ok, - 'material': wm.material, - 'batch': wm.batch, - 'mgroup': wm.mgroup, - 'belong_dept': wm.belong_dept, - } - ) - if not new_create: - wm.count = wm.count + count_ok + if ins.need_update_wm: + if wm.state == WMaterial.WM_TEST: + # 更新对应的车间库存 + wm.count = wm.count - ins.count + if wm.count >= 0: + # 已检测的数量 + wm.count_xtest = wm.count_xtest + ins.count wm.save() - else: - wm.count = wm.count - ins.count_notok - if wm.count >= 0: - wm.save() + else: + raise ParseError("超过待检数量") + # 生成合格的 + count_ok = ins.count_ok + if count_ok > 0: + wm, new_create = WMaterial.objects.get_or_create( + material=wm.material, + batch=wm.batch, + mgroup=wm.mgroup, + belong_dept=wm.belong_dept, + state=WMaterial.WM_OK, + defaults={ + 'count': count_ok, + 'material': wm.material, + 'batch': wm.batch, + 'mgroup': wm.mgroup, + 'belong_dept': wm.belong_dept, + } + ) + if not new_create: + wm.count = wm.count + count_ok + wm.save() else: - raise ParseError("不合格数不可大于批次数量") - - # 生成不合格的 - count_notok_json = ins.count_notok_json - for k, v in count_notok_json.items(): - if v > 0: - notok_sign = k.replace('count_n_', '') - wm_n, new_create = WMaterial.objects.get_or_create( - material=wm.material, - batch=wm.batch, - mgroup=wm.mgroup, - belong_dept=wm.belong_dept, - notok_sign=notok_sign, - state=WMaterial.WM_NOTOK, - defaults={ - 'count': v, - 'material': wm.material, - 'batch': wm.batch, - 'mgroup': wm.mgroup, - 'belong_dept': wm.belong_dept, - 'notok_sign': notok_sign, - 'state': WMaterial.WM_NOTOK, - } - ) - if not new_create: - wm_n.count = wm_n.count + v - wm_n.save() + wm.count = wm.count - ins.count_notok + if wm.count >= 0: + wm.save() + else: + raise ParseError("不合格数不可大于批次数量") + + # 生成不合格的 + count_notok_json = ins.count_notok_json + for k, v in count_notok_json.items(): + if v > 0: + notok_sign = k.replace('count_n_', '') + wm_n, new_create = WMaterial.objects.get_or_create( + material=wm.material, + batch=wm.batch, + mgroup=wm.mgroup, + belong_dept=wm.belong_dept, + notok_sign=notok_sign, + state=WMaterial.WM_NOTOK, + defaults={ + 'count': v, + 'material': wm.material, + 'batch': wm.batch, + 'mgroup': wm.mgroup, + 'belong_dept': wm.belong_dept, + 'notok_sign': notok_sign, + 'state': WMaterial.WM_NOTOK, + } + ) + if not new_create: + wm_n.count = wm_n.count + v + wm_n.save() ins.submit_user = user ins.submit_time = timezone.now() ins.save() \ No newline at end of file diff --git a/apps/wpm/datax.py b/apps/wpm/datax.py new file mode 100644 index 00000000..48f37bf6 --- /dev/null +++ b/apps/wpm/datax.py @@ -0,0 +1,111 @@ +from apps.utils.viewsets import GenericViewSet +from rest_framework.decorators import action +from apps.em.models import Equipment +from apps.wpm.models import Mlog, Handover +from apps.mtm.models import Mgroup +from django.utils import timezone +from django.db.models import Sum +from datetime import datetime, timedelta +from rest_framework.response import Response +from rest_framework.serializers import Serializer +from apps.inm.models import MIOItem, MIO + +def tran_time_to_mstate(mstate_json, reminder_interval_list, work_start_time: datetime, now: datetime): + if len(reminder_interval_list) != len(mstate_json): + return '未运行' + xval = 0 + for ind, val in enumerate(reminder_interval_list): + xval += val + if work_start_time + timedelta(minutes=xval) > now: + return mstate_json[ind]['name'] + return '未运行' + +class AnaViewSet(GenericViewSet): + perms_map = {} + @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=Serializer) + def equip_last_mlog(self, request): + """设备最后生产日志 + + 设备最后生产日志 + """ + data = request.data + now = timezone.now() + mgroup: Mgroup = Mgroup.objects.get(name=data['mgroup_name']) + # 子状态 + mstate_json = mgroup.process.mstate_json + # 生产设备 + equip_qs = Equipment.objects.filter(mgroup=mgroup, type = 10) + equip_qs_v = equip_qs.values('id', 'name', 'number', 'state').order_by('number') + equip_qs_l = list(equip_qs_v) + # 设备最后生产日志 + mlog_qs_0 = Mlog.objects.filter(equipment__in=equip_qs, mgroup=mgroup) + mlog_qs = mlog_qs_0.exclude(work_end_time__lt=now) + mlog_qs = mlog_qs.annotate(t_count_use=Sum('b_mlog__count_use')) + mlog_qs = mlog_qs.order_by('work_start_time') + mlog_qs_v = mlog_qs.values('id', 'equipment__id', 't_count_use', 'reminder_interval_list', 'work_start_time', 'work_end_time') + mlog_qs_l = list(mlog_qs_v) + mlog_dict = {item['equipment__id']: item for item in mlog_qs_l} + + # 统计数据 + 保温 = 0 + 冷却 = 0 + 未运行 = 0 + 故障 = 0 + now = timezone.now() + for item in equip_qs_l: + item['mstate'] = '未运行' + if item['id'] in mlog_dict: + mlog_dict_v = mlog_dict[item['id']] + item['t_count_use'] = mlog_dict_v['t_count_use'] + item['reminder_interval_list'] = mlog_dict_v['reminder_interval_list'] + item['work_start_time'] = timezone.localtime(mlog_dict_v['work_start_time']).strftime('%Y-%m-%d %H:%M:%S') if mlog_dict_v['work_start_time'] else None + item['work_end_time'] = timezone.localtime(mlog_dict_v['work_end_time']).strftime('%Y-%m-%d %H:%M:%S') if mlog_dict_v['work_end_time'] else None + item['mstate'] = tran_time_to_mstate(mstate_json, mlog_dict_v['reminder_interval_list'], mlog_dict_v['work_start_time'], now) + + if item['state'] in [Equipment.EQUIP_STATE_SCRAP, Equipment.EQUIP_STATE_FIX]: + item['mstate'] = '故障' + + if item['mstate'] == '保温': + 保温 += 1 + elif item['mstate'] == '冷却': + 冷却 += 1 + elif item['mstate'] == '未运行': + 未运行 += 1 + elif item['mstate'] == '故障': + 故障 += 1 + ret = {"保温": 保温, "冷却": 冷却, "未运行": 未运行, "故障": 故障} + ret['mstate_json'] = mstate_json + ret['now'] = timezone.localtime(now).strftime('%Y-%m-%d %H:%M:%S') + ret["rows"] = equip_qs_l + return Response(ret) + + @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=Serializer) + def put_prod(self, request): + """ + 投产分析 + """ + now = timezone.now() + now_2 = now.replace(hour=2, minute=0, second=0, microsecond=0) + mgroup: Mgroup = Mgroup.objects.get(name='退火') + # 子状态 + mstate_json = mgroup.process.mstate_json + ret = {} + ret['今日退火投产预测'] = 0 + ret['明日退火投产预测'] = 0 + ret['rows'] = {} + # 生产记录/今日退火投产预测 + # 昨日2点 + now_2_yesterday = now_2 - timedelta(days=1) + + mlog_qs_0 = Mlog.objects.filter(mgroup=mgroup) + mlog_qs = mlog_qs_0.exclude(work_end_time__lt=now_2_yesterday).annotate(t_count_use=Sum('b_mlog__count_use')).order_by('work_start_time') + mlog_qs_v = mlog_qs.values('id', 'equipment__id', 't_count_use', 'reminder_interval_list', 'work_start_time', 'work_end_time') + mlog_qs_l = list(mlog_qs_v) + + for item in mlog_qs_l: + if tran_time_to_mstate(mstate_json, item['reminder_interval_list'], item['work_start_time'], now) == '冷却': + if item['work_end_time'] is None or item['work_end_time'] > now_2: + ret['明日退火投产预测'] += item['t_count_use'] + ret['今日退火投产预测'] += item['t_count_use'] + + return Response(ret) \ No newline at end of file diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index 9d4a1077..cd296143 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -50,7 +50,8 @@ class WMaterialFilter(filters.FilterSet): "mgroup__name": ["exact", "in"], "count": ["gte", "lte", "exact"], "notok_sign": ["exact", "in", "isnull"], - "count_xtest": ["gte", "isnull"] + "count_xtest": ["gte", "isnull"], + "supplier": ["exact"], } class MlogFilter(filters.FilterSet): @@ -65,15 +66,20 @@ class MlogFilter(filters.FilterSet): "mtask__mgroup__belong_dept__name": ["exact", "contains", "in"], "mgroup__belong_dept__name": ["exact", "in", "contains"], "mgroup__name": ["exact", "in", "contains"], - "submit_time": ["isnull"] + "submit_time": ["isnull"], + "fmlog": ["exact"] } class HandoverFilter(filters.FilterSet): mgroup = filters.CharFilter(label='MgroupId', method='filter_mgroup') + dept = filters.CharFilter(label='DeptId', method='filter_dept') def filter_mgroup(self, queryset, name, value): return queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value) + + def filter_dept(self, queryset, name, value): + return queryset.filter(send_dept__id=value)|queryset.filter(recive_dept__id=value) class Meta: model = Handover diff --git a/apps/wpm/migrations/0063_wmaterial_supplier.py b/apps/wpm/migrations/0063_wmaterial_supplier.py new file mode 100644 index 00000000..7948d9dc --- /dev/null +++ b/apps/wpm/migrations/0063_wmaterial_supplier.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2024-08-30 01:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pum', '0008_auto_20240731_1829'), + ('wpm', '0062_auto_20240828_1052'), + ] + + operations = [ + migrations.AddField( + model_name='wmaterial', + name='supplier', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pum.supplier', verbose_name='外协供应商'), + ), + ] diff --git a/apps/wpm/migrations/0064_auto_20240902_1411.py b/apps/wpm/migrations/0064_auto_20240902_1411.py new file mode 100644 index 00000000..8820b4a6 --- /dev/null +++ b/apps/wpm/migrations/0064_auto_20240902_1411.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2024-09-02 06:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wpm', '0063_wmaterial_supplier'), + ] + + operations = [ + migrations.AddField( + model_name='mlog', + name='count_pn_jgqbl', + field=models.PositiveIntegerField(default=0, verbose_name='加工前不良'), + ), + migrations.AddField( + model_name='mlogb', + name='count_pn_jgqbl', + field=models.PositiveIntegerField(default=0, verbose_name='加工前不良'), + ), + ] diff --git a/apps/wpm/migrations/0065_auto_20240903_0904.py b/apps/wpm/migrations/0065_auto_20240903_0904.py new file mode 100644 index 00000000..ab6836dc --- /dev/null +++ b/apps/wpm/migrations/0065_auto_20240903_0904.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2024-09-03 01:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wf', '0002_alter_state_filter_dept'), + ('wpm', '0064_auto_20240902_1411'), + ] + + operations = [ + migrations.AddField( + model_name='mlog', + name='oinfo_json', + field=models.JSONField(blank=True, default=dict, verbose_name='其他信息'), + ), + migrations.AddField( + model_name='mlog', + name='ticket', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mlog_ticket', to='wf.ticket', verbose_name='关联工单'), + ), + ] diff --git a/apps/wpm/migrations/0066_auto_20240903_1401.py b/apps/wpm/migrations/0066_auto_20240903_1401.py new file mode 100644 index 00000000..98e1ff94 --- /dev/null +++ b/apps/wpm/migrations/0066_auto_20240903_1401.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.12 on 2024-09-03 06:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wpm', '0065_auto_20240903_0904'), + ] + + operations = [ + migrations.AddField( + model_name='mlog', + name='test_file', + field=models.TextField(blank=True, null=True, verbose_name='检验文件'), + ), + migrations.AddField( + model_name='mlogb', + name='count_notok_json', + field=models.JSONField(blank=True, default=list, verbose_name='不合格情况'), + ), + migrations.AddField( + model_name='mlogb', + name='note', + field=models.TextField(blank=True, default='', verbose_name='备注'), + ), + ] diff --git a/apps/wpm/migrations/0067_auto_20240904_0903.py b/apps/wpm/migrations/0067_auto_20240904_0903.py new file mode 100644 index 00000000..7b2a4520 --- /dev/null +++ b/apps/wpm/migrations/0067_auto_20240904_0903.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.12 on 2024-09-04 01:03 + +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 = [ + ('mtm', '0041_process_mstate_json'), + ('pm', '0020_mtaskb'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('wpm', '0066_auto_20240903_1401'), + ] + + operations = [ + migrations.CreateModel( + name='Fmlog', + 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='删除标记')), + ('note', models.TextField(blank=True, default='', verbose_name='备注')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fmlog_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('mgroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fmlog_mgroup', to='mtm.mgroup', verbose_name='工段')), + ('mtask', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fmlog_mtask', to='pm.mtask', verbose_name='任务')), + ('route', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mtm.route', verbose_name='生产路线')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fmlog_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='mlog', + name='fmlog', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mlog_fmlog', to='wpm.fmlog', verbose_name='关联生产日志'), + ), + ] diff --git a/apps/wpm/migrations/0068_auto_20240904_1549.py b/apps/wpm/migrations/0068_auto_20240904_1549.py new file mode 100644 index 00000000..968abaea --- /dev/null +++ b/apps/wpm/migrations/0068_auto_20240904_1549.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.12 on 2024-09-04 07:49 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pm', '0020_mtaskb'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('wpm', '0067_auto_20240904_0903'), + ] + + operations = [ + migrations.AddField( + model_name='mlog', + name='mtaskb', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mlog_mtaskb', to='pm.mtaskb', verbose_name='关联个人任务'), + ), + migrations.AddField( + model_name='mlog', + name='test_time', + field=models.DateTimeField(blank=True, null=True, verbose_name='检验时间'), + ), + migrations.AddField( + model_name='mlog', + name='test_user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mlog_test_user', to=settings.AUTH_USER_MODEL, verbose_name='检验人'), + ), + migrations.AlterField( + model_name='mlog', + name='fill_way', + field=models.PositiveSmallIntegerField(default=10, help_text='10:仅二级;20:二三级;30:一二级', verbose_name='填写方式'), + ), + ] diff --git a/apps/wpm/migrations/0069_auto_20240905_1057.py b/apps/wpm/migrations/0069_auto_20240905_1057.py new file mode 100644 index 00000000..efbae9e8 --- /dev/null +++ b/apps/wpm/migrations/0069_auto_20240905_1057.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.12 on 2024-09-05 02:57 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('wpm', '0068_auto_20240904_1549'), + ] + + operations = [ + migrations.AlterField( + model_name='handover', + name='batch', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='批次号'), + ), + migrations.AlterField( + model_name='handover', + name='type', + field=models.PositiveSmallIntegerField(choices=[(10, '正常交接'), (20, '返修交接'), (30, '检验交接'), (40, '报废交接')], default=10, verbose_name='交接类型'), + ), + migrations.CreateModel( + name='Handoverb', + 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='删除标记')), + ('count', models.PositiveIntegerField(default=0, verbose_name='送料数')), + ('handover', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wpm.handover', verbose_name='关联交接记录')), + ('wm', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='handoverb_wm', to='wpm.wmaterial', verbose_name='关联车间库存')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/wpm/models.py b/apps/wpm/models.py index 41237f6f..34b91399 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -1,13 +1,14 @@ from django.db import models from apps.utils.models import CommonADModel, CommonBDModel, BaseModel from apps.mtm.models import Mgroup, Team, Shift, Material, Route -from apps.pm.models import Mtask +from apps.pm.models import Mtask, Mtaskb from apps.system.models import User from django.utils.timezone import localtime from apps.em.models import Equipment from apps.system.models import Dept from datetime import timedelta from apps.pum.models import Supplier +from django.db.models import Sum # Create your models here. @@ -97,6 +98,7 @@ class WMaterial(CommonBDModel): state = models.PositiveSmallIntegerField('状态', default=10, choices=((10, '合格'), (20, '不合格'), (30, '返修'), (40, '检验'), (50, '报废'))) material = models.ForeignKey( Material, verbose_name='物料', on_delete=models.CASCADE, related_name='wm_m') + supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True) mgroup = models.ForeignKey(Mgroup, verbose_name='所在工段', on_delete=models.CASCADE, null=True, blank=True) batch = models.CharField('批次号', max_length=50) count = models.PositiveIntegerField('当前数量', default=0) @@ -105,17 +107,29 @@ class WMaterial(CommonBDModel): material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mo') count_xtest = models.PositiveIntegerField('已检数量', null=True, blank=True) + @property + def count_working(self): + return int(Mlogb.objects.filter(wm_in=self, mlog__work_end_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0) + +class Fmlog(CommonADModel): + route = models.ForeignKey(Route, verbose_name='生产路线', on_delete=models.SET_NULL, null=True, blank=True) + mtask = models.ForeignKey(Mtask, verbose_name='任务', on_delete=models.CASCADE, related_name='fmlog_mtask') + mgroup = models.ForeignKey(Mgroup, verbose_name='工段', on_delete=models.CASCADE, related_name='fmlog_mgroup') + note = models.TextField('备注', default='', blank=True) class Mlog(CommonADModel): """ 生产日志 """ # 变成父级的字段 - MLOG_ONETIME = 10 - MLOG_STEP = 20 + MLOG_2 = 10 + MLOG_23 = 20 + MLOG_12 = 30 MTYPE_SELF = 10 MTYPE_OUT = 20 - fill_way = models.PositiveSmallIntegerField("填写方式", default=10, help_text='10:一次填写;20:分步填写') + fmlog = models.ForeignKey(Fmlog, verbose_name='关联生产日志', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlog_fmlog') + mtaskb = models.ForeignKey(Mtaskb, verbose_name='关联个人任务', on_delete=models.CASCADE, related_name='mlog_mtaskb', null=True, blank=True) + fill_way = models.PositiveSmallIntegerField("填写方式", default=10, help_text='10:仅二级;20:二三级;30:一二级') mtype = models.PositiveSmallIntegerField('生产类型', default=10, help_text='10:自生产;20:外协生产', choices=((10, '自生产'), (20, '外协生产'))) supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True) work_start_time = models.DateTimeField('生产开始时间', null=True, blank=True) @@ -154,6 +168,7 @@ class Mlog(CommonADModel): count_notok = models.PositiveIntegerField('不合格数', default=0) count_break_t = models.PositiveIntegerField('检验碎料数', default=0) + count_pn_jgqbl = models.PositiveIntegerField('加工前不良', default=0) count_n_zw = models.PositiveIntegerField('炸纹', default=0) count_n_tw = models.PositiveIntegerField('条纹', default=0) count_n_qp = models.PositiveIntegerField('气泡', default=0) @@ -191,8 +206,6 @@ class Mlog(CommonADModel): count_n_yd = models.PositiveIntegerField('圆度', default=0) count_n_txd = models.PositiveIntegerField('同心度', default=0) count_n_hd = models.PositiveIntegerField('厚度', default=0) - - count_n_qt = models.PositiveIntegerField('其他', default=0) handle_date = models.DateField('操作日期', null=True, blank=True) @@ -212,6 +225,14 @@ class Mlog(CommonADModel): submit_user = models.ForeignKey( User, verbose_name='提交人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_submit_user') + oinfo_json = models.JSONField('其他信息', default=dict, blank=True) + ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单', + on_delete=models.SET_NULL, related_name='mlog_ticket', null=True, blank=True, db_constraint=False) + test_file = models.TextField('检验文件', null=True, blank=True) + test_user = models.ForeignKey( + User, verbose_name='检验人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_test_user') + test_time = models.DateTimeField('检验时间', null=True, blank=True) + @property def mlogb(self): return Mlogb.objects.filter(mlog=self).exclude(material_out=None) @@ -229,6 +250,7 @@ class Mlog(CommonADModel): class Mlogb(BaseModel): mlog = models.ForeignKey(Mlog, verbose_name='关联日志', on_delete=models.CASCADE, related_name='b_mlog') + note = models.TextField('备注', default='', blank=True) batch = models.CharField('批次号', max_length=50, null=True, blank=True) mtask = models.ForeignKey(Mtask, verbose_name='关联任务', on_delete=models.CASCADE, related_name='mlogb_mtask', null=True, blank=True) @@ -245,6 +267,7 @@ class Mlogb(BaseModel): count_ok = models.PositiveIntegerField('合格数量', default=0) count_notok = models.PositiveIntegerField('不合格数', default=0) + count_pn_jgqbl = models.PositiveIntegerField('加工前不良', default=0) # 添加不合格字段后需要更改cal_mlog_count_from_mlogb count_n_hs = models.PositiveIntegerField('划伤', default=0) count_n_qp = models.PositiveIntegerField('气泡', default=0) @@ -260,6 +283,7 @@ class Mlogb(BaseModel): count_n_txd = models.PositiveIntegerField('同心度', default=0) count_n_hd = models.PositiveIntegerField('厚度', default=0) count_n_qt = models.PositiveIntegerField('其他', default=0) + count_notok_json = models.JSONField('不合格情况', default=list, blank=True) class Handover(CommonADModel): """ @@ -268,7 +292,8 @@ class Handover(CommonADModel): H_NORMAL = 10 H_REPAIR = 20 H_TEST = 30 - type = models.PositiveSmallIntegerField('交接类型', choices=[(H_NORMAL, '正常交接'), (H_REPAIR, '返修交接'), (H_TEST, '检验交接')], default=H_NORMAL) + H_SCRAP = 40 + type = models.PositiveSmallIntegerField('交接类型', choices=[(H_NORMAL, '正常交接'), (H_REPAIR, '返修交接'), (H_TEST, '检验交接'), (H_SCRAP, '报废交接')], default=H_NORMAL) send_date = models.DateField('送料日期') send_user = models.ForeignKey( User, verbose_name='交送人', on_delete=models.CASCADE, related_name='handover_send_user') @@ -276,7 +301,7 @@ class Handover(CommonADModel): Mgroup, verbose_name='送料工段', on_delete=models.CASCADE, null=True, blank=True) send_dept = models.ForeignKey( Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept') - batch = models.CharField('批次号', max_length=50) + batch = models.CharField('批次号', max_length=50, null=True, blank=True) material = models.ForeignKey( Material, verbose_name='物料', on_delete=models.CASCADE, related_name='h_ma') material_changed = models.ForeignKey(Material, verbose_name='变更后物料', on_delete=models.CASCADE, null=True, blank=True, related_name='h_ma_c') @@ -298,6 +323,15 @@ class Handover(CommonADModel): submit_user = models.ForeignKey( User, verbose_name='提交人', on_delete=models.CASCADE, null=True, blank=True, related_name='handover_submit_user') + @property + def handoverb(self): + return Handoverb.objects.filter(handover=self) + +class Handoverb(BaseModel): + handover = models.ForeignKey(Handover, verbose_name='关联交接记录', on_delete=models.CASCADE) + wm = models.ForeignKey(WMaterial, verbose_name='关联车间库存', on_delete=models.SET_NULL, + null=True, blank=True, related_name='handoverb_wm') + count = models.PositiveIntegerField('送料数', default=0) class AttLog(CommonADModel): """ diff --git a/apps/wpm/serializers.py b/apps/wpm/serializers.py index 064b610e..f84ee9fa 100644 --- a/apps/wpm/serializers.py +++ b/apps/wpm/serializers.py @@ -4,12 +4,13 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError, ParseError from datetime import datetime -from .models import SfLog, StLog, SfLogExp, WMaterial, Mlog, Handover, Mlogb, AttLog, OtherLog +from .models import (SfLog, StLog, SfLogExp, WMaterial, Mlog, + Handover, Handoverb, Mlogb, AttLog, OtherLog, Fmlog) from apps.system.models import Dept, User from apps.system.serializers import UserSimpleSerializer -from apps.pm.models import Mtask +from apps.pm.models import Mtask, Mtaskb from apps.wpm.tasks import cal_enstat_when_pcoal_heat_change, cal_enstat_when_team_change, cal_exp_duration_sec -from apps.wpm.services import get_sflog, find_material_can_change +from apps.wpm.services import get_sflog, find_material_can_change, generate_new_batch from apps.mtm.models import Mgroup, TeamMember, Shift, Material, Route from apps.mtm.serializers import MaterialSimpleSerializer from django.db import transaction @@ -17,7 +18,7 @@ from django.utils import timezone from django.core.cache import cache from django.utils.timezone import localdate from apps.qm.models import NotOkOption - +from apps.wf.serializers import TicketSimpleSerializer class OtherLogSerializer(CustomModelSerializer): class Meta: @@ -172,6 +173,7 @@ class SflogExpSerializer(CustomModelSerializer): class WMaterialSerializer(CustomModelSerializer): material_ = MaterialSimpleSerializer(source='material', read_only=True) + supplier_name = serializers.CharField(source='supplier.name', read_only=True) material_name = serializers.StringRelatedField( source='material', read_only=True) mgroup_name = serializers.StringRelatedField(source='mgroup.name', read_only=True) @@ -179,6 +181,7 @@ class WMaterialSerializer(CustomModelSerializer): source='belong_dept.name', read_only=True) material_origin_name = serializers.StringRelatedField(source='material_origin', read_only=True) notok_sign_name = serializers.SerializerMethodField() + count_working = serializers.CharField(read_only=True, label='在制数量') def get_notok_sign_name(self, obj): return getattr(NotOkOption, obj.notok_sign, NotOkOption.qt).label if obj.notok_sign else None @@ -211,7 +214,9 @@ class MlogbDetailSerializer(CustomModelSerializer): fields = '__all__' class MlogSerializer(CustomModelSerializer): + mstate_json = serializers.JSONField(source='mgroup.process.mstate_json', read_only=True) supplier_name = serializers.CharField(source='supplier.name', read_only=True) + routepack_name = serializers.CharField(source='route.routepack.name', read_only=True) belong_dept = serializers.CharField( source='mgroup.belong_dept.id', read_only=True) belong_dept_name = serializers.CharField( @@ -260,6 +265,8 @@ class MlogSerializer(CustomModelSerializer): source='handle_users', many=True, read_only=True) equipments_name = serializers.StringRelatedField( source='equipments', read_only=True, many=True) + ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) + test_user_name = serializers.CharField(source='test_user.name', read_only=True) class Meta: model = Mlog @@ -271,21 +278,23 @@ class MlogSerializer(CustomModelSerializer): } def create(self, validated_data): - mtask: Mtask = validated_data.get('mtask', None) - if mtask: - validated_data['mgroup'] = mtask.mgroup - validated_data['material_in'] = mtask.material_in - material_out = mtask.material_out - validated_data['material_out'] = material_out - validated_data['handle_date'] = mtask.end_date - else: - mgroup = validated_data['mgroup'] - material_out = validated_data['material_out'] - if not (mgroup and material_out): - raise ValidationError('缺少工段或产物!') + material_out = validated_data['material_out'] with transaction.atomic(): mlogb = validated_data.pop('mlogb', []) instance: Mlog = super().create(validated_data) + # 自动生成mlogb + batch_in = instance.batch + if instance.wm_in: + batch_in = instance.wm_in.batch + add_dict = { + 'mlog': instance, 'batch': batch_in, 'wm_in': instance.wm_in, + 'mtask': instance.mtask, 'material_in': instance.material_in, + 'count_use': instance.count_use, 'count_break': instance.count_break, + 'count_pn_jgqbl': instance.count_pn_jgqbl + } + Mlogb.objects.create(**add_dict) + + # mlogb只用于组合件输出物填写 brotherId_should_list = material_out.brothers if brotherId_should_list: if mlogb: @@ -295,9 +304,29 @@ class MlogSerializer(CustomModelSerializer): mlog=instance, batch=instance.batch, mtask=instance.mtask, material_out=item['material_out'], count_ok=item['count_ok']) else: raise ValidationError('缺少产出物信息') + else: + # 生成产出物 + batch_out = validated_data.get('batch', None) + if batch_out: + pass + else: + batch_out = generate_new_batch(batch_in, instance) + + add_dict_2 = { + 'mlog': instance, 'batch': batch_out, + 'mtask': instance.mtask, 'material_out': instance.material_out, + 'count_ok': instance.count_ok, 'count_notok': instance.count_notok, + 'count_break_t': instance.count_break_t + } + for f in Mlogb._meta.fields: + if 'count_n_' in f.name: + add_dict_2[f.name] = getattr(instance, f.name) + Mlogb.objects.create(**add_dict_2) return instance def update(self, instance, validated_data): + if instance.fill_way == Mlog.MLOG_23: + raise ParseError('不支持的填写类型') validated_data.pop('mtask', None) validated_data.pop('mgroup', None) if instance.mtask: @@ -305,17 +334,70 @@ class MlogSerializer(CustomModelSerializer): # validated_data.pop('handle_user', None) with transaction.atomic(): mlogb = validated_data.pop('mlogb', []) - instance = super().update(instance, validated_data) - if mlogb: + instance: Mlog = super().update(instance, validated_data) + if instance.fill_way == Mlog.MLOG_12: + # 自动生成mlogb + batch_in = instance.batch + if instance.wm_in: + batch_in = instance.wm_in.batch + minx, _ = Mlogb.objects.get_or_create( + mlog=instance, + batch=batch_in, + wm_in=instance.wm_in, + mtask=instance.mtask, + material_in=instance.material_in + ) + minx.count_use = instance.count_use + minx.count_break = instance.count_break + minx.count_pn_jgqbl = instance.count_pn_jgqbl + minx.save() + + if mlogb and instance.fill_way == Mlog.MLOG_2: Mlogb.objects.filter(mlog=instance, material_out__isnull=False).update(count_ok=0) for item in mlogb: Mlogb.objects.filter(mlog=instance, material_out=item['material_out']).update( count_ok=item['count_ok']) + elif instance.fill_way == Mlog.MLOG_12: + # 生成产出物 + batch_out = instance.batch + if batch_out: + pass + else: + batch_out = generate_new_batch(batch_in, instance) + + mox, _ = Mlogb.objects.get_or_create(mlog=instance, batch=batch_out, + mtask=instance.mtask, material_out=instance.material_out) + mox.count_ok = instance.count_ok + mox.count_notok = instance.count_notok + mox.count_break_t = instance.count_break_t + for f in Mlogb._meta.fields: + if 'count_n_' in f.name: + setattr(mox, f.name, getattr(instance, f.name)) + mox.save() + Mlogb.objects.filter(mlog=instance, material_out__isnull=False).exclude(id=mox.id).delete() return instance def validate(self, attrs): - attrs['fill_way'] = Mlog.MLOG_ONETIME + attrs['fill_way'] = Mlog.MLOG_2 attrs['mtype'] = Mlog.MTYPE_SELF # 默认为自生产 + fmlog = attrs.get('fmlog', None) + mtaskb = attrs.get('mtaskb', None) + if fmlog: + attrs['fill_way'] = Mlog.MLOG_12 + wm_in: WMaterial = attrs.get('wm_in', None) + if wm_in: + pass + else: + raise ParseError('未提供消耗的车间物料') + attrs['route'] = fmlog.route + attrs['mgroup'] = fmlog.mgroup + attrs['mtask'] = fmlog.mtask + if attrs['mtask'].mtaskb and mtaskb is None: + raise ParseError('子任务不能为空') + if mtaskb and mtaskb.mtask != fmlog.mtask: + raise ParseError('子任务不一致') + if wm_in.material != attrs['mtask'].material_in: + raise ParseError('消耗物料与任务不一致') mtask = attrs.get('mtask', None) count_notok = 0 for i in attrs: @@ -330,17 +412,45 @@ class MlogSerializer(CustomModelSerializer): if mtask.start_date == mtask.end_date: attrs['handle_date'] = mtask.start_date else: - if attrs['handle_date'] >= mtask.start_date and attrs['handle_date'] <= mtask.end_date: - pass - else: - raise ValidationError('操作日期错误') + if attrs['work_end_time']: + attrs['handle_date'] = attrs['work_end_time'].date() + elif attrs['work_start_time']: + attrs['handle_date'] = attrs['work_start_time'].date() + # if attrs['handle_date'] >= mtask.start_date and attrs['handle_date'] <= mtask.end_date: + # pass + # else: + # if attrs['handle_date'] >= mtask.start_date and attrs['handle_date'] <= mtask.end_date: + # pass + # else: + # raise ValidationError('操作日期错误') + mtaskb: Mtaskb = attrs.get('mtaskb', None) + if mtaskb: + mtask = mtaskb.mtask + attrs['mtask'] = mtask + attrs['handle_user'] = mtaskb.handle_user + else: + mtask: Mtask = attrs.get('mtask', None) + if mtask: + attrs['mgroup'] = mtask.mgroup + attrs['material_in'] = mtask.material_in + material_out = mtask.material_out + attrs['material_out'] = material_out + if mtask.start_date == mtask.end_date: + attrs['handle_date'] = mtask.end_date + else: + mgroup = attrs['mgroup'] + material_out = attrs['material_out'] + if not (mgroup and material_out): + raise ValidationError('缺少工段或产物!') return attrs class MlogInitSerializer(CustomModelSerializer): class Meta: model = Mlog - fields = ['id', 'work_start_time', 'mgroup', 'reminder_interval_list', 'route', 'equipment', 'handle_user', 'note', 'mtype', 'supplier'] + fields = ['id', + 'work_start_time', 'work_end_time', 'mgroup', 'reminder_interval_list', + 'route', 'equipment', 'handle_user', 'note', 'mtype', 'supplier', 'test_file', 'test_user', 'test_time', 'oinfo_json'] extra_kwargs = { 'work_start_time': {'required': True}, 'route':{'required': True}, @@ -357,17 +467,19 @@ class MlogInitSerializer(CustomModelSerializer): attrs['hour_work'] = route.hour_work attrs['material_in'] = route.material_in attrs['material_out'] = route.material_out - attrs['fill_way'] = Mlog.MLOG_STEP + attrs['fill_way'] = Mlog.MLOG_23 if mtype == Mlog.MTYPE_OUT: supplier = attrs.get('supplier', None) if not supplier: raise ValidationError('外协必须选择外协单位') + if attrs.get('work_end_time', None): + attrs['handle_date'] = localdate(attrs['work_end_time']) return attrs class MlogChangeSerializer(CustomModelSerializer): class Meta: model = Mlog - fields = ['id', 'work_end_time', 'handle_user', 'note'] + fields = ['id', 'work_end_time', 'handle_user', 'note', 'oinfo_json', 'test_file', 'test_user', 'test_time'] def validate(self, attrs): if attrs.get('work_end_time', None): @@ -378,12 +490,14 @@ class MlogChangeSerializer(CustomModelSerializer): class MlogbInSerializer(CustomModelSerializer): class Meta: model = Mlogb - fields = ['id', 'mlog', 'mtask', 'wm_in', 'count_use'] + fields = ['id', 'mlog', 'mtask', 'wm_in', 'count_use', 'count_pn_jgqbl', 'count_break', 'note'] extra_kwargs = {'count_use': {'required': True}, 'mtask': {'required': True}, 'wm_in': {'required': True}} def validate(self, attrs): mlog: Mlog = attrs['mlog'] mtask: Mtask = attrs['mtask'] + if mtask.state != Mtask.MTASK_ASSGINED: + raise ValidationError('该任务非下达中不可选择') wm_in: WMaterial = attrs['wm_in'] if wm_in.state != WMaterial.WM_OK: raise ValidationError('非合格品不可使用') @@ -416,18 +530,41 @@ class MlogbInSerializer(CustomModelSerializer): class MlogbInUpdateSerializer(CustomModelSerializer): class Meta: model = Mlogb - fields = ['id', 'count_use', 'count_break'] + fields = ['id', 'count_use', 'count_break', 'count_pn_jgqbl', 'note'] class MlogbOutUpdateSerializer(CustomModelSerializer): class Meta: model = Mlogb fields = "__all__" - read_only_fields = EXCLUDE_FIELDS_BASE + ['mlog', 'mtask', 'wm_in', 'material_in', 'material_out', 'count_use', 'count_break'] + read_only_fields = EXCLUDE_FIELDS_BASE + ['mlog', 'mtask', 'wm_in', 'material_in', 'material_out', 'count_use', 'count_break', 'count_pn_jgqbl'] def validate(self, attrs): + count_notok_json = attrs.get('count_notok_json', []) + # count_notok_json字段处理 + if count_notok_json: + # 先置0字段 + for i in attrs: + if 'count_n_' in i: + i = 0 + count_notok_dict = {} + for item in count_notok_json: + notok = item['notok'] + count = item['count'] + full_notok = f'count_n_{notok}' + if not hasattr(Mlogb, full_notok): + raise ValidationError(f'{notok}-该不合格项不存在') + if full_notok in count_notok_dict: + count_notok_dict[full_notok] = count_notok_dict[full_notok] + count + else: + count_notok_dict[full_notok] = count + for k, v in count_notok_dict.items(): + attrs[k] = v + count_notok = 0 for i in attrs: if 'count_n_' in i: + if not hasattr(Mlogb, i): + raise ValidationError(f'{i}不存在') count_notok = count_notok + attrs[i] attrs['count_notok'] = count_notok if attrs['count_real'] >= attrs['count_ok'] + attrs['count_notok']: @@ -448,6 +585,14 @@ class MlogRelatedSerializer(serializers.Serializer): class DeptBatchSerializer(serializers.Serializer): belong_dept_name = serializers.CharField(label='车间名称') +class HandoverbSerializer(CustomModelSerializer): + batch = serializers.CharField(source='wm.batch', read_only=True) + notok_sign = serializers.CharField(source='wm.notok_sign', read_only=True) + class Meta: + model = Handoverb + fields = "__all__" + read_only_fields = EXCLUDE_FIELDS_BASE + ['handover'] + extra_kwargs = {'wm': {'required': True}} class HandoverSerializer(CustomModelSerializer): # wm = serializers.PrimaryKeyRelatedField( @@ -466,18 +611,28 @@ class HandoverSerializer(CustomModelSerializer): material_name = serializers.StringRelatedField( source='material', read_only=True) wm_notok_sign = serializers.CharField(source='wm.notok_sign', read_only=True) + handoverb = HandoverbSerializer(many=True) def validate(self, attrs): if 'type' not in attrs: attrs['type'] = Handover.H_NORMAL - wm:WMaterial = attrs['wm'] - material = wm.material + wm:WMaterial = attrs.get('wm', None) + handoverb = attrs.get('handoverb', []) + if wm: + attrs['handoverb'] = [{"wm": wm, "count": attrs["count"] }] + handoverb = attrs['handoverb'] + attrs['batch'] = wm.batch + elif handoverb: + wm: WMaterial = handoverb[0]["wm"] + if wm: + pass + else: + raise ParseError('必须指定车间库存') attrs['material'] = wm.material - attrs['batch'] = wm.batch attrs['send_dept'] = wm.belong_dept - if attrs['wm'].mgroup: + if wm.mgroup: attrs['send_mgroup'] = wm.mgroup - if material.process and material.process.into_wm_mgroup and 'recive_mgroup' not in attrs: + if attrs['material'].process and attrs['material'].process.into_wm_mgroup and 'recive_mgroup' not in attrs: raise ValidationError('必须指定交接工段') if 'recive_mgroup' in attrs and attrs['recive_mgroup']: attrs['recive_dept'] = attrs['recive_mgroup'].belong_dept @@ -485,10 +640,24 @@ class HandoverSerializer(CustomModelSerializer): raise ValidationError('收料车间和收料工段必须有一个') if 'send_dept' not in attrs and 'send_mgroup' not in attrs: raise ValidationError('送料车间和送料工段必须有一个') - if wm.notok_sign is not None and attrs['type'] in [Handover.H_NORMAL, Handover.H_TEST]: - raise ValidationError('物料不合格,不能进行正常/检验交接') - if wm.count_xtest is not None: - raise ValidationError('物料检验中,不能进行交接') + t_count = 0 + for ind, item in enumerate(attrs['handoverb']): + wm = item["wm"] + t_count += item["count"] + if wm.mgroup != attrs['send_mgroup']: + raise ParseError(f'第{ind+1}物料与交接工段不一致') + if attrs["material"] != wm.material: + raise ParseError(f'第{ind+1}物料与交接物料不一致') + if wm.notok_sign is not None and attrs['type'] in [Handover.H_NORMAL, Handover.H_TEST]: + raise ParseError(f'第{ind+1}物料不合格,不能进行正常/检验交接') + if wm.count_xtest is not None: + raise ParseError(f'第{ind+1}物料检验中,不能进行交接') + attrs["count"] = t_count + if attrs['type'] == Handover.H_REPAIR: + recive_mgroup = attrs.get("recive_mgroup", None) + if recive_mgroup is None: + raise ParseError('返工交接需指定工段') + attrs['material_changed'] = find_material_can_change(attrs['material'], recive_mgroup) return attrs class Meta: @@ -497,22 +666,36 @@ class HandoverSerializer(CustomModelSerializer): read_only_fields = EXCLUDE_FIELDS + ["mlog"] extra_kwargs = { "type": {"required": False}, - "wm": {"required": True}, + "wm": {"required": False}, "send_dept": {"required": False}, "recive_mgroup": {"required": False}, "recive_dept": {"required": False}, "material": {"required": False}, "batch": {"required": False}, + "count": {"required": False}, + "count_eweight": {"required": False} } def create(self, validated_data): - type = validated_data['type'] - if type == Handover.H_REPAIR: - recive_mgroup = validated_data.get("recive_mgroup", None) - if recive_mgroup is None: - raise ParseError('返工交接需指定工段') - validated_data['material_changed'] = find_material_can_change(validated_data['material'], recive_mgroup) - return super().create(validated_data) + handoverb = validated_data.pop('handoverb', []) + with transaction.atomic(): + ins = super().create(validated_data) + for item in handoverb: + Handoverb.objects.get_or_create(handover=ins, wm=item["wm"], count=item["count"]) + return ins + + def update(self, instance, validated_data): + handoverb = validated_data.pop('handoverb', []) + with transaction.atomic(): + super().update(instance, validated_data) + wmIds = [] + for item in handoverb: + wmIds.append(item["wm"].id) + hb, _ = Handoverb.objects.get_or_create(handover=instance, wm=item["wm"]) + hb.count = item["count"] + hb.save() + Handoverb.objects.filter(handover=instance).exclude(wm__in=wmIds).delete() + return instance class HandoverUpdateSerializer(CustomModelSerializer): class Meta: @@ -520,6 +703,7 @@ class HandoverUpdateSerializer(CustomModelSerializer): fields = ['id', 'send_date', 'send_user', 'count', 'count_eweight', 'recive_user'] + class GenHandoverSerializer(serializers.Serializer): mlogs = serializers.PrimaryKeyRelatedField( label='mlog的ID列表', queryset=Mlog.objects.all(), many=True) @@ -570,3 +754,34 @@ class AttLogSerializer(CustomModelSerializer): class Meta: model = AttLog fields = '__all__' + + +class FmlogSerializer(CustomModelSerializer): + routepack_name = serializers.CharField( + source='route.routepack.name', read_only=True) + mtask_number = serializers.CharField(source='mtask.number', read_only=True) + class Meta: + model = Fmlog + fields = '__all__' + read_only_fields = EXCLUDE_FIELDS + + def validate(self, attrs): + route: Route = attrs['route'] + mtask: Mtask = attrs['mtask'] + if mtask.state != Mtask.MTASK_ASSGINED: + raise ParseError('该任务非下达中不可选择') + mgroup: Mgroup = attrs['mgroup'] + if route.process != mgroup.process: + raise ParseError('工序不匹配') + if mtask.mgroup != mgroup: + raise ParseError('工段不匹配') + return attrs + +class FmlogUpdateSerializer(CustomModelSerializer): + class Meta: + model = Fmlog + fields = ['id', 'note'] + + +class MlogTCreateSerializer(CustomModelSerializer): + pass \ No newline at end of file diff --git a/apps/wpm/services.py b/apps/wpm/services.py index 4840a932..a85f9c46 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -4,7 +4,6 @@ from django.core.cache import cache from django.db.models import Sum from django.utils import timezone from typing import Union -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from rest_framework.exceptions import ParseError @@ -13,9 +12,25 @@ from apps.inm.models import MIO, MIOItem, MIOItemA from apps.pm.models import Mtask from apps.mtm.models import Mgroup, Shift, Material, Route, RoutePack -from .models import SfLog, SfLogExp, WMaterial, Mlog, Mlogb, Handover -from apps.mtm.models import Process +from .models import SfLog, WMaterial, Mlog, Mlogb, Handover, Handoverb from apps.mtm.services import cal_material_count +from apps.wf.models import Ticket +from django.db import transaction +from apps.utils.thread import MyThread + +def generate_new_batch(old_batch: str, mlog: Mlog): + new_batch = old_batch + supplier = mlog.supplier + process = mlog.mgroup.process + if mlog.mtype == Mlog.MTYPE_OUT: + supplier_number = supplier.number if supplier else '' + if supplier_number: + new_batch = f'{new_batch}-{supplier_number}' + elif process.batch_append_equip: + number = mlog.equipment.number if mlog.equipment else '' + if number: + new_batch = f'{new_batch}-{number}' + return new_batch def find_material_can_change(material: Material, mgroup_to: Mgroup): """ @@ -204,10 +219,16 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): belong_dept = mgroup.belong_dept material_out = mlog.material_out material_in = mlog.material_in + supplier = mlog.supplier # 外协 if material_in: # 需要进行车间库存管理 m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False) if m_ins.exists(): - m_ins_list = [(mi.material_in, mi.batch, mi.count_use, mi.wm_in) for mi in m_ins.all()] + m_ins_list = [] + m_ins_bl_list = [] + for mi in m_ins.all(): + m_ins_list.append((mi.material_in, mi.batch, mi.count_use, mi.wm_in)) + if mi.count_pn_jgqbl > 0: + m_ins_bl_list.append((mi.material_in, mi.batch, mi.count_pn_jgqbl)) else: m_ins_list = [(material_in, mlog.batch, mlog.count_use, mlog.wm_in)] for mi in m_ins_list: @@ -238,6 +259,20 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): wm.count = wm.count - mi_count wm.update_by = user wm.save() + # 针对加工前不良的暂时额外处理 + for item in m_ins_bl_list: + material, batch, count_pn_jgqbl = item + if count_pn_jgqbl> 0: + lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'notok_sign': 'jgqbl', 'state': WMaterial.WM_NOTOK} + wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm.count = wm.count + count_pn_jgqbl + if is_create: + wm.create_by = user + else: + wm.update_by = user + wm.save() + + if material_out: # 需要入车间库存 into_wm_mgroup = material_out.process.into_wm_mgroup if material_out.process else False need_store_notok = material_out.process.store_notok if material_out.process else False @@ -271,6 +306,8 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): wm.count = wm.count + mo_count wm.count_eweight = mo_count_eweight wm.update_by = user + if supplier is not None: + wm.supplier = supplier wm.save() mlog.submit_time = now @@ -299,7 +336,12 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): into_wm_mgroup = material_in.process.into_wm_mgroup if material_in.process else False m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False) if m_ins.exists(): - m_ins_list = [(mi.material_in, mi.batch, mi.count_use, mi.wm_in) for mi in m_ins.all()] + m_ins_list = [] + m_ins_bl_list = [] + for mi in m_ins.all(): + m_ins_list.append((mi.material_in, mi.batch, mi.count_use, mi.wm_in)) + if mi.count_pn_jgqbl > 0: + m_ins_bl_list.append((mi.material_in, mi.batch, mi.count_pn_jgqbl)) else: m_ins_list = [(material_in, mlog.batch, mlog.count_use, mlog.wm_in)] for mi in m_ins_list: @@ -321,6 +363,21 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): wm.count = wm.count + mi_count wm.update_by = user wm.save() + # 针对加工前不良的暂时额外处理 + for item in m_ins_bl_list: + material, batch, count_pn_jgqbl = item + if count_pn_jgqbl> 0: + lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'notok_sign': 'jgqbl', 'state': WMaterial.WM_NOTOK} + wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm.count = wm.count - count_pn_jgqbl + if wm.count < 0: + raise ParseError('加工前不良数量大于库存量') + if is_create: + wm.create_by = user + else: + wm.update_by = user + wm.save() + if material_out: # 产物退回 # 有多个产物的情况 # 需要考虑不合格品退回的情况 @@ -339,6 +396,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): for mo in m_outs_list: mo_ma, mo_batch, mo_count, _, notok_sign = mo + if mo_count == 0: + continue wm_state = WMaterial.WM_OK if notok_sign is None else WMaterial.WM_NOTOK lookup = {'batch': mo_batch, 'material': mo_ma, 'mgroup': None, 'notok_sign': notok_sign, 'state': wm_state} if stored_mgroup: @@ -379,7 +438,7 @@ def cal_mlog_count_from_mlogb(mlog: Mlog): """ 通过mlogb计算mlog count 合计 """ - if mlog.fill_way == Mlog.MLOG_STEP: + if mlog.fill_way == Mlog.MLOG_23: a_dict = { "total_count_use": Sum('count_use'), "total_count_break": Sum('count_break'), @@ -388,7 +447,7 @@ def cal_mlog_count_from_mlogb(mlog: Mlog): "total_count_ok": Sum('count_ok'), "total_count_notok": Sum('count_notok'), } - f_names = [f.name for f in Mlogb._meta.fields if 'count_n' in f.name] + f_names = [f.name for f in Mlogb._meta.fields if 'count_n_' in f.name] for f in f_names: a_dict[f'total_{f}'] = Sum(f) mlogb_summary = Mlogb.objects.filter(mlog=mlog).aggregate( @@ -410,9 +469,9 @@ def cal_mtask_progress_from_mlog(mlog: Mlog): """ 更新mlog关联的任务进度(可线程中执行) """ - if mlog.fill_way == Mlog.MLOG_ONETIME and mlog.mtask: - update_mtask(mlog.mtask, fill_way=Mlog.MLOG_ONETIME) - elif mlog.fill_way == Mlog.MLOG_STEP: + if mlog.fill_way in [Mlog.MLOG_2, Mlog.MLOG_12] and mlog.mtask: + update_mtask(mlog.mtask, fill_way=mlog.fill_way) + elif mlog.fill_way == Mlog.MLOG_23: cal_mlog_count_from_mlogb(mlog) m_outs_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False) caled_mtask = [] @@ -420,7 +479,7 @@ def cal_mtask_progress_from_mlog(mlog: Mlog): mtask = item.mtask if mtask in caled_mtask: continue - update_mtask(mtask, fill_way=Mlog.MLOG_STEP) + update_mtask(mtask, fill_way=mlog.fill_way) caled_mtask.append(mtask) def cal_material_count_from_mlog(mlog: Mlog): @@ -443,7 +502,7 @@ def cal_material_count_from_mlog(mlog: Mlog): def update_mtask(mtask: Mtask, fill_way: int = 10): from apps.pm.models import Utask - if fill_way == Mlog.MLOG_ONETIME: + if fill_way == Mlog.MLOG_2: res = Mlog.objects.filter(mtask=mtask).exclude(submit_time=None).aggregate(sum_count_real=Sum( 'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok')) mtask.count_real = res['sum_count_real'] if res['sum_count_real'] else 0 @@ -462,7 +521,7 @@ def update_mtask(mtask: Mtask, fill_way: int = 10): if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0: utask.state = Utask.UTASK_SUBMIT utask.save() - elif fill_way == Mlog.MLOG_STEP: + elif fill_way in [Mlog.MLOG_23, Mlog.MLOG_12]: # 已经提交的日志 m_outs_qs_mtask = Mlogb.objects.filter(mtask=mtask, material_out__isnull=False, mlog__submit_time__isnull=False) res = m_outs_qs_mtask.aggregate( @@ -502,90 +561,117 @@ def handover_submit(handover: Handover, user: User, now: Union[datetime.datetime if handover.submit_time is not None: return now = timezone.now() + handoverb_qs = Handoverb.objects.filter(handover=handover) need_add = True material = handover.material - batch = handover.batch - wm_from = handover.wm - if wm_from is None: - raise ParseError('找不到车间库存') if '混料' in material.name: # hard code need_add = False - count_x = wm_from.count - handover.count - if count_x < 0: - raise ParseError('车间库存不足!') + if handoverb_qs.exists(): + handoverb_list = [(item.wm, item.count) for item in handoverb_qs] else: - wm_from.count = count_x - wm_from.save() - if need_add: - if handover.type == Handover.H_NORMAL: - if handover.recive_mgroup: + handoverb_list = [(handover.wm, handover.count)] + + recive_mgroup = handover.recive_mgroup + recive_dept = handover.recive_dept + for item in handoverb_list: + wm_from, xcount = item + batch = wm_from.batch + if wm_from is None: + raise ParseError('找不到车间库存') + + count_x = wm_from.count - xcount + if count_x < 0: + raise ParseError('车间库存不足!') + else: + wm_from.count = count_x + wm_from.save() + if need_add: + # 开始变动 + if handover.type == Handover.H_NORMAL: wm_to, _ = WMaterial.objects.get_or_create( batch=batch, material=material, - mgroup=handover.recive_mgroup, - state=WMaterial.WM_OK, - defaults={"batch": batch, "material": material, "mgroup": handover.recive_mgroup, "belong_dept": handover.recive_dept}, + mgroup=recive_mgroup, + belong_dept=recive_dept, + state=WMaterial.WM_OK ) - else: - wm_to, _ = WMaterial.objects.get_or_create( - batch=batch, - material=material, - belong_dept=handover.recive_dept, - mgroup=None, - state=WMaterial.WM_OK, - defaults={"batch": batch, "material": material, "belong_dept": handover.recive_dept} - ) - elif handover.type == Handover.H_REPAIR: - if handover.recive_mgroup: + elif handover.type == Handover.H_REPAIR: + if handover.recive_mgroup: + wm_to, _ = WMaterial.objects.get_or_create( + batch=batch, + material=handover.material_changed, + mgroup=recive_mgroup, + belong_dept=recive_dept, + notok_sign=wm_from.notok_sign, + material_origin=material, + state=WMaterial.WM_REPAIR + ) + else: + raise ParseError("返工交接必须指定接收工段") + elif handover.type == Handover.H_TEST: wm_to, _ = WMaterial.objects.get_or_create( batch=batch, - material=handover.material_changed, - mgroup=handover.recive_mgroup, - notok_sign=handover.wm.notok_sign, - material_origin=handover.material, - state=WMaterial.WM_REPAIR, + material=material, + mgroup=recive_mgroup, + state=WMaterial.WM_TEST, + belong_dept=recive_dept, defaults={ - "batch": batch, - "material": handover.material_changed, - "mgroup": handover.recive_mgroup, - "notok_sign": handover.wm.notok_sign, - "material_origin": handover.material, - "belong_dept": handover.recive_dept, - "state": WMaterial.WM_REPAIR + "count_xtest": 0, }, ) + elif handover.type == Handover.H_SCRAP: + if recive_mgroup: + wm_to, _ = WMaterial.objects.get_or_create( + batch=batch, + material=material, + mgroup=recive_mgroup, + belong_dept=recive_dept, + notok_sign=wm_from.notok_sign, + state=WMaterial.WM_SCRAP + ) + else: + raise ParseError("不支持非工段报废") else: - raise ParseError("返工交接必须指定接收工段") - elif handover.type == Handover.H_TEST: - if handover.recive_mgroup: - wm_to, _ = WMaterial.objects.get_or_create( - batch=batch, - material=material, - mgroup=handover.recive_mgroup, - state=WMaterial.WM_TEST, - defaults={ - "batch": batch, - "material": material, - "mgroup": handover.recive_mgroup, - "belong_dept": handover.recive_dept, - "count_xtest": 0, - "state": WMaterial.WM_TEST}, - ) - else: - wm_to, _ = WMaterial.objects.get_or_create( - batch=batch, - material=material, - belong_dept=handover.recive_dept, - mgroup=None, - state=WMaterial.WM_TEST, - defaults={"batch": batch, "material": material, "belong_dept": handover.recive_dept, "count_xtest": 0, "state": WMaterial.WM_TEST}, - ) - else: - raise ParseError("不支持交接类型") + raise ParseError("不支持该交接类型") - wm_to.count = wm_to.count + handover.count - wm_to.count_eweight = handover.count_eweight # 这行代码有隐患 - wm_to.save() + wm_to.count = wm_to.count + xcount + wm_to.count_eweight = handover.count_eweight # 这行代码有隐患 + wm_to.save() handover.submit_user = user handover.submit_time = now handover.save() + +def mlog_submit_validate(ins: Mlog): + if ins.submit_time: + raise ParseError('该日志已提交!') + if ins.mtask and ins.mtask.state == Mtask.MTASK_STOP: + raise ParseError('该任务已停止!') + if ins.fill_way == Mlog.MLOG_23: + if not Mlogb.objects.filter(material_out__isnull=False, mlog=ins).exists(): + raise ParseError('该日志未指定产出!') + if not Mlogb.objects.filter(material_in__isnull=False, mlog=ins).exists(): + raise ParseError('该日志未指定消耗!') + if Mlogb.objects.filter(material_out__isnull=False, count_real=0, mlog=ins).exists(): + raise ParseError('产出数量不能为0!') + +def bind_mlog(ticket: Ticket, transition, new_ticket_data: dict): + ins = Mlog.objects.get(id=new_ticket_data['t_id']) + mlog_submit_validate(ins) # 校验是否可submit + ticket_data = ticket.ticket_data + ticket_data.update({ + 't_model': 'mlog', + 't_id': ins.id, + }) + ticket.ticket_data = ticket_data + ticket.create_by = ins.create_by + ticket.save() + if ins.ticket is None: + ins.ticket = ticket + ins.save() + +def mlog_audit_end(ticket: Ticket): + now = timezone.now() + ins = Mlog.objects.get(id=ticket.ticket_data['t_id']) + mlog_submit(ins, ticket.create_by, now) + MyThread(target=cal_mtask_progress_from_mlog,args=(ins,)).start() + MyThread(target=cal_material_count_from_mlog,args=(ins,)).start() diff --git a/apps/wpm/urls.py b/apps/wpm/urls.py index 005f4377..ec342726 100644 --- a/apps/wpm/urls.py +++ b/apps/wpm/urls.py @@ -3,7 +3,9 @@ from rest_framework.routers import DefaultRouter from apps.wpm.views import (SfLogViewSet, StLogViewSet, SfLogExpViewSet, WMaterialViewSet, MlogViewSet, HandoverViewSet, - AttlogViewSet, OtherLogViewSet, MlogbViewSet, MlogbInViewSet, MlogbOutViewSet) + AttlogViewSet, OtherLogViewSet, MlogbViewSet, MlogbInViewSet, + MlogbOutViewSet, FmlogViewSet) +from apps.wpm.datax import AnaViewSet API_BASE_URL = 'api/wpm/' @@ -14,6 +16,7 @@ router.register('sflog', SfLogViewSet, basename='sflog') router.register('stlog', StLogViewSet, basename='stlog') router.register('sflogexp', SfLogExpViewSet, basename='sflogexp') router.register('wmaterial', WMaterialViewSet, basename='wmaterial') +router.register('fmlog', FmlogViewSet, basename='fmlog') router.register('mlog', MlogViewSet, basename='mlog') router.register('mlogb', MlogbViewSet) router.register('mlogb/in', MlogbInViewSet) @@ -21,6 +24,8 @@ router.register('mlogb/out', MlogbOutViewSet) router.register('handover', HandoverViewSet, basename='handover') router.register('attlog', AttlogViewSet, basename='attlog') router.register('otherlog', OtherLogViewSet, basename='otherlog') +router.register('ana', AnaViewSet, basename='ana') + urlpatterns = [ path(API_BASE_URL, include(router.urls)), ] diff --git a/apps/wpm/views.py b/apps/wpm/views.py index f3827da4..005d86a2 100644 --- a/apps/wpm/views.py +++ b/apps/wpm/views.py @@ -16,15 +16,17 @@ from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from apps.utils.mixins import BulkCreateModelMixin from .filters import StLogFilter, SfLogFilter, WMaterialFilter, MlogFilter, HandoverFilter, MlogbFilter -from .models import SfLog, SfLogExp, StLog, WMaterial, Mlog, Handover, Mlogb, AttLog, OtherLog +from .models import SfLog, SfLogExp, StLog, WMaterial, Mlog, Handover, Mlogb, AttLog, OtherLog, Fmlog from .serializers import (SflogExpSerializer, SfLogSerializer, StLogSerializer, WMaterialSerializer, MlogRevertSerializer, MlogSerializer, MlogRelatedSerializer, DeptBatchSerializer, HandoverSerializer, HandoverUpdateSerializer, GenHandoverSerializer, GenHandoverWmSerializer, MlogAnaSerializer, AttLogSerializer, OtherLogSerializer, MlogInitSerializer, MlogChangeSerializer, - MlogbDetailSerializer, MlogbInSerializer, MlogbInUpdateSerializer, MlogbOutUpdateSerializer) + MlogbDetailSerializer, MlogbInSerializer, MlogbInUpdateSerializer, + MlogbOutUpdateSerializer, FmlogSerializer, FmlogUpdateSerializer) from .services import mlog_submit, update_mtask, handover_submit, mlog_revert, cal_material_count_from_mlog, cal_mtask_progress_from_mlog from apps.utils.thread import MyThread from apps.monitor.services import create_auditlog, delete_auditlog +from apps.wpm.services import mlog_submit_validate, generate_new_batch # Create your views here. @@ -112,11 +114,17 @@ class WMaterialViewSet(ListModelMixin, CustomGenericViewSet): perms_map = {'get': '*'} queryset = WMaterial.objects.filter(count__gt=0) serializer_class = WMaterialSerializer - select_related_fields = ['material', 'belong_dept', 'material__process'] + select_related_fields = ['material', 'belong_dept', 'material__process', 'supplier'] search_fields = ['material__name', 'material__number', 'material__specification', 'batch', 'material__model'] filterset_class = WMaterialFilter + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + if self.request.query_params.get('state_all'): + return queryset + return queryset.exclude(state=WMaterial.WM_SCRAP) + @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=DeptBatchSerializer) @transaction.atomic def batchs(self, request): @@ -141,7 +149,9 @@ class MlogViewSet(CustomModelViewSet): queryset = Mlog.objects.all() serializer_class = MlogSerializer select_related_fields = ['create_by', 'update_by', 'mtask', - 'handle_user', 'handle_user_2', 'equipment', 'equipment_2', 'material_in', 'material_out', 'supplier'] + 'handle_user', 'handle_user_2', 'equipment', + 'equipment_2', 'material_in', 'material_out', 'route__routepack', + 'supplier', 'ticket', 'mgroup__process', 'test_user'] prefetch_related_fields = ['handle_users', 'material_outs', 'b_mlog', 'equipments'] filterset_class = MlogFilter @@ -204,15 +214,13 @@ class MlogViewSet(CustomModelViewSet): ins: Mlog = self.get_object() vdata_old = MlogSerializer(ins).data now = timezone.now() - if ins.submit_time: - raise ParseError('该日志已提交!') - if ins.mtask and ins.mtask.state == Mtask.MTASK_STOP: - raise ParseError('该任务已停止!') - if ins.fill_way == Mlog.MLOG_STEP: - if not Mlogb.objects.filter(material_out__isnull=False).exists(): - raise ParseError('该日志未指定产出!') - if not Mlogb.objects.filter(material_in__isnull=False).exists(): - raise ParseError('该日志未指定消耗!') + if ins.ticket: + raise ParseError('该日志存在审批!') + else: + p: Process = ins.mgroup.process + if p.mlog_need_ticket: + raise ParseError('该日志需要审批!') + mlog_submit_validate(ins) with transaction.atomic(): mlog_submit(ins, self.request.user, now) vdata_new = MlogSerializer(ins).data @@ -339,10 +347,11 @@ class HandoverViewSet(CustomModelViewSet): """ ins: Handover = self.get_object() user: User = self.request.user - if user == ins.recive_user or user.belong_dept == ins.recive_user.belong_dept: - pass - else: - raise ParseError('非接收人不可提交') + if ins.type != Handover.H_SCRAP: + if user == ins.recive_user or user.belong_dept == ins.recive_user.belong_dept: + pass + else: + raise ParseError('非接收人不可提交') if ins.submit_time is None: handover_submit(ins, user, None) return Response() @@ -468,8 +477,6 @@ class MlogbInViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, Cust def perform_create(self, serializer): ins: Mlogb = serializer.save() mlog: Mlog = ins.mlog - process: Process = mlog.mgroup.process - supplier = mlog.supplier # 创建输出 if ins.mtask and ins.material_in: material_out = mlog.material_out @@ -480,20 +487,26 @@ class MlogbInViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, Cust "mlog": ins.mlog, "material_out": ins.mlog.material_out } - new_batch = ins.batch - if mlog.mtype == Mlog.MTYPE_OUT: - supplier_number = supplier.number if supplier else '' - if supplier_number: - new_batch = f'{new_batch}-{supplier_number}' - elif process.batch_append_equip: - number = mlog.equipment.number if mlog.equipment else '' - if number: - new_batch = f'{new_batch}-{number}' - m_dict['batch'] = new_batch + m_dict['batch'] = generate_new_batch(ins.batch, mlog) Mlogb.objects.get_or_create(**m_dict, defaults=m_dict) class MlogbOutViewSet(UpdateModelMixin, CustomGenericViewSet): perms_map = {"put": "mlog.update"} queryset = Mlogb.objects.filter(material_out__isnull=False) - serializer_class = MlogbOutUpdateSerializer \ No newline at end of file + serializer_class = MlogbOutUpdateSerializer + + +class FmlogViewSet(CustomModelViewSet): + perms_map = {'get': '*', 'post': 'mlog.create', 'put': 'mlog.update', 'delete': 'mlog.delete'} + queryset = Fmlog.objects.all() + serializer_class = FmlogSerializer + update_serializer_class = FmlogUpdateSerializer + filterset_fields = ['mtask', 'mgroup', 'route'] + select_related_fields = ['mtask', 'mgroup', 'route', 'route__routepack'] + + def destroy(self, request, *args, **kwargs): + ins = self.get_object() + if Mlog.objects.filter(fmlog=ins).exists(): + raise ParseError('因存在二级日志不可删除') + return super().destroy(request, *args, **kwargs) \ No newline at end of file diff --git a/server/settings.py b/server/settings.py index 92604674..d6701e6a 100755 --- a/server/settings.py +++ b/server/settings.py @@ -341,7 +341,7 @@ LOGGING = { ##### 加载客户可自定义配置并提供操作方法 ##### SYS_JSON_PATH = os.path.join(BASE_DIR, 'server/conf.json') -def get_sysconfig(key='', reload=False): +def get_sysconfig(key='', default='raise_error', reload=False): """获取系统配置可指定key字符串 """ config = cache.get('system_config', None) @@ -355,7 +355,13 @@ def get_sysconfig(key='', reload=False): if key: k_l = key.split('.') for k in k_l: - config = config[k] + try: + config = config[k] + except KeyError: + if default == 'raise_error': + raise + else: + return default return config