diff --git a/apps/cm/migrations/0004_labeltemplate.py b/apps/cm/migrations/0004_labeltemplate.py new file mode 100644 index 00000000..862fb412 --- /dev/null +++ b/apps/cm/migrations/0004_labeltemplate.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.12 on 2025-04-30 05:17 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('cm', '0003_alter_lablemat_state'), + ] + + operations = [ + migrations.CreateModel( + name='LabelTemplate', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.TextField(verbose_name='名称')), + ('commands', models.JSONField(blank=True, default=list, verbose_name='指令模板')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/cm/models.py b/apps/cm/models.py index e080d261..3329dcfe 100644 --- a/apps/cm/models.py +++ b/apps/cm/models.py @@ -13,4 +13,9 @@ class LableMat(BaseModel): supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True) notok_sign = models.CharField('不合格标记', max_length=10, null=True, blank=True) defect = models.ForeignKey("qm.defect", verbose_name='缺陷', on_delete=models.SET_NULL, null=True, blank=True) - material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='lm_mo') \ No newline at end of file + material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='lm_mo') + + +class LabelTemplate(BaseModel): + name = models.TextField("名称") + commands = models.JSONField("指令模板", default=list, blank=True) \ No newline at end of file diff --git a/apps/cm/serializers.py b/apps/cm/serializers.py index ba6976a4..075c7f0f 100644 --- a/apps/cm/serializers.py +++ b/apps/cm/serializers.py @@ -1,7 +1,8 @@ from rest_framework import serializers -from .models import LableMat +from .models import LableMat, LabelTemplate from apps.qm.models import NotOkOption from apps.wpm.models import WmStateOption +from apps.utils.serializers import CustomModelSerializer class TidSerializer(serializers.Serializer): @@ -23,4 +24,10 @@ class LabelMatSerializer(serializers.ModelSerializer): return getattr(NotOkOption, obj.notok_sign, NotOkOption.qt).label if obj.notok_sign else None def get_state_name(self, obj): - return getattr(WmStateOption, str(obj.state), WmStateOption.OK).label if obj.state else None \ No newline at end of file + return getattr(WmStateOption, str(obj.state), WmStateOption.OK).label if obj.state else None + + +class LabelTemplateSerializer(CustomModelSerializer): + class Meta: + model = LabelTemplate + fields = '__all__' \ No newline at end of file diff --git a/apps/cm/urls.py b/apps/cm/urls.py index cf30518d..087b32d4 100644 --- a/apps/cm/urls.py +++ b/apps/cm/urls.py @@ -1,12 +1,13 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from apps.cm.views import LableMatViewSet +from apps.cm.views import LableMatViewSet, LabelTemplateViewSet API_BASE_URL = 'api/cm/' HTML_BASE_URL = 'dhtml/cm/' router = DefaultRouter() router.register('labelmat', LableMatViewSet, basename='labelmat') +router.register('labeltemplate', LabelTemplateViewSet, basename='labeltemplate') urlpatterns = [ path(API_BASE_URL, include(router.urls)), ] \ No newline at end of file diff --git a/apps/cm/views.py b/apps/cm/views.py index ccc4f90f..2ab8c35d 100644 --- a/apps/cm/views.py +++ b/apps/cm/views.py @@ -1,11 +1,11 @@ -from apps.cm.models import LableMat +from apps.cm.models import LableMat, LabelTemplate from rest_framework.decorators import action -from apps.cm.serializers import TidSerializer, LabelMatSerializer +from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer from apps.inm.models import MaterialBatch, MIOItem from apps.wpm.models import WMaterial from rest_framework.exceptions import ParseError, NotFound from rest_framework.response import Response -from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin +from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin, CustomModelViewSet # Create your views here. SPLIT_FIELD = "#" @@ -68,3 +68,13 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie rdata = LabelMatSerializer(obj).data rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}" return Response(rdata) + + +class LabelTemplateViewSet(CustomModelViewSet): + """ + list: 标签模板 + + 标签模板 + """ + queryset = LabelTemplate.objects.all() + serializer_class = LabelTemplateSerializer \ No newline at end of file diff --git a/apps/inm/services.py b/apps/inm/services.py index f7617c04..081adc9b 100644 --- a/apps/inm/services.py +++ b/apps/inm/services.py @@ -18,6 +18,7 @@ def do_out(item: MIOItem): """ if item.mb and item.mb.defect is not None: raise ParseError("生产领料不支持不合格品") + from apps.inm.models import MaterialBatch mio:MIO = item.mio belong_dept = mio.belong_dept @@ -26,6 +27,16 @@ def do_out(item: MIOItem): material:Material = item.material if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目 return + + # 获取defect + defect:Defect = None + if item.wm and item.mb: + raise ParseError("车间和仓库库存不能同时存在") + if item.wm: + defect = item.wm.defect + elif item.mb: + defect = item.mb.defect + action_list = [] mias = MIOItemA.objects.filter(mioitem=item) is_zhj = False # 是否组合件领料 @@ -35,16 +46,18 @@ def do_out(item: MIOItem): for i in range(len(mias_list)): material, batch, rate = mias_list[i] new_count = rate * item.count # 假设 item.count 存在 - action_list.append([material, batch, new_count]) + action_list.append([material, batch, new_count, None]) else: - action_list = [[item.material, item.batch, item.count]] + action_list = [[item.material, item.batch, item.count, defect]] if is_zhj: try: mb = MaterialBatch.objects.get( material=item.material, warehouse=item.warehouse, - batch=item.batch + batch=item.batch, + state=WMaterial.WM_OK, + defect=None ) except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e: raise ParseError(f"组合件批次错误!{e}") @@ -58,10 +71,14 @@ def do_out(item: MIOItem): raise ParseError("组合件暂不支持追踪单件") xbatches = [] + if is_zhj: + xbatches = [item.batch] for al in action_list: xmaterial:Material = al[0] xbatch:str = al[1] xcount:str = al[2] + defect:Defect = al[3] + xbatches.append(xbatch) if xcount <= 0: raise ParseError("存在非正数!") @@ -72,8 +89,8 @@ def do_out(item: MIOItem): material=xmaterial, warehouse=item.warehouse, batch=xbatch, - state=10, - defect=None + state=WMaterial.WM_OK, + defect=defect ) except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e: raise ParseError(f"批次错误!{e}") @@ -85,9 +102,10 @@ def do_out(item: MIOItem): # 领到车间库存(或工段) - wm, new_create = WMaterial.objects.get_or_create(batch=xbatch, material=xmaterial, - belong_dept=belong_dept, mgroup=mgroup, - state=WMaterial.WM_OK) + wm, new_create = WMaterial.objects.get_or_create( + batch=xbatch, material=xmaterial, + belong_dept=belong_dept, mgroup=mgroup, + state=WMaterial.WM_OK, defect=defect) if new_create: wm.create_by = do_user wm.batch_ofrom = mb.batch if mb else None @@ -116,8 +134,9 @@ def do_in(item: MIOItem): 生产入库后更新车间物料 """ mio = item.mio - if item.wm and item.wm.defect is not None: - raise ParseError("不合格物料无法入库") + wmin:WMaterial = item.wm + if wmin and wmin.state != WMaterial.WM_OK: + raise ParseError("非合格物料无法入库") belong_dept = mio.belong_dept mgroup = mio.mgroup do_user = mio.do_user @@ -127,30 +146,44 @@ def do_in(item: MIOItem): action_list = [] mias = MIOItemA.objects.filter(mioitem=item) is_zhj = False # 是否组合件入仓库 + + # 获取defect + defect:Defect = None + if item.wm and item.mb: + raise ParseError("车间和仓库库存不能同时存在") + if item.wm: + defect = item.wm.defect + elif item.mb: + defect = item.mb.defect + if mias.exists(): is_zhj = True mias_list = mias.values_list('material', 'batch', 'rate') for i in mias_list: material, batch, rate = i new_count = rate * item.count # 假设 item.count 存在 - action_list.append([material, batch, new_count]) + action_list.append([material, batch, new_count, None]) else: - action_list = [[item.material, item.batch, item.count]] + action_list = [[item.material, item.batch, item.count, defect]] production_dept = None xbatchs = [] + if is_zhj: + xbatchs = [item.batch] for al in action_list: - xmaterial, xbatch, xcount = al + xmaterial, xbatch, xcount, defect = al if xcount <= 0: raise ParseError("存在非正数!") + xbatchs.append(xbatch) - # 扣减车间库存 + wm_qs = WMaterial.objects.filter( batch=xbatch, material=xmaterial, belong_dept=belong_dept, - mgroup=mgroup, + mgroup=mgroup, + defect=defect, state=WMaterial.WM_OK) count_x = wm_qs.count() if count_x == 1: @@ -161,6 +194,8 @@ def do_in(item: MIOItem): else: raise ParseError( f'{str(xmaterial)}-{xbatch}-存在多个相同批次!') + + # 扣减车间库存 new_count = wm.count - xcount if new_count >= 0: wm.count = new_count @@ -180,8 +215,8 @@ def do_in(item: MIOItem): material=xmaterial, warehouse=item.warehouse, batch=xbatch, - state=10, - defect=None, + state=WMaterial.WM_OK, + defect=defect, defaults={ "count": 0, "batch_ofrom": wm.batch_ofrom, @@ -208,6 +243,8 @@ def do_in(item: MIOItem): material=item.material, warehouse=item.warehouse, batch=item.batch, + defect=None, + state=WMaterial.WM_OK, defaults={"count": 0, "production_dept": production_dept} ) if not is_created: @@ -351,7 +388,9 @@ class InmService: defect = defects_map[defect_id] m_list.append((material, warehouse, i.batch, xcount, defect, i)) + xbatchs = [] for material, warehouse, batch, change_count, defect, mioitem in m_list: + xbatchs.append(batch) if change_count <= 0: raise ParseError("存在非正数!") state = WMaterial.WM_OK @@ -402,6 +441,10 @@ class InmService: else: raise ParseError("不支持的操作") + # 批次统计分析 + xbatchs = list(set(xbatchs)) + for xbatch in xbatchs: + MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start() def daoru_mb(path: str): """ diff --git a/apps/mtm/migrations/0057_process_number_to_batch.py b/apps/mtm/migrations/0057_process_number_to_batch.py new file mode 100644 index 00000000..fa8017f0 --- /dev/null +++ b/apps/mtm/migrations/0057_process_number_to_batch.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2025-04-28 06:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0056_mgroup_batch_append_code'), + ] + + operations = [ + migrations.AddField( + model_name='process', + name='number_to_batch', + field=models.BooleanField(default=False, verbose_name='个号转批号'), + ), + ] diff --git a/apps/mtm/models.py b/apps/mtm/models.py index c432a023..c1dde7f9 100644 --- a/apps/mtm/models.py +++ b/apps/mtm/models.py @@ -16,6 +16,7 @@ class Process(CommonBModel): PRO_NORMAL = 10 PRO_DIV = 20 PRO_MERGE = 30 + name = models.CharField('工序名称', max_length=100) type = models.PositiveSmallIntegerField("工序类型", default=PRO_PROD, choices=((PRO_PROD, '生产工序'), (PRO_TEST, '检验工序'))) mtype = models.PositiveSmallIntegerField("工序生产类型", default=PRO_NORMAL, choices=((PRO_NORMAL, '常规'), (PRO_DIV, '切分'), (PRO_MERGE, '合并'))) @@ -30,6 +31,7 @@ class Process(CommonBModel): mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False) mstate_json = models.JSONField('中间状态', default=list, blank=True) parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True) + number_to_batch = models.BooleanField('个号转批号', default=False) class Meta: verbose_name = '工序' diff --git a/apps/sam/services.py b/apps/sam/services.py index 65ce2b01..b577991e 100644 --- a/apps/sam/services.py +++ b/apps/sam/services.py @@ -1,6 +1,6 @@ from apps.sam.models import Order, OrderItem from rest_framework.exceptions import ValidationError -from django.db.models import F +from django.db.models import F, Sum from apps.inm.models import MIO, MIOItem diff --git a/apps/wpm/migrations/0114_auto_20250428_1350.py b/apps/wpm/migrations/0114_auto_20250428_1350.py new file mode 100644 index 00000000..34924007 --- /dev/null +++ b/apps/wpm/migrations/0114_auto_20250428_1350.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2025-04-28 05:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wpm', '0113_mlog_team'), + ] + + operations = [ + migrations.AddField( + model_name='mlogb', + name='number_from', + field=models.TextField(blank=True, null=True, verbose_name='来源个编号'), + ), + migrations.AddField( + model_name='wmaterial', + name='number_from', + field=models.TextField(blank=True, null=True, verbose_name='来源于个号'), + ), + ] diff --git a/apps/wpm/models.py b/apps/wpm/models.py index cf57b70a..f6e306dd 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -118,6 +118,7 @@ class WMaterial(CommonBDModel): count_xtest = models.DecimalField('已检数量', null=True, blank=True, max_digits=11, decimal_places=1) batch_ofrom = models.TextField('原料批次号', null=True, blank=True) material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom') + number_from = models.TextField("来源于个号", null=True, blank=True) @property def count_working(self): @@ -382,6 +383,7 @@ class Mlogb(BaseModel): test_user = models.ForeignKey( User, verbose_name='抽检人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlogb_test_user') need_inout = models.BooleanField('是否需要出入库', default=True) + number_from = models.TextField('来源个编号', null=True, blank=True) def get_tracking(self): @@ -466,6 +468,8 @@ class Mlogbw(BaseModel): @classmethod def cal_count_notok(cls, mlogb: Mlog): from apps.qm.models import Defect + # 锁定mlogb以防止并发修改 + mlogb = Mlogb.objects.select_for_update().get(pk=mlogb.pk) count = Mlogbw.objects.filter(mlogb=mlogb).count() if mlogb.material_in: mlogb.count_use = count @@ -631,6 +635,8 @@ class BatchSt(BaseModel): return node, False else: version = 1 + if mio is None and handover is None and mlog is None: + raise ParseError("mio or handover or mlog must be provided") # 带有来源的批次获取,需检查批次号是否可用 if cls.objects.filter(batch=batch).exists(): if reuse_node: @@ -639,11 +645,8 @@ class BatchSt(BaseModel): raise ParseError(f"{batch}-该批次号因物料不同不可引用") return node, False else: - raise ParseError(f"{batch}-该批次号不可使用") - # latest_version = BatchSt.objects.filter(batch=batch).aggregate(Max("version"))["version__max"] - # version = latest_version + 1 - if mio is None and handover is None and mlog is None: - raise ParseError("mio or handover or mlog must be provided") + latest_version = BatchSt.objects.filter(batch=batch).aggregate(Max("version"))["version__max"] + version = latest_version + 1 ins = cls.objects.create(batch=batch, mio=mio, handover=handover, mlog=mlog, material_start=material_start, version=version) return ins, True diff --git a/apps/wpm/serializers.py b/apps/wpm/serializers.py index 8edd22c3..fe1a62c1 100644 --- a/apps/wpm/serializers.py +++ b/apps/wpm/serializers.py @@ -724,6 +724,7 @@ class MlogbInSerializer(CustomModelSerializer): attrs['batch'] = wm_in.batch attrs["batch_ofrom"] = wm_in.batch_ofrom attrs["material_ofrom"] = wm_in.material_ofrom + attrs["number_from"] = wm_in.number_from if route and route.batch_bind and mtask is not None: if not WMaterial.mat_in_qs(mtask).filter(id=wm_in.id).exists(): raise ParseError('该车间库存非本任务使用') @@ -799,8 +800,11 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer): in_or_out, tracking = mlogb.get_tracking() if tracking != Material.MA_TRACKING_SINGLE: raise ParseError('非单件追踪不可使用') - if in_or_out == 'in' and not wpr: - raise ParseError('请选择相应产品') + if in_or_out == 'in': + if not wpr: + raise ParseError('请选择相应产品') + if mlogb.wm_in and wpr.wm != mlogb.wm_in: + raise ParseError(f'{wpr.number}-该产品非本批次') return attrs def save_ftest(self, mlogbw, ftest_data): diff --git a/apps/wpm/services.py b/apps/wpm/services.py index 825fb63d..48df5540 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -340,6 +340,10 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): elif isinstance(mlog_or_b, Mlogb): wm.batch_ofrom = mlog_or_b.batch_ofrom wm.material_ofrom = mlog_or_b.material_ofrom + if isinstance(mlog_or_b, Mlogb): + if mlog_or_b.number_from and wm.number_from is not None and wm.number_from != mlog_or_b.number_from: + raise ParseError(f'{wm.batch}-该批号现有库存来源于个号{wm.number_from}') + wm.number_from = mlog_or_b.number_from wm.save() if mo_ma.tracking == Material.MA_TRACKING_SINGLE: if notok_sign_or_defect: @@ -358,7 +362,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): wpr_from = item.mlogbw_from.wpr wpr = Wpr.change_or_new(number=item.number, wm=wm, ftest=item.ftest, - wpr_from=wpr_from, batch_from=item.mlogb.batch) + wpr_from=wpr_from) item.wpr = wpr item.save() @@ -481,6 +485,9 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): if wm.count < 0: raise ParseError('车间库存不足, 产物无法回退') elif wm.count >= 0: + if isinstance(mlog_or_b, Mlogb): + if mlog_or_b.number_from == wm.number_from: + wm.number_from = None wm.update_by = user wm.save() if mo_ma.tracking == Material.MA_TRACKING_SINGLE: @@ -793,7 +800,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, defaults={ "batch_ofrom": wm_from.batch_ofrom, "material_ofrom": wm_from.material_ofrom, - "create_by": user + "create_by": user, + "number_from": wm_from.number_from } ) elif handover.type == Handover.H_REPAIR: @@ -816,7 +824,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, defaults={ "batch_ofrom": wm_from.batch_ofrom, "material_ofrom": wm_from.material_ofrom, - "create_by": user + "create_by": user, + "number_from": wm_from.number_from } ) else: @@ -848,7 +857,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, defaults={ "batch_ofrom": wm_from.batch_ofrom, "material_ofrom": wm_from.material_ofrom, - "create_by": user + "create_by": user, + "number_from": wm_from.number_from } ) else: @@ -867,7 +877,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, defaults={ "batch_ofrom": wm_from.batch_ofrom, "material_ofrom": wm_from.material_ofrom, - "create_by": user + "create_by": user, + "number_from": wm_from.number_from } ) else: @@ -887,7 +898,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, defaults={ "batch_ofrom": wm_from.batch_ofrom, "material_ofrom": wm_from.material_ofrom, - "create_by": user + "create_by": user, + "number_from": wm_from.number_from } ) else: diff --git a/apps/wpm/services_2.py b/apps/wpm/services_2.py index 66bfea14..1f0787ff 100644 --- a/apps/wpm/services_2.py +++ b/apps/wpm/services_2.py @@ -12,7 +12,7 @@ import decimal import logging myLogger = logging.getLogger('log') -def get_alldata_with_batch_and_store(batch: str): +def get_alldata_with_batch_and_store(batch: str, need_update_time=True): """ 获取某个批次的整体生产数据并保存 """ @@ -29,7 +29,8 @@ def get_alldata_with_batch_and_store(batch: str): bobj, _ = BatchSt.objects.get_or_create(batch=batch, defaults={ "last_time": last_time }) - bobj.last_time = last_time + if need_update_time: + bobj.last_time = last_time bobj.data = json.loads(json.dumps(data, cls=MyJSONEncoder)) bobj.save() @@ -278,23 +279,6 @@ def get_alldata_with_batch(batch: str): myLogger.error(f"六车间_{mgroup_name}_合格率decimal.InvalidOperation-{data}") data[f'六车间_{mgroup_name}_合格率'] = 0 - # 六车间入库/检验数据 - # mioitem6_qs2 = MIOItem.objects.filter(mio__belong_dept=dept6, mio__type="do_in", - # batch=batch, - # mio__submit_time__isnull=False) - # if mioitem6_qs2.exists(): - # data["六车间生产入库_日期"] = [] - # for item in mioitem6_qs: - # data["六车间生产入库_日期"].append(item.mio.inout_date) - # for field in mioitem_count_fields: - # if getattr(item, field) > 0: - # if f'六车间生产入库_{field}' not in data: - # data[f'六车间生产入库_{field}'] = getattr(item, field) - # else: - # data[f'六车间生产入库_{field}'] += getattr(item, field) - # data["六车间生产入库_日期"] = list(set(data["六车间生产入库_日期"])) - # data["六车间生产入库_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间生产入库_日期"]]) - ftestwork_count_fields = FtestWork.count_fields() # 六车间中检数据 ftestwork_qs = FtestWork.objects.filter(batch=batch, type="process") @@ -325,6 +309,34 @@ def get_alldata_with_batch(batch: str): data['六车间中检_检验人'] = list(set(data['六车间中检_检验人'])) data['六车间中检_检验人'] = ";".join([item.name for item in data['六车间中检_检验人']]) + # 六车间入库/检验数据 + mioitem6_qs2 = MIOItem.objects.filter(mio__belong_dept=dept6, mio__type="do_in", + batch=batch, + mio__submit_time__isnull=False) + if mioitem6_qs2.exists(): + data["六车间生产入库_日期"] = [] + data["六车间生产入库_检验日期"] = [] + data["六车间生产入库_检验人"] = [] + for item in mioitem6_qs2: + data["六车间生产入库_日期"].append(item.mio.inout_date) + if item.test_date: + data["六车间生产入库_检验日期"].append(item.test_date) + for field in mioitem_count_fields: + if getattr(item, field) is not None and (getattr(item, field) > 0 or field in ["count", "count_notok"]): + if f'六车间生产入库_{field}' not in data: + data[f'六车间生产入库_{field}'] = getattr(item, field) + else: + data[f'六车间生产入库_{field}'] += getattr(item, field) + data["六车间生产入库_日期"] = list(set(data["六车间生产入库_日期"])) + data["六车间生产入库_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间生产入库_日期"]]) + data["六车间生产入库_检验日期"] = list(set(data["六车间生产入库_检验日期"])) + data["六车间生产入库_检验日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["六车间生产入库_检验日期"]]) + try: + data['六车间生产入库_合格率'] = round((data['六车间生产入库_count'] - data['六车间生产入库_count_notok']) * 100/ data['六车间生产入库_count'], 1) + except decimal.InvalidOperation: + myLogger.error("六车间生产入库_合格率decimal.InvalidOperation-{data}") + data['六车间生产入库_合格率'] = 0 + # 成品检验数据 ftestwork_qs = FtestWork.objects.filter(batch=batch, type="prod") if ftestwork_qs.exists(): @@ -354,9 +366,29 @@ def get_alldata_with_batch(batch: str): data['成品检验_检验人'] = list(set(data['成品检验_检验人'])) data['成品检验_检验人'] = ";".join([item.name for item in data['成品检验_检验人']]) data['成品检验_合格率'] = round(data['成品检验_count_ok'] * 100/ data['成品检验_count'], 1) - data["六车间_批次生产合格率"] = round(data["成品检验_count_ok"] * 100/ data["六车间领料_count"], 1) - if data["棒料成型_count_real"]: + if getattr(data, "六车间领料_count", 0) > 0: + data["六车间_批次生产合格率"] = round(data["成品检验_count_ok"] * 100/ data["六车间领料_count"], 1) + if getattr(data, "棒料成型_count_real", 0) > 0: data["七车间_批次应出合格率"] = round(data["成品检验_count_ok"] * 100/ data["棒料成型_count_real"], 1) + # 销售发货数据 + mioitem_qs = MIOItem.objects.filter(batch=batch, mio__type="sale_out", mio__submit_time__isnull=False) + if mioitem_qs.exists(): + data["销售发货_日期"] = [] + data['销售发货_仓库执行人'] = [] + data['销售发货_count'] = 0 + for item in mioitem_qs: + last_time = item.mio.update_time if item.mio.update_time > last_time else last_time + if item.mio.inout_date: + data["销售发货_日期"].append(item.mio.inout_date) + if item.mio.do_user: + data['销售发货_仓库执行人'].append(item.mio.do_user) + data['销售发货_count']+= item.count + if getattr(data, "棒料成型_count_real", 0) > 0: + data["七车间_批次发货合格率"] = round(data["销售发货_count"] * 100/ data["棒料成型_count_real"], 1) + if getattr(data, "六车间领料_count", 0) > 0: + data["六车间_批次发货合格率"] = round(data["销售发货_count"] * 100/ data["六车间领料_count"], 1) + data['销售发货_仓库执行人'] = ";".join([item.name for item in data['销售发货_仓库执行人']]) + return last_time, data \ No newline at end of file diff --git a/apps/wpm/views.py b/apps/wpm/views.py index 653bab38..67c49ed1 100644 --- a/apps/wpm/views.py +++ b/apps/wpm/views.py @@ -563,6 +563,7 @@ class MlogbInViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, Cust mlogbin: Mlogb = serializer.save() mlog:Mlog = mlogbin.mlog route:Route = mlog.route + process: Process = route.process if route else None mtype = route.process.mtype if route else None is_fix = mlog.is_fix qct = mlog.qct @@ -614,9 +615,17 @@ class MlogbInViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, Cust elif mtype == Process.PRO_DIV: # 切分 支持批到批,个到个, 个到批 div_number = route.div_number if material_in.tracking == Material.MA_TRACKING_SINGLE and material_out.tracking == Material.MA_TRACKING_BATCH: + lenx = Mlogbw.objects.filter(mlogb=mlogbin).count() + # 用个号做批号是用于后续在复用个号可以追踪到原先的个 for mlogbwin in Mlogbw.objects.filter(mlogb=mlogbin).order_by("number"): - m_dict["batch"] = mlogbwin.number - Mlogb.objects.get_or_create(mlogbw_from=mlogbwin, defaults=update_dict(m_dict, {"count_real": div_number, "count_ok": div_number})) + if process and process.number_to_batch: + m_dict["batch"] = mlogbwin.number + mlogbout, _ = Mlogb.objects.get_or_create( + mlogbw_from=mlogbwin, defaults=update_dict(m_dict, {"count_real": div_number, "count_ok": div_number})) + if lenx == 1: + mlogbout.mlogb_from = mlogbin + mlogbout.number_from = mlogbwin.number + mlogbout.save() elif material_in.tracking == Material.MA_TRACKING_SINGLE and material_out.tracking == Material.MA_TRACKING_SINGLE: d_count_real = mlogbin.count_use * div_number d_count_ok = d_count_real @@ -635,13 +644,22 @@ class MlogbInViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, Cust mlogbout, _ = Mlogb.objects.get_or_create(mlogb_from=mlogbin, defaults=update_dict(m_dict,{"count_real": d_count_real, "count_ok": d_count_ok})) mlogbout.count_json_from = mlogbin.count_json_from mlogbout.save(update_fields=["count_json_from"]) - elif mtype == Process.PRO_MERGE: # 支持批到批 + elif mtype == Process.PRO_MERGE: # 支持批到批,批到个 xcount = math.floor( mlogbin.count_use / route.div_number) d_count_real = xcount d_count_ok = xcount mlogbout, _ = Mlogb.objects.get_or_create(mlogb_from=mlogbin, defaults=update_dict(m_dict, {"count_real": d_count_real, "count_ok": d_count_ok})) mlogbout.count_json_from = mlogbin.count_json_from mlogbout.save(update_fields=["count_json_from"]) + if material_out.tracking == Material.MA_TRACKING_SINGLE: + number = mlogbin.batch + if mlogbin.number_from: + number = mlogbin.number_from + if d_count_real == 1: + Mlogbw.objects.get_or_create(number=number, mlogb=mlogbout) + else: + for i in range(d_count_real): + Mlogbw.objects.get_or_create(number=f'{number}-{i+1}', mlogb=mlogbout) elif is_fix:# 支持批到批,个到个 d_count_real = mlogbin.count_use d_count_ok = mlogbin.count_use @@ -711,43 +729,57 @@ class MlogbwViewSet(CustomModelViewSet): @transaction.atomic def perform_create(self, serializer): ins:Mlogbw = serializer.save() - route:Route = ins.mlogb.mlog.route - mlogb:Mlogb = ins.mlogb - Mlogbw.cal_count_notok(mlogb) - # 如果是输入且输出追踪到个,需同步创建 - material_in:Material = mlogb.material_in - if material_in is not None: - mlogb_qs = Mlogb.objects.filter(mlogb_from=mlogb) - material_out:Material = mlogb.mlog.material_out - mtype = route.process.mtype if route.process else None - if mtype == Process.PRO_DIV: - mlogbin = ins.mlogb - wm_in = mlogbin.wm_in - mlog = mlogbin.mlog - div_number = route.div_number - m_dict = { - "mtask": mlogbin.mtask, - "mlog": mlog, - "batch": ins.number, - "material_out": material_out, - "batch_ofrom": wm_in.batch_ofrom, - "material_ofrom": wm_in.material_ofrom, - "count_real": div_number, - "count_ok": div_number, "qct": mlog.qct - } - mlogbout, _ = Mlogb.objects.get_or_create(mlogbw_from=ins, defaults=m_dict) - if material_out.tracking == Material.MA_TRACKING_SINGLE: - for i in range(div_number): - Mlogbw.objects.get_or_create(mlogb=mlogbout, number=f"{ins.number}-{i+1}", defaults={"mlogbw_from": ins}) - Mlogbw.cal_count_notok(mlogbout) - elif mlogb_qs.exists() and material_out.tracking == Material.MA_TRACKING_SINGLE: - for mlogb in mlogb_qs: - if route.process.mtype == Process.PRO_NORMAL: - Mlogbw.objects.get_or_create(mlogb=mlogb, wpr=ins.wpr, defaults={"number": ins.number, "mlogbw_from": ins}) - elif route.process.mtype == Process.PRO_DIV: - for i in range(route.div_number): - Mlogbw.objects.get_or_create(mlogb=mlogb, number=f'{ins.number}-{i+1}', defaults={"mlogbw_from": ins}) - Mlogbw.cal_count_notok(mlogb) + if isinstance(ins, list): + insx = ins + else: + insx = [ins] + for ins in insx: + route:Route = ins.mlogb.mlog.route + mlogb:Mlogb = ins.mlogb + Mlogbw.cal_count_notok(mlogb) + # 如果是输入且输出追踪到个,需同步创建 + material_in:Material = mlogb.material_in + if material_in is not None: + mlogb_qs = Mlogb.objects.filter(mlogb_from=mlogb) + material_out:Material = mlogb.mlog.material_out + mtype = route.process.mtype if route.process else None + if mtype == Process.PRO_DIV: + mlogbin = ins.mlogb + wm_in = mlogbin.wm_in + mlog = mlogbin.mlog + div_number = route.div_number + m_dict = { + "mtask": mlogbin.mtask, + "mlog": mlog, + "batch": ins.number, + "material_out": material_out, + "batch_ofrom": wm_in.batch_ofrom, + "material_ofrom": wm_in.material_ofrom, + "count_real": div_number, + "count_ok": div_number, "qct": mlog.qct + } + mlogbout, _ = Mlogb.objects.get_or_create(mlogbw_from=ins, defaults=m_dict) + if material_out.tracking == Material.MA_TRACKING_SINGLE: + for i in range(div_number): + Mlogbw.objects.get_or_create(mlogb=mlogbout, number=f"{ins.number}-{i+1}", defaults={"mlogbw_from": ins}) + Mlogbw.cal_count_notok(mlogbout) + elif material_out.tracking == Material.MA_TRACKING_BATCH: + number_from = mlogbout.number_from + if number_from is None: + mlogbout.number_from = ins.number + mlogbout.save() + elif number_from == ins.number: + pass + else: + raise ParseError("该个号不可产生该批") + elif mlogb_qs.exists() and material_out.tracking == Material.MA_TRACKING_SINGLE: + for mlogb in mlogb_qs: + if route.process.mtype == Process.PRO_NORMAL: + Mlogbw.objects.get_or_create(mlogb=mlogb, wpr=ins.wpr, defaults={"number": ins.number, "mlogbw_from": ins}) + elif route.process.mtype == Process.PRO_DIV: + for i in range(route.div_number): + Mlogbw.objects.get_or_create(mlogb=mlogb, number=f'{ins.number}-{i+1}', defaults={"mlogbw_from": ins}) + Mlogbw.cal_count_notok(mlogb) @transaction.atomic def perform_update(self, serializer): diff --git a/apps/wpmw/models.py b/apps/wpmw/models.py index 2c0df756..f1121708 100644 --- a/apps/wpmw/models.py +++ b/apps/wpmw/models.py @@ -26,7 +26,7 @@ class Wpr(BaseModel): wpr_from = models.ForeignKey("self", verbose_name="来源于", on_delete=models.CASCADE, null=True, blank=True) @classmethod - def change_or_new(cls, wpr=None, number=None, mb=None, wm=None, old_mb=None, old_wm=None, ftest=None, wpr_from=None, batch_from=None): + def change_or_new(cls, wpr=None, number=None, mb=None, wm=None, old_mb=None, old_wm=None, ftest=None, wpr_from=None): if wpr is None and number is None: raise ParseError("id和number不能同时为空") if mb and wm: @@ -38,30 +38,44 @@ class Wpr(BaseModel): wpr.delete() return elif number: - try: - ins_x = cls.objects.get(number=number) + ins_x = cls.objects.filter(number=number).order_by("-version").first() + if ins_x: if ins_x.wm is None and ins_x.mb is None: if ins_x.version > 1: # 说明被复用了 if wpr_from is None: wpr_from = ins_x + # 创建新的wpr + ins = cls(number=number) + ins.version = -1 + ins.oinfo = {} else: raise ParseError(f"该物料编号{number}-已存在不可使用") else: raise ParseError(f"该物料编号{number}-已存在不可使用") - except cls.DoesNotExist: + else: ins = cls(number=number) ins.version = -1 ins.oinfo = {} - if batch_from: # 尝试从批号追踪来源 - try: - ins_from = cls.objects.get(number=number) - if ins_from.wm is None and ins_from.mb is None: - if ins_from.version > 1: # 说明被复用了 - wpr_from = ins_from - else: - raise ParseError(f"该物料编号{number}-已存在不可使用") - except cls.DoesNotExist: - pass + # if batch_from: # 尝试从批号追踪来源 + # ins_from = cls.objects.filter(number=batch_from).order_by("-version").first() + # if ins_from: + # if ins_from.wm is None and ins_from.mb is None: + # if ins_from.version > 1: # 说明被复用了 + # wpr_from = ins_from + # else: + # raise ParseError(f"该物料编号{number}-已存在不可使用") + # elif wpr_from is None: + # raise ParseError(f"该物料编号{number}-尝试从批号追踪来源失败") + # elif number_from: # 尝试从编号追踪来源 + # ins_from = cls.objects.filter(number=number_from).order_by("-version").first() + # if ins_from: + # if ins_from.wm is None and ins_from.mb is None: + # if ins_from.version > 1: # 说明被复用了 + # wpr_from = ins_from + # else: + # raise ParseError(f"该物料编号{number}-已存在不可使用") + # elif wpr_from is None: + # raise ParseError(f"该物料编号{number}-尝试历史编号追踪来源失败") if old_mb and ins.mb != old_mb: raise ParseError(f"请检查-{ins.number}-所属仓库批次") diff --git a/changelog.md b/changelog.md index 9b22aba2..0daaedaf 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,42 @@ +## 2.6.2025043014 +- feat: 新增功能 + - 生产入库和领料支持b类合格品 [caoqianming] + - 添加标签模板接口 [caoqianming] + - 完善光子批次统计数据 [caoqianming] + - 组合件和入库检验追加统计分析 [caoqianming] + - get_alldata_with_batch添加内容 [caoqianming] +- fix: 问题修复 + - mio_saleout缺少import [caoqianming] +## 2.6.2025042816 +- feat: 新增功能 + - 支持个号转批号及配置 [caoqianming] + - 批次追踪链条还是可以复用批次 [caoqianming] + - mlog结合工序支持批到个 [caoqianming] + - mlogbw支持批量创建 [caoqianming] + - mlogbwcreate校验增加wm所属 [caoqianming] + - ichat 修改LLM 的接口 [zty] + - wmaterial筛选条件优化来料已完成 [caoqianming] + - 获取该批次的dag数据需要传入version [caoqianming] + - 单填写mlog支持返工 [caoqianming] + - ichat 修改大模型接口 [zty] + - mlogbupdate支持变更批号 [caoqianming] + - 6车间合格率统计decimal invalid [caoqianming] + - 修改大模型文件 [zty] + - model add note [zty] + - ichat添加表 [caoqianming] + - 缺陷项分类字段数据库约束放开 [caoqianming] + - 通过django settings延迟获取BASE_PROJECT_CODE [caoqianming] + - base 将配置文件放到单独的config文件夹中防止误操作 [caoqianming] +- fix: 问题修复 + - wpr复用number的bug [caoqianming] + - cal_count_notok锁定mlogb以防止并发修改 [caoqianming] + - 获取batchst时默认使用version=1 [caoqianming] + - mloginit在返工时不接收route [caoqianming] + - 生产入库时存入生产车间 [caoqianming] + - 返工校验输入物料选择错误 [caoqianming] + - 完善负数校验 [caoqianming] + - mlogbupdate时attrs遍历时修改的bug [caoqianming] + - batchst的version字段bug [caoqianming] ## 2.6.2025042311 - feat: 新增功能 - 切分工序切分数量支持1 [caoqianming] diff --git a/server/settings.py b/server/settings.py index cdfee897..dceec682 100755 --- a/server/settings.py +++ b/server/settings.py @@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) ALLOWED_HOSTS = ['*'] SYS_NAME = '星途工厂综合管理系统' -SYS_VERSION = '2.6.2025042311' +SYS_VERSION = '2.6.2025043014' X_FRAME_OPTIONS = 'SAMEORIGIN' # Application definition diff --git a/server/urls.py b/server/urls.py index 0eb2954c..473e362f 100755 --- a/server/urls.py +++ b/server/urls.py @@ -44,7 +44,7 @@ urlpatterns = [ # api path('', include('apps.auth1.urls')), - path('', include('apps.ichat.urls')), + # path('', include('apps.ichat.urls')), path('', include('apps.system.urls')), path('', include('apps.monitor.urls')), path('', include('apps.wf.urls')),