From 02e3265133f05b741e347cd2bc698fd3fa8df985 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 8 Jan 2026 09:59:39 +0800 Subject: [PATCH 01/30] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/edu/urls.py | 2 +- apps/inm/urls.py | 8 ++-- apps/wpm/urls.py | 6 +-- requirements.txt | 101 ++++++++++++++++++++++++++++++++++------------- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/apps/edu/urls.py b/apps/edu/urls.py index 55341e70..c23392b1 100644 --- a/apps/edu/urls.py +++ b/apps/edu/urls.py @@ -11,7 +11,7 @@ router.register('question', QuestionViewSet, basename='question') router.register('paper', PaperViewSet, basename='paper') router.register('exam', ExamViewSet, basename='exam') router.register('examrecord', ExamRecordViewSet, basename='examrecord') -router.register('training', TrainRecordViewSet, basename='examrecord') +router.register('training', TrainRecordViewSet, basename='training') urlpatterns = [ path(API_BASE_URL, include(router.urls)), ] diff --git a/apps/inm/urls.py b/apps/inm/urls.py index 48000721..430cb21f 100644 --- a/apps/inm/urls.py +++ b/apps/inm/urls.py @@ -13,10 +13,10 @@ router.register('warehouse', WarehouseVIewSet, basename='warehouse') router.register('materialbatch', MaterialBatchViewSet, basename='materialbatch') router.register('mio', MIOViewSet, basename='mio') -router.register('mio/do', MioDoViewSet) -router.register('mio/sale', MioSaleViewSet) -router.register('mio/pur', MioPurViewSet) -router.register('mio/other', MioOtherViewSet) +router.register('mio/do', MioDoViewSet, basename='mio_do') +router.register('mio/sale', MioSaleViewSet, basename='mio_sale') +router.register('mio/pur', MioPurViewSet, basename='mio_pur') +router.register('mio/other', MioOtherViewSet, basename='mio_other') router.register('mioitem', MIOItemViewSet, basename='mioitem') router.register('mioitemw', MIOItemwViewSet, basename='mioitemw') # router.register('pack', PackViewSet, basename='pack') diff --git a/apps/wpm/urls.py b/apps/wpm/urls.py index 9dfb2fc4..f2dedfe5 100644 --- a/apps/wpm/urls.py +++ b/apps/wpm/urls.py @@ -20,9 +20,9 @@ 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) -router.register('mlogb/out', MlogbOutViewSet) +router.register('mlogb', MlogbViewSet, basename='mlogb') +router.register('mlogb/in', MlogbInViewSet, basename='mlogb_in') +router.register('mlogb/out', MlogbOutViewSet, basename='mlogb_out') router.register('handover', HandoverViewSet, basename='handover') router.register('attlog', AttlogViewSet, basename='attlog') router.register('otherlog', OtherLogViewSet, basename='otherlog') diff --git a/requirements.txt b/requirements.txt index f04b0c51..b6e620fc 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,37 +1,82 @@ -celery==5.2.3 -Django==3.2.12 -django-celery-beat==2.3.0 -django-celery-results==2.4.0 -django-cors-headers==3.11.0 -django-filter==21.1 -djangorestframework==3.13.1 -djangorestframework-simplejwt==5.1.0 -drf-yasg==1.21.7 -psutil==5.9.0 -pillow==9.0.1 -opencv-python==4.5.5.62 -redis==4.4.0 -django-redis==5.2.0 -user-agents==2.2.0 -daphne==4.0.0 -channels-redis==4.0.0 +# ======================= +# Core +# ======================= +Django>=4.2,<4.3 + +djangorestframework>=3.14.0 +django-filter>=23.5 +django-cors-headers>=4.3.0 + +djangorestframework-simplejwt>=5.2.2 django-restql==0.15.2 + +# ======================= +# Celery +# ======================= +celery>=5.3.6 +django-celery-beat>=2.5.0 +django-celery-results>=2.5.1 +redis>=4.4.0 +django-redis>=5.3.0 +cron-descriptor==1.2.35 + +# ======================= +# Channels / ASGI +# ======================= +channels>=4.0.0 +daphne>=4.0.0 +channels-redis>=4.1.0 + +# ======================= +# API Docs +# ======================= +drf-yasg==1.21.7 + +# ======================= +# Auth / Utils +# ======================= +user-agents==2.2.0 +psutil==5.9.0 + +# ======================= +# Media / Image / CV +# ======================= +pillow>=9.5.0 +opencv-python==4.5.5.62 shapely==1.8.3 -aliyun-python-sdk-core==2.13.36 -baidu-aip==4.16.6 -chardet==5.0.0 -requests==2.28.1 + +# ======================= +# Network / RPC +# ======================= +requests>=2.31.0 grpcio==1.47.0 grpcio-tools==1.47.0 protobuf==3.20.1 -pycryptodome==3.15.0 + +# ======================= +# Cloud SDK +# ======================= aliyun-python-sdk-core==2.13.36 +baidu-aip==4.16.6 + +# ======================= +# Crypto +# ======================= +pycryptodome==3.15.0 + +# ======================= +# Excel / Docs +# ======================= xlwt==1.3.0 -openpyxl==3.1.0 -cron-descriptor==1.2.35 -pymysql==1.0.3 -# face-recognition==1.3.0 +openpyxl>=3.1.2 docxtpl==0.16.7 + +# ======================= +# DB +# ======================= +pymysql==1.0.3 + +# ======================= +# IoT / MQTT +# ======================= paho-mqtt==2.0.0 -# deepface==0.0.79 -# edge-tts==6.1.12 From e2a92b6faa1729811e5326a0450074f7aaa95cc4 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 8 Jan 2026 10:40:00 +0800 Subject: [PATCH 02/30] =?UTF-8?q?feat:=20=E5=9B=BA=E5=AE=9A=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/requirements.txt b/requirements.txt index b6e620fc..0f5f390e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,31 +1,31 @@ # ======================= # Core # ======================= -Django>=4.2,<4.3 +Django==4.2.27 -djangorestframework>=3.14.0 -django-filter>=23.5 -django-cors-headers>=4.3.0 +djangorestframework==3.16.1 +django-filter==23.5 +django-cors-headers==4.9.0 -djangorestframework-simplejwt>=5.2.2 +djangorestframework-simplejwt==5.5.1 django-restql==0.15.2 # ======================= # Celery # ======================= -celery>=5.3.6 -django-celery-beat>=2.5.0 -django-celery-results>=2.5.1 -redis>=4.4.0 -django-redis>=5.3.0 +celery==5.6.2 +django-celery-beat==2.8.1 +django-celery-results==2.6.0 +redis==7.1.0 +django-redis==6.0.0 cron-descriptor==1.2.35 # ======================= # Channels / ASGI # ======================= -channels>=4.0.0 -daphne>=4.0.0 -channels-redis>=4.1.0 +channels==4.3.2 +daphne==4.0.0 +channels-redis==4.3.0 # ======================= # API Docs @@ -41,14 +41,14 @@ psutil==5.9.0 # ======================= # Media / Image / CV # ======================= -pillow>=9.5.0 +pillow==9.5.0 opencv-python==4.5.5.62 shapely==1.8.3 # ======================= # Network / RPC # ======================= -requests>=2.31.0 +requests==2.32.5 grpcio==1.47.0 grpcio-tools==1.47.0 protobuf==3.20.1 @@ -68,7 +68,7 @@ pycryptodome==3.15.0 # Excel / Docs # ======================= xlwt==1.3.0 -openpyxl>=3.1.2 +openpyxl==3.1.5 docxtpl==0.16.7 # ======================= From 43abcbaa4872da6fcf453df35248dbcebeee7619 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 9 Jan 2026 15:55:56 +0800 Subject: [PATCH 03/30] =?UTF-8?q?feat:=20=E6=9F=A5=E8=AF=A2-n=E6=89=B9?= =?UTF-8?q?=E6=AC=A1=E4=BB=8E=E6=AD=A3=E5=88=99=E6=94=B9=E7=94=A8like?= =?UTF-8?q?=E4=BB=A5=E4=BC=98=E5=8C=96=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/models.py | 75 +++++++++++++++++++++++++--------------------- apps/wpmw/views.py | 25 ++++++++++++---- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/apps/wpm/models.py b/apps/wpm/models.py index e45d71c2..26e37796 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -17,6 +17,8 @@ import re from django.db.models import Q import django.utils.timezone as timezone from apps.utils.sql import query_all_dict +import logging +myLogger = logging.getLogger('log') # Create your models here. class SfLog(CommonADModel): @@ -837,41 +839,46 @@ class BatchLog(BaseModel): @classmethod def batches_to(cls, batch:str): - - # query = """ - # SELECT batch FROM wpm_batchst - # WHERE batch ~ %s - # """ query = """ - SELECT batch - FROM wpm_batchst - WHERE batch ~ %s - ORDER BY - -- 先按前缀部分排序(例如 'A') - SUBSTRING(batch FROM '^(.*)-') DESC, - -- 再按后缀的数值部分排序(将 '2', '11' 转为整数) - CAST(SUBSTRING(batch FROM '-([0-9]+)$') AS INTEGER) DESC - """ # 排序可在sql层处理 - query_ = """SELECT batch FROM wpm_batchst WHERE batch ~ %s""" - pattern = f'^{batch}-[0-9]+$' + SELECT + batch, + CAST(substring(batch FROM LENGTH(%s) + 2) AS INTEGER) AS batch_num + FROM wpm_batchst + WHERE batch LIKE %s AND translate( + substring(batch FROM LENGTH(%s) + 2), + '0123456789', + '' + ) = '' + ORDER BY batch_num DESC + """ - """可以用如下方法直接查询 - """ - # batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct() - # batches = sorted(list(batches), key=custom_key) - batches_r = query_all_dict(query_, params=(pattern,)) - batches = [b["batch"] for b in batches_r] - batches = sorted(list(batches), key=custom_key) - last_batch_num = None - if batches: - last_batch = batches[-1] - last_batch_list = last_batch.split("-") - if last_batch_list: - try: - last_batch_num = int(last_batch_list[-1]) - except Exception: - pass - return {"batches": batches, "last_batch_num": last_batch_num, "last_batch": last_batch} - return {"batches": [], "last_batch_num": None, "last_batch": None} + prefix = batch + params = ( + prefix, + f"{prefix}-%", + prefix + ) + + try: + rows = query_all_dict(query, params=params) + except Exception as e: + myLogger.error(f"BatchLog.batches_to error: {(str(e), query, params)}") + raise + + if not rows: + return { + "batches": [], + "last_batch_num": None, + "last_batch": None, + } + + batches = [r["batch"] for r in rows] + last = rows[0] + + return { + "batches": batches, + "last_batch_num": last["batch_num"], + "last_batch": last["batch"], + } \ No newline at end of file diff --git a/apps/wpmw/views.py b/apps/wpmw/views.py index e99d2473..3663f180 100644 --- a/apps/wpmw/views.py +++ b/apps/wpmw/views.py @@ -92,11 +92,20 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu # 使用原始sql query = """ SELECT id, number_out FROM wpmw_wpr - WHERE number_out ~ %s order by number_out desc limit 1 + WHERE number_out LIKE %s + AND translate( + substring(number_out FROM LENGTH(%s) + 2), + '0123456789', + '' + ) = '' + order by number_out desc limit 1 """ - pattern = f"^{prefix}[0-9]+$" + params = ( + f"{prefix}-%", + prefix + ) number_outs = [] - wpr_qs_last = query_one_dict(query, [pattern]) + wpr_qs_last = query_one_dict(query, [params]) if wpr_qs_last: number_outs.append(wpr_qs_last["number_out"]) # 查找未出库的记录 @@ -106,9 +115,15 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu query2 = """ select mioitemw.id, mioitemw.number_out from inm_mioitemw mioitemw left join inm_mioitem mioitem on mioitem.id = mioitemw.mioitem_id left join inm_mio mio on mio.id = mioitem.mio_id - where mio.submit_time is null and mioitemw.number_out ~ %s order by mioitemw.number_out desc limit 1 + where mio.submit_time is null and mioitemw.number_out LIKE %s + AND translate( + substring(mioitemw.number_out FROM LENGTH(%s) + 2), + '0123456789', + '' + ) = '' + order by mioitemw.number_out desc limit 1 """ - mioitemw_last = query_one_dict(query2, [pattern]) + mioitemw_last = query_one_dict(query2, [params]) if mioitemw_last: number_outs.append(mioitemw_last["number_out"]) if number_outs: From 3417515e721b2abaeb47c3db77b8e306f389004e Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 9 Jan 2026 16:53:57 +0800 Subject: [PATCH 04/30] =?UTF-8?q?feat:=20base=20=E6=B7=BB=E5=8A=A0locked?= =?UTF-8?q?=5Fget=5For=5Fcreate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/utils/models.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/utils/models.py b/apps/utils/models.py index 1e715a9b..bdc4a486 100755 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -150,7 +150,32 @@ class BaseModel(models.Model): raise time.sleep(0.1 * (attempt + 1)) - + + @classmethod + def locked_get_or_create(cls, defaults: dict, **kwargs): + """ + 仅用于事务内 + 并发安全的 get_or_create + """ + if not connection.in_atomic_block: + raise RuntimeError("locked_get_or_create 必须在事务中调用") + + defaults = defaults or {} + + qs = cls.objects.select_for_update().filter(**kwargs) + + cnt = qs.count() + if cnt > 1: + raise RuntimeError( + f"{cls.__name__} 数据异常:定位条件 {kwargs} 命中 {cnt} 条" + ) + + if cnt == 1: + return qs.get(), False + + obj = cls.objects.create(**kwargs, **defaults) + return obj, True + def handle_parent(self): pass From 6eee0e1e536499fc8fc461bdf2a31f85122b5d5f Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 9 Jan 2026 16:54:24 +0800 Subject: [PATCH 05/30] =?UTF-8?q?feat:=20handover=5Fsubmit=20=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/services.py | 71 +++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/apps/wpm/services.py b/apps/wpm/services.py index 7b265863..1ce91ae0 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -22,7 +22,7 @@ from ..qm.models import Defect, Ftest from django.db.models import Count, Q from apps.utils.tasks import ctask_run from apps.mtm.models import Process -from apps.utils.lock import lock_model_record_d_func +from django.db.models import F myLogger = logging.getLogger('log') @@ -696,11 +696,15 @@ def update_mtask(mtask: Mtask, fill_way: int = 10): # utask.state = Utask.UTASK_SUBMIT utask.save() -@lock_model_record_d_func(Handover) def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]): """ 交接提交后需要执行的操作 """ + handover = ( + Handover.objects + .select_for_update() + .get(pk=handover.pk) + ) if handover.submit_time is not None: return now = timezone.now() @@ -744,7 +748,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, wmId, xcount, handover_or_b = item if xcount <= 0: raise ParseError("存在非正数!") - wm_from = WMaterial.objects.get(id=wmId) + wm_from = ( + WMaterial.objects + .select_for_update() + .get(id=wmId) + ) mids.append(wm_from.material.id) # 合并为新批 @@ -768,25 +776,24 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, batch = wm_from.batch batches.append(batch) - if wm_from is None: - raise ParseError(f'{wm_from.batch} 找不到车间库存') - - count_x = wm_from.count - xcount - if count_x < 0: + updated = ( + WMaterial.objects + .filter(id=wm_from.id, count__gte=xcount) + .update(count=F('count') - xcount) + ) + + if updated == 0: raise ParseError(f'{wm_from.batch} 车间库存不足!') - else: - wm_from.count = count_x - wm_from.save() if need_add: # 开始变动 if handover.type == Handover.H_NORMAL: if mtype == Handover.H_MERGE and handover.new_wm: - wm_to = handover.new_wm + wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id) if wm_to.state != wm_from.state or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect: raise ParseError("正常合并到的车间库存状态或物料异常") else: - wm_to, _ = WMaterial.objects.get_or_create( + wm_to, _ = WMaterial.locked_get_or_create( batch=batch, material=material, mgroup=recive_mgroup, @@ -806,11 +813,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, recive_mgroup = handover.recive_mgroup wm_state = WMaterial.WM_REPAIR if mtype == Handover.H_MERGE and handover.new_wm: - wm_to = handover.new_wm + wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id) if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect: raise ParseError("返修合并到的车间库存状态或物料异常") elif recive_mgroup: - wm_to, _ = WMaterial.objects.get_or_create( + wm_to, _ = WMaterial.locked_get_or_create( batch=batch, material=material, mgroup=recive_mgroup, @@ -828,28 +835,13 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, ) else: raise ParseError("返工交接必须指定接收工段") - elif handover.type == Handover.H_TEST: - raise ParseError("检验交接已废弃") - wm_to, _ = WMaterial.objects.get_or_create( - batch=batch, - material=material, - mgroup=recive_mgroup, - state=WMaterial.WM_TEST, - belong_dept=recive_dept, - defaults={ - "count_xtest": 0, - "batch_ofrom": wm_from.batch_ofrom, - "material_ofrom": wm_from.material_ofrom, - "create_by": user - }, - ) elif handover.type == Handover.H_SCRAP: if mtype == Handover.H_MERGE and handover.new_wm: - wm_to = handover.new_wm + wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id) if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect: raise ParseError("报废合并到的车间库存状态或物料异常") elif recive_mgroup: - wm_to, _ = WMaterial.objects.get_or_create( + wm_to, _ = WMaterial.locked_get_or_create( batch=batch, material=material, mgroup=recive_mgroup, @@ -868,11 +860,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, raise ParseError("不支持非工段报废") elif handover.type == Handover.H_CHANGE: if mtype == Handover.H_MERGE and handover.new_wm: - wm_to = handover.new_wm + wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id) if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed: raise ParseError("改版合并到的车间库存状态或物料异常") elif handover.recive_mgroup: - wm_to, _ = WMaterial.objects.get_or_create( + wm_to, _ = WMaterial.locked_get_or_create( batch=batch, material=handover.material_changed, state=handover.state_changed, @@ -895,9 +887,9 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, if wm_from and wm_from.state != WMaterial.WM_OK: raise ParseError("仅合格品支持退回") if mtype == Handover.H_MERGE and handover.new_wm: - wm_to = handover.new_wm + wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id) else: - wm_to, _ = WMaterial.objects.get_or_create( + wm_to, _ = WMaterial.locked_get_or_create( batch=batch, material=material, mgroup=recive_mgroup, @@ -915,9 +907,9 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, else: raise ParseError("不支持该交接类型") - wm_to.count = wm_to.count + xcount - wm_to.count_eweight = handover.count_eweight # 这行代码有隐患 - wm_to.save() + WMaterial.objects.filter(id=wm_to.id).update(count=F('count') + xcount) + if handover.count_eweight: + WMaterial.objects.filter(id=wm_to.id).update(count_eweight=handover.count_eweight) handover_or_b.wm_to = wm_to handover_or_b.save() if material.tracking == Material.MA_TRACKING_SINGLE: @@ -943,7 +935,6 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, ana_batch_thread(xbatchs=batches) -@lock_model_record_d_func(Handover) def handover_revert(handover:Handover, handler:User=None): if handover.submit_time is None: raise ParseError('该交接单未提交!') From 2ecaeadff7991e1bf5390ba24cb13fb61947542d Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 9 Jan 2026 16:59:53 +0800 Subject: [PATCH 06/30] =?UTF-8?q?feat:=20handover=5Fsubmit=20=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E4=BC=98=E5=8C=962?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/wpm/services.py b/apps/wpm/services.py index 1ce91ae0..a424ef09 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -924,7 +924,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, for item in handoverbws: wpr:Wpr = item.wpr Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb) - if wm_to.count != Wpr.objects.filter(wm=wm_to).count(): + db_count = WMaterial.objects.filter(id=wm_to.id).values_list("count", flat=True).get() + if db_count != Wpr.objects.filter(wm=wm_to).count(): raise ParseError("交接与明细数量不一致2,操作失败") handover.submit_user = user From f9eee5a52304c6877a1888c6a1fc9de94aaa9a1a Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 10:21:15 +0800 Subject: [PATCH 07/30] =?UTF-8?q?feat:=20handover=5Frevert=20=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/models.py | 26 +++++++++++++++++++++++++- apps/wpm/services.py | 23 ++++++----------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/wpm/models.py b/apps/wpm/models.py index 26e37796..a00b2557 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -14,7 +14,7 @@ from django.db.models import Count from django.db import transaction from django.db.models import Max import re -from django.db.models import Q +from django.db.models import Q, F import django.utils.timezone as timezone from apps.utils.sql import query_all_dict import logging @@ -163,6 +163,30 @@ class WMaterial(CommonBDModel): ), state__in=[WMaterial.WM_OK, WMaterial.WM_REPAIR] ) + + @classmethod + def increase(cls, wm_id: str, user:User, count, count_eweight=None): + updates = {} + if count: + updates['count'] = F('count') + count + if count_eweight: + updates['count_eweight'] = count_eweight + if not updates: + return 0 + updates["update_by"] = user + updates['update_time'] = timezone.now() + return cls.objects.filter(id=wm_id).update(**updates) + + @classmethod + def decrease(cls, wm_id: str, user:User, count): + if not count: + return 0 + updated = cls.objects.filter(id=wm_id, count__gte= count).update( + count=F('count') - count, update_by=user, update_time=timezone.now()) + if updated == 0: + batch = WMaterial.objects.get(id=wm_id).batch + raise ParseError(f'{batch}_库存不足,无法完成扣减') + return updated class Fmlog(CommonADModel): """TN: 父级生产日志 diff --git a/apps/wpm/services.py b/apps/wpm/services.py index a424ef09..59406afa 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -776,14 +776,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, batch = wm_from.batch batches.append(batch) - updated = ( - WMaterial.objects - .filter(id=wm_from.id, count__gte=xcount) - .update(count=F('count') - xcount) - ) - - if updated == 0: - raise ParseError(f'{wm_from.batch} 车间库存不足!') + WMaterial.decrease(wm_id=wm_from.id, user=user, count=xcount) if need_add: # 开始变动 @@ -907,9 +900,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, else: raise ParseError("不支持该交接类型") - WMaterial.objects.filter(id=wm_to.id).update(count=F('count') + xcount) - if handover.count_eweight: - WMaterial.objects.filter(id=wm_to.id).update(count_eweight=handover.count_eweight) + WMaterial.increase(wm_id=wm_to.id, user=user,count=xcount, count_eweight=handover.count_eweight if handover.count_eweight else None) handover_or_b.wm_to = wm_to handover_or_b.save() if material.tracking == Material.MA_TRACKING_SINGLE: @@ -937,6 +928,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, ana_batch_thread(xbatchs=batches) def handover_revert(handover:Handover, handler:User=None): + handover = Handover.objects.select_for_update().get(id=handover.id) if handover.submit_time is None: raise ParseError('该交接单未提交!') ticket:Ticket = handover.ticket @@ -968,16 +960,13 @@ def handover_revert(handover:Handover, handler:User=None): # 此时是自己交给自己,不需要做任何操作 pass else: - wm.count = wm.count + item.count - wm.save() - wm_to.count = wm_to.count - item.count - if wm_to.count < 0: - raise ParseError('库存不足无法撤回!') - wm_to.save() + WMaterial.increase(wm_id=wm.id, user=handler, count=item.count) + WMaterial.decrease(wm_id=wm_to.id, user=handler, count=item.count) if material.tracking == Material.MA_TRACKING_SINGLE: handoverbws = Handoverbw.objects.filter(handoverb=item) if handoverbws.count() != item.count: raise ParseError("交接与明细数量不一致,操作失败") + wm = WMaterial.objects.get(id=wm.id) for item in handoverbws: wpr:Wpr = item.wpr Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False) From def22f6b182fce564b493c2b37d98cbcee814f70 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 10:28:51 +0800 Subject: [PATCH 08/30] =?UTF-8?q?feat:=20handover=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E4=BB=85=E4=BA=A4=E6=8E=A5=E5=88=B0=E8=BD=A6?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/filters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index d649b0d3..ca2631ba 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -154,10 +154,16 @@ class MlogFilter(filters.FilterSet): class HandoverFilter(filters.FilterSet): cbatch = filters.CharFilter(label='批次号', method='filter_cbatch') mgroup = filters.CharFilter(label='MgroupId', method='filter_mgroup') + mgroupx = filters.CharFilter(label='MgroupId', method='filter_mgroupx') 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_mgroupx(self, queryset, name, value): + dept = Mgroup.objects.get(id=value).belong_dept + return (queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value)| + queryset.filter(send_dept=dept, send_mgroup__isnull=True)|queryset.filter(recive_dept=dept, recive_mgroup__isnull=True)) def filter_dept(self, queryset, name, value): return queryset.filter(send_dept__id=value)|queryset.filter(recive_dept__id=value) From 70563a6c021712561731de3322f33cb3528cf129 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 11:16:04 +0800 Subject: [PATCH 09/30] =?UTF-8?q?feat:=20mlog=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/services.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/wpm/services.py b/apps/wpm/services.py index 59406afa..b47c2529 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -150,11 +150,12 @@ def get_pcoal_heat(year_s: int, month_s: int, day_s: int): myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True) return 25000 -# @lock_model_record_d_func(Mlog) + def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): """ 生产日志提交后需要执行的操作 """ + mlog = Mlog.objects.select_for_update().get(id=mlog.id) if mlog.work_start_time and mlog.work_start_time > timezone.now(): raise ParseError('操作开始时间不能晚于当前时间') if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time: @@ -223,21 +224,21 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): # 需要判断领用数是否合理 # 优先使用工段库存 if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in: - wm_qs = WMaterial.objects.filter(id=mlog_or_b.wm_in.id) + wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id) else: wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK) if not wm_qs.exists(): wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK) - count_x = wm_qs.count() - if count_x == 1: - wm = wm_qs.first() - elif count_x == 0: - raise ParseError( - f'{str(mi_ma)}-{mi_batch}-批次库存不存在!') - else: - raise ParseError( - f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!') + count_x = wm_qs.count() + if count_x == 1: + wm = WMaterial.objects.select_for_update().get(id=wm_qs.first().id) + elif count_x == 0: + raise ParseError( + f'{str(mi_ma)}-{mi_batch}-批次库存不存在!') + else: + raise ParseError( + f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!') if mi_count > wm.count: raise ParseError( @@ -260,7 +261,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): if count <= 0: raise ParseError('存在非正数!') lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'defect': defect, 'state': WMaterial.WM_NOTOK} - wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={"belong_dept": belong_dept}) + wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept}) wm.count = wm.count + count if is_create: wm.create_by = user @@ -343,7 +344,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): else: lookup['belong_dept'] = belong_dept - wm, is_create2 = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) wm.count = wm.count + mo_count wm.count_eweight = mo_count_eweight wm.update_by = user @@ -405,12 +406,13 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): if wprIds: ana_wpr_thread(wprIds, mlog.mgroup) -# @lock_model_record_d_func(Mlog) + def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): """日志撤回 """ # if mlog.submit_time is None: # return + mlog = Mlog.objects.select_for_update().get(id=mlog.id) if now is None: now = timezone.now() @@ -506,6 +508,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): else: raise ParseError( f'{str(mo_ma)}-{mo_batch}-存在多个相同批次!') + + wm = WMaterial.objects.select_for_update().get(id=wm.id) wm.count = wm.count - mo_count if wm.count < 0: raise ParseError(f'{wm.batch} 车间库存不足, 产物无法回退') @@ -547,7 +551,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): if mi_count <= 0: raise ParseError('存在非正数!') if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in: - wm = WMaterial.objects.get(id=mlog_or_b.wm_in.id) + wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id) else: # 针对光子的情况,实际上必须需要wm_in lookup = {'batch': mi_batch, 'material': mi_ma, 'mgroup': None, 'state': WMaterial.WM_OK} @@ -557,7 +561,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): else: lookup['belong_dept'] = belong_dept - wm, _ = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) wm.count = wm.count + mi_count wm.update_by = user wm.save() @@ -579,7 +583,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): lookup['mgroup'] = mgroup else: lookup['belong_dept'] = belong_dept - wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) wm.count = wm.count - count if wm.count < 0: raise ParseError('加工前不良数量大于库存量') From b39b0e79233d54b7875e05428c4b7eeb9d0c1019 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 13:27:29 +0800 Subject: [PATCH 10/30] =?UTF-8?q?fix:=20mlog=E5=B9=B6=E5=8F=91=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/services.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wpm/services.py b/apps/wpm/services.py index b47c2529..5f3bfd55 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -344,7 +344,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): else: lookup['belong_dept'] = belong_dept - wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept}) wm.count = wm.count + mo_count wm.count_eweight = mo_count_eweight wm.update_by = user @@ -561,7 +561,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): else: lookup['belong_dept'] = belong_dept - wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept}) wm.count = wm.count + mi_count wm.update_by = user wm.save() @@ -583,7 +583,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): lookup['mgroup'] = mgroup else: lookup['belong_dept'] = belong_dept - wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) + wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept}) wm.count = wm.count - count if wm.count < 0: raise ParseError('加工前不良数量大于库存量') From cf6633592a8848da57e3d8da930a6c63974edf93 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 13:44:08 +0800 Subject: [PATCH 11/30] =?UTF-8?q?feat:=20=E4=BA=A4=E6=8E=A5=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=AD=90=E9=A1=B9=E9=9C=80=E4=BF=9D=E8=AF=81=E5=B7=A5?= =?UTF-8?q?=E6=AE=B5/=E8=BD=A6=E9=97=B4=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/models.py | 4 ++++ apps/wpm/serializers.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/wpm/models.py b/apps/wpm/models.py index a00b2557..cbaf639d 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -654,6 +654,10 @@ class Handover(CommonADModel): @property def handoverb(self): return Handoverb.objects.filter(handover=self) + + @property + def belong_dept_or_mgroup_id(self): + return self.mgroup.id if self.mgroup else self.belong_dept.id class Handoverb(BaseModel): """TN: 子级交接记录 diff --git a/apps/wpm/serializers.py b/apps/wpm/serializers.py index f306bccc..a724afd7 100644 --- a/apps/wpm/serializers.py +++ b/apps/wpm/serializers.py @@ -1261,12 +1261,18 @@ class HandoverSerializer(CustomModelSerializer): next_mat = new_wm.material next_state = new_wm.state next_defect = new_wm.defect + deptOrmgroupId = None for ind, item in enumerate(attrs['handoverb']): if item["count"] > 0: pass else: raise ParseError(f'第{ind+1}行-交接数量必须大于0') - wm = item["wm"] + wm: WMaterial = item["wm"] + current_mdept_id = wm.belong_dept_or_mgroup_id + if deptOrmgroupId is None: + deptOrmgroupId = current_mdept_id + elif deptOrmgroupId != current_mdept_id: + raise ParseError(f'第{ind+1}行-交接物料所属工段/车间不一致') if mtype == Handover.H_MERGE: if next_mat is None: next_mat = wm.material From 143d9cb719b9dbddd421c454c342d70c9d80d616 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 15:30:55 +0800 Subject: [PATCH 12/30] =?UTF-8?q?fix:=20base=20locked=5Fget=5For=5Fcreate?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/utils/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/utils/models.py b/apps/utils/models.py index bdc4a486..6fc66c5b 100755 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -173,7 +173,8 @@ class BaseModel(models.Model): if cnt == 1: return qs.get(), False - obj = cls.objects.create(**kwargs, **defaults) + params = {**kwargs, **defaults} + obj = cls.objects.create(**params) return obj, True def handle_parent(self): From d5ea72a021231f88aa50afb8a2c2211605cf58f3 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 12 Jan 2026 15:44:48 +0800 Subject: [PATCH 13/30] =?UTF-8?q?feat:=20=E4=BA=A4=E6=8E=A5=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=AD=90=E9=A1=B9=E9=9C=80=E4=BF=9D=E8=AF=81=E5=B7=A5?= =?UTF-8?q?=E6=AE=B5/=E8=BD=A6=E9=97=B4=E4=B8=80=E8=87=B42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/wpm/models.py b/apps/wpm/models.py index cbaf639d..52f21f3c 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -127,6 +127,10 @@ class WMaterial(CommonBDModel): material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom') number_from = models.TextField("来源于个号", null=True, blank=True) + @property + def belong_dept_or_mgroup_id(self): + return self.mgroup.id if self.mgroup else self.belong_dept.id + @property def count_working(self): return Mlogb.objects.filter(wm_in=self, mlog__submit_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0 @@ -654,10 +658,6 @@ class Handover(CommonADModel): @property def handoverb(self): return Handoverb.objects.filter(handover=self) - - @property - def belong_dept_or_mgroup_id(self): - return self.mgroup.id if self.mgroup else self.belong_dept.id class Handoverb(BaseModel): """TN: 子级交接记录 From 43f5f11ca8efd30a6a1e002931d778d3c27c5f87 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 13 Jan 2026 09:05:19 +0800 Subject: [PATCH 14/30] =?UTF-8?q?feat:=20=E6=9C=AA=E6=9C=89ftest=E7=9A=84?= =?UTF-8?q?=E4=B9=9F=E8=A7=A6=E5=8F=91=E5=8D=95=E4=B8=AA=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wpm/services.py b/apps/wpm/services.py index 5f3bfd55..5a7f9615 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -402,7 +402,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup) # 触发单个统计 - wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True)) + wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True)) if wprIds: ana_wpr_thread(wprIds, mlog.mgroup) @@ -623,7 +623,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): ana_batch_thread(xbatches, mgroup=mlog.mgroup) # 触发单个统计 - wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True)) + wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True)) if wprIds: ana_wpr_thread(wprIds, mlog.mgroup) From feb8bd6770dbc73a742b2591266fd29dd17eac8f Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 13 Jan 2026 10:29:41 +0800 Subject: [PATCH 15/30] =?UTF-8?q?feat:=20wpr=5Fbxerp=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/scripts/wpr_bxerp.py | 85 ++++++++++++++++++++++++----------- apps/wpmw/views.py | 10 +++++ 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/apps/wpm/scripts/wpr_bxerp.py b/apps/wpm/scripts/wpr_bxerp.py index 8ef74e42..7efb4cba 100644 --- a/apps/wpm/scripts/wpr_bxerp.py +++ b/apps/wpm/scripts/wpr_bxerp.py @@ -1,36 +1,67 @@ from apps.wpmw.models import Wpr -from apps.wpm.models import Mlogbw -from apps.qm.models import Ftest, FtestDefect, FtestItem +from apps.wpm.models import Mlogbw, Mlog, MlogUser +from apps.qm.models import Ftest, FtestDefect, FtestItem, TestItem from rest_framework.exceptions import ParseError from apps.mtm.models import Mgroup -def main(wprId, mgroup:Mgroup): +def main(wprId, mgroup:Mgroup=None): wpr = Wpr.objects.get(id=wprId) + if mgroup is None: + mgroup_ids = Mlogbw.objects.filter( + wpr=wpr, + mlogb__mlog__submit_time__isnull=False, + mlogb__mlog__is_fix=False + ).values_list( + 'mlogb__mlog__mgroup', + flat=True + ).distinct() + mgroups = Mgroup.objects.filter(id__in=mgroup_ids) + else: + mgroups = [mgroup] data = {} - mgroup_name = mgroup.name - mlogbw = Mlogbw.objects.filter(wpr=wpr, mlogb__mlog__mgroup=mgroup, mlogb__mlog__submit_time__isnull=False).order_by("-update_time").first() - if mlogbw: - data[f"{mgroup_name}_批次号"] = mlogbw.mlogb.batch - data[f"{mgroup_name}_日期"] = mlogbw.mlogb.mlog.handle_date.strftime("%Y-%m-%d") - ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr, + for mgroup in mgroups: + mgroup_name = mgroup.name + mlogbw = Mlogbw.objects.filter(wpr=wpr, + mlogb__mlog__mgroup=mgroup, + mlogb__mlog__submit_time__isnull=False, mlogb__mlog__is_fix=False).order_by("-update_time").first() + if mlogbw: + mlog:Mlog = mlogbw.mlogb.mlog + data[f"{mgroup_name}_批次号"] = mlogbw.mlogb.batch + data[f"{mgroup_name}_设备编号"] = mlog.equipment.number if mlog.equipment else None + data[f"{mgroup_name}_操作人"] = mlog.handle_user.name if mlog.handle_user else None + data[f"{mgroup_name}_日期"] = mlog.handle_date.strftime("%Y-%m-%d") + # 日志操作数据 + if mlog.oinfo_json: + oinfo_keys = list(mlog.oinfo_json.keys()) + oinfo_keys_qs = TestItem.objects.filter(id__in=oinfo_keys) + for item in oinfo_keys_qs: + data[f"{mgroup_name}_操作项_{item.name}"] = mlog.oinfo_json[item.id] + # 子工序操作人和日期 + mlogusers = MlogUser.objects.filter(mlog=mlog) + if mlogusers.exists(): + datab = mlogusers.values("handle_user__name", "process__name", "handle_date") + for ind, item in enumerate(datab): + data[f"{mgroup_name}_{item['process__name']}_操作人"] = item["handle_user__name"] + data[f"{mgroup_name}_{item['process__name']}_日期"] = item["handle_date"].strftime("%Y-%m-%d") + # 检测数据 + ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr, + ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup, + ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False, + ftest__mlogbw_ftest__mlogb__mlog__is_fix=False) + for ftestitem in ftestitems: + data[f"{mgroup_name}_检测项_{ftestitem.testitem.name}"] = ftestitem.test_val_json + + ftestdefects = FtestDefect.objects.filter(ftest__mlogbw_ftest__wpr=wpr, ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup, ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False, ftest__mlogbw_ftest__mlogb__mlog__is_fix=False) - for ftestitem in ftestitems: - data[f"{mgroup_name}_检测项_{ftestitem.testitem.name}"] = ftestitem.test_val_json - - ftestdefects = FtestDefect.objects.filter(ftest__mlogbw_ftest__wpr=wpr, - ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup, - ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False, - ftest__mlogbw_ftest__mlogb__mlog__is_fix=False) - for ftestdefect in ftestdefects: - data[f"{mgroup_name}_缺陷项_{ftestdefect.defect.name}"] = 1 if ftestdefect.has is True else 0 - - old_data:dict = wpr.data - if old_data: - for item in list(old_data.keys()): - if f'{mgroup_name}_' in item: - del old_data[item] - old_data.update(data) - wpr.data = old_data - wpr.save(update_fields=["data"]) \ No newline at end of file + for ftestdefect in ftestdefects: + data[f"{mgroup_name}_缺陷项_{ftestdefect.defect.name}"] = 1 if ftestdefect.has is True else 0 + old_data:dict = wpr.data + if old_data: + for item in list(old_data.keys()): + if f'{mgroup_name}_' in item: + del old_data[item] + old_data.update(data) + wpr.data = old_data + wpr.save(update_fields=["data"]) \ No newline at end of file diff --git a/apps/wpmw/views.py b/apps/wpmw/views.py index 3663f180..dcad152a 100644 --- a/apps/wpmw/views.py +++ b/apps/wpmw/views.py @@ -34,6 +34,16 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu "number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(wpmw_wpr.number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []), } + def add_info_for_list(self, data): + parent_ids = [item["wpr_from"] for item in data if item["wpr_from"]] + if parent_ids: + parent_data = Wpr.objects.filter(id__in=parent_ids).values("id", "number", "data") + parent_map = {item["id"]: item for item in parent_data} + for item in data: + if item["wpr_from"]: + item["parent"] = parent_map[item["wpr_from"]] + return data + def filter_queryset(self, queryset): qs = super().filter_queryset(queryset) if "mb__isnull" in self.request.query_params or "wm__isnull" in self.request.query_params: From fce66da1d9dba46d6fc9ec15e11a6d8486ebd021 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 13 Jan 2026 14:15:16 +0800 Subject: [PATCH 16/30] =?UTF-8?q?feat:=20wpr=20=E6=B7=BB=E5=8A=A0=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E6=9D=A1=E4=BB=B6wpr=5Ffrom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpmw/filters.py | 1 + apps/wpmw/views.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/wpmw/filters.py b/apps/wpmw/filters.py index 36d89328..939e6b88 100644 --- a/apps/wpmw/filters.py +++ b/apps/wpmw/filters.py @@ -18,6 +18,7 @@ class WprFilter(filters.FilterSet): "wm": ["exact", "isnull"], "material__process": ["exact"], "material__name": ["exact", "contains"], + "wpr_from": ["exact", "isnull"], "state": ["exact"], "defects": ["exact"], "number": ["exact"] diff --git a/apps/wpmw/views.py b/apps/wpmw/views.py index dcad152a..0b7eb1be 100644 --- a/apps/wpmw/views.py +++ b/apps/wpmw/views.py @@ -41,7 +41,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu parent_map = {item["id"]: item for item in parent_data} for item in data: if item["wpr_from"]: - item["parent"] = parent_map[item["wpr_from"]] + item["wpr_from_"] = parent_map[item["wpr_from"]] return data def filter_queryset(self, queryset): From 3e173f7a721b73e07d966b4c02351f1065643d39 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 13 Jan 2026 14:57:13 +0800 Subject: [PATCH 17/30] =?UTF-8?q?feat:=20base=20cquery=E6=94=AF=E6=8C=81ad?= =?UTF-8?q?d=5Finfo=5Ffor=5Flist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/utils/mixins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/utils/mixins.py b/apps/utils/mixins.py index c3df566c..a8f64c0d 100755 --- a/apps/utils/mixins.py +++ b/apps/utils/mixins.py @@ -302,7 +302,10 @@ class ComplexQueryMixin: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(new_qs, many=True) - return Response(serializer.data) + rdata = serializer.data + if hasattr(self, 'add_info_for_list'): + rdata = self.add_info_for_list(rdata) + return Response(rdata) class MyLoggingMixin(object): """Mixin to log requests""" From 1ffbe0cc4409971b17d1f5ec3f6310773875f342 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 13 Jan 2026 15:02:52 +0800 Subject: [PATCH 18/30] =?UTF-8?q?feat:=20wpr=20=E8=BF=94=E5=9B=9Ewpr=5Ffro?= =?UTF-8?q?m=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpmw/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wpmw/views.py b/apps/wpmw/views.py index 0b7eb1be..93c927f2 100644 --- a/apps/wpmw/views.py +++ b/apps/wpmw/views.py @@ -1,6 +1,6 @@ from rest_framework.decorators import action from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet -from apps.utils.mixins import CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin +from apps.utils.mixins import CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin from apps.wpmw.models import Wpr, WprDefect from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer @@ -13,7 +13,7 @@ from apps.utils.sql import query_one_dict from django.db.models.expressions import RawSQL -class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet): +class WprViewSet(CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet): """动态产品 动态产品 @@ -35,7 +35,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu } def add_info_for_list(self, data): - parent_ids = [item["wpr_from"] for item in data if item["wpr_from"]] + parent_ids = [item["wpr_from"] for item in data if item.get("wpr_from", False)] if parent_ids: parent_data = Wpr.objects.filter(id__in=parent_ids).values("id", "number", "data") parent_map = {item["id"]: item for item in parent_data} From 47b1887c4bbcb7adb89e6b37325a5187e989ebfd Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 15 Jan 2026 09:13:09 +0800 Subject: [PATCH 19/30] =?UTF-8?q?feat:=20base=20=E8=B0=83=E6=95=B4asgi?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E4=BB=A5=E4=BF=9D=E8=AF=81=E6=AD=A3=E5=B8=B8?= =?UTF-8?q?=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/asgi.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/asgi.py b/server/asgi.py index 5632900a..298f8c4c 100755 --- a/server/asgi.py +++ b/server/asgi.py @@ -8,13 +8,16 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ """ import os +import django from channels.routing import ProtocolTypeRouter, URLRouter + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') +django.setup() + from django.core.asgi import get_asgi_application from apps.utils.middlewares import TokenAuthMiddleware import apps.ws.routing -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') - application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": TokenAuthMiddleware( From 146e842642efd25025e3aafc1585750d65ca6033 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 15 Jan 2026 16:47:15 +0800 Subject: [PATCH 20/30] =?UTF-8?q?feat:=20with=5Fsource=5Fnear=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E4=BD=93=E7=8E=B0=E5=9C=A8swagger=E9=87=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/filters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index ca2631ba..4c400285 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -229,7 +229,11 @@ class MlogbFilter(filters.FilterSet): class BatchStFilter(filters.FilterSet): batch__startswith__in = filters.CharFilter(method='filter_batch') data__has_key = filters.CharFilter(method='filter_data') + with_source_near = filters.CharFilter(label='来源', method='filter_source_near') + def filter_source_near(self, queryset, name, value): + return queryset + def filter_data(self, queryset, name, value): return queryset.filter(data__has_key=value) From e99b2ecbbc8ff509e9f04c193de3d0fe94e59fb0 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 16 Jan 2026 14:00:11 +0800 Subject: [PATCH 21/30] =?UTF-8?q?fix:=20base=20complexquerymixin=E6=94=AF?= =?UTF-8?q?=E6=8C=81add=5Finfo=5Ffor=5Flist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/utils/mixins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/utils/mixins.py b/apps/utils/mixins.py index a8f64c0d..5e0bd9ea 100755 --- a/apps/utils/mixins.py +++ b/apps/utils/mixins.py @@ -300,7 +300,10 @@ class ComplexQueryMixin: page = self.paginate_queryset(new_qs) if page is not None: serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) + rdata = serializer.data + if hasattr(self, 'add_info_for_list'): + rdata = self.add_info_for_list(rdata) + return self.get_paginated_response(rdata) serializer = self.get_serializer(new_qs, many=True) rdata = serializer.data if hasattr(self, 'add_info_for_list'): From 70e49eb27ea77da6527ab29e9ee5a385a47dcc2f Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 16 Jan 2026 14:00:52 +0800 Subject: [PATCH 22/30] =?UTF-8?q?fix:=20batchst=E6=94=AF=E6=8C=81=E8=BF=94?= =?UTF-8?q?=E5=9B=9Esource=5Fnear=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/wpm/views.py b/apps/wpm/views.py index 29e2074f..4e19926d 100644 --- a/apps/wpm/views.py +++ b/apps/wpm/views.py @@ -1078,7 +1078,8 @@ class BatchStViewSet(CustomListModelMixin, ComplexQueryMixin, CustomGenericViewS filterset_class = BatchStFilter def add_info_for_list(self, data): - if self.request.query_params.get("with_source_near", None) == "yes": + if (self.request.query_params.get("with_source_near", None) == "yes" or + self.request.data.get("with_source_near", None) == "yes"): batchstIds = [ins["id"] for ins in data] batchlog_qs = BatchLog.objects.filter(target__id__in=batchstIds).values("id", "source", "target") source_data = BatchStSerializer(instance=BatchSt.objects.filter(id__in=[ins["source"] for ins in batchlog_qs]), many=True).data From 80f832aa854a4f1394c4c14c7f9795eda8765bb7 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 16 Jan 2026 14:01:13 +0800 Subject: [PATCH 23/30] =?UTF-8?q?feat:=20=E5=85=89=E5=AD=90=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=B7=A5=E6=AE=B5=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/scripts/batch_gzerp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wpm/scripts/batch_gzerp.py b/apps/wpm/scripts/batch_gzerp.py index 1d87c585..75f08d40 100644 --- a/apps/wpm/scripts/batch_gzerp.py +++ b/apps/wpm/scripts/batch_gzerp.py @@ -292,7 +292,7 @@ def main(batch: str, mgroup_obj=None): data["六车间交接领料_接料人"] = ";".join([item.name for item in data["六车间交接领料_接料人"]]) # 六车间工段生产数据 - mgroup_list = ["平头", "粘铁头", "粗中细磨", "平磨", "掏管", "抛光", "开槽", "倒角"] + mgroup_list = ["平头", "粘铁头", "粗中细磨", "平磨", "掏管", "抛光", "开槽", "倒角", "加工前检验", "中检"] for mgroup_name in mgroup_list: if mgroup_name == '粗中细磨': mgroups = Mgroup.objects.filter(name__in=['粗磨', '粗中磨', '粗中细磨']) From 2759114ede81736ac99b6fb87b5de124d1dbd53b Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 16 Jan 2026 14:42:42 +0800 Subject: [PATCH 24/30] =?UTF-8?q?feat:=20base=20=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E5=90=8E=E5=90=8C=E6=AD=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...reate_by_alter_dept_third_info_and_more.py | 120 ++++++++++++++++++ apps/system/models.py | 8 +- 2 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 apps/system/migrations/0007_alter_dept_create_by_alter_dept_third_info_and_more.py diff --git a/apps/system/migrations/0007_alter_dept_create_by_alter_dept_third_info_and_more.py b/apps/system/migrations/0007_alter_dept_create_by_alter_dept_third_info_and_more.py new file mode 100644 index 00000000..195e4ef8 --- /dev/null +++ b/apps/system/migrations/0007_alter_dept_create_by_alter_dept_third_info_and_more.py @@ -0,0 +1,120 @@ +# Generated by Django 4.2.27 on 2026-01-16 06:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0006_auto_20241213_1249'), + ] + + operations = [ + migrations.AlterField( + model_name='dept', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='dept', + name='third_info', + field=models.JSONField(blank=True, default=dict, verbose_name='三方系统信息'), + ), + migrations.AlterField( + model_name='dept', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='dictionary', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='dictionary', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='dicttype', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='dicttype', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='file', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='file', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='myschedule', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='myschedule', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='post', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='post', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='postrole', + name='post', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pr_post', to='system.post', verbose_name='关联岗位'), + ), + migrations.AlterField( + model_name='postrole', + name='role', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pr_role', to='system.role', verbose_name='关联角色'), + ), + migrations.AlterField( + model_name='role', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='role', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='user', + name='belong_dept', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'), + ), + migrations.AlterField( + model_name='user', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='user', + name='roles', + field=models.ManyToManyField(blank=True, to='system.role', verbose_name='关联角色'), + ), + migrations.AlterField( + model_name='user', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + ] diff --git a/apps/system/models.py b/apps/system/models.py index 8dcca519..43a7bd21 100755 --- a/apps/system/models.py +++ b/apps/system/models.py @@ -54,7 +54,7 @@ class Dept(ParentModel, CommonAModel): name = models.CharField('名称', max_length=60) type = models.CharField('类型', max_length=20, default='dept') sort = models.PositiveSmallIntegerField('排序标记', default=1) - third_info = models.JSONField('三方系统信息', default=dict) + third_info = models.JSONField('三方系统信息', default=dict, blank=True) class Meta: verbose_name = '部门' @@ -107,9 +107,9 @@ class PostRole(BaseModel): data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices, default=DataFilter.THISLEVEL_AND_BELOW) post = models.ForeignKey(Post, verbose_name='关联岗位', - on_delete=models.CASCADE) + on_delete=models.CASCADE, related_name="pr_post") role = models.ForeignKey(Role, verbose_name='关联角色', - on_delete=models.CASCADE) + on_delete=models.CASCADE, related_name='pr_role') class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager): @@ -132,7 +132,7 @@ class User(AbstractUser, CommonBModel): posts = models.ManyToManyField( Post, through='system.userpost', related_name='user_posts') depts = models.ManyToManyField(Dept, through='system.userpost') - roles = models.ManyToManyField(Role, verbose_name='关联角色') + roles = models.ManyToManyField(Role, verbose_name='关联角色', blank=True) # 关联账号 secret = models.CharField('密钥', max_length=100, null=True, blank=True) From 0d80e182cd5241ad76ab3ac94df9419f02c33746 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 16 Jan 2026 14:48:08 +0800 Subject: [PATCH 25/30] =?UTF-8?q?feat:=20base=20user=E5=A2=9E=E5=8A=A0has?= =?UTF-8?q?=5Fperm=E7=AD=9B=E9=80=89=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/system/filters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/system/filters.py b/apps/system/filters.py index 481b6398..c20c5143 100755 --- a/apps/system/filters.py +++ b/apps/system/filters.py @@ -7,6 +7,7 @@ from rest_framework.exceptions import ParseError class UserFilterSet(filters.FilterSet): ubelong_dept__name = filters.CharFilter(label='归属于该部门及以下(按名称)', method='filter_ubelong_dept__name') ubelong_dept = filters.CharFilter(label='归属于该部门及以下', method='filter_ubelong_dept') + has_perm = filters.CharFilter(label='拥有指定权限标识', method='filter_has_perm') class Meta: model = User @@ -37,6 +38,9 @@ class UserFilterSet(filters.FilterSet): except Exception as e: raise ParseError(f"部门ID错误: {value} {str(e)}") return queryset.filter(belong_dept__in=depts) + + def filter_has_perm(self, queryset, name, value): + return queryset.filter(up_user__post__pr_post__role__perms__codes__contains=value) class DeptFilterSet(filters.FilterSet): From dc26c7cc466fdb93980da05bc9af979f4999386c Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 16 Jan 2026 15:29:58 +0800 Subject: [PATCH 26/30] =?UTF-8?q?feat:=20handoverb=E6=B7=BB=E5=8A=A0oinfo?= =?UTF-8?q?=5Fjson=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...fo_json_alter_attlog_create_by_and_more.py | 117 ++++++++++++++++++ apps/wpm/models.py | 1 + 2 files changed, 118 insertions(+) create mode 100644 apps/wpm/migrations/0127_handoverb_oinfo_json_alter_attlog_create_by_and_more.py diff --git a/apps/wpm/migrations/0127_handoverb_oinfo_json_alter_attlog_create_by_and_more.py b/apps/wpm/migrations/0127_handoverb_oinfo_json_alter_attlog_create_by_and_more.py new file mode 100644 index 00000000..b9343156 --- /dev/null +++ b/apps/wpm/migrations/0127_handoverb_oinfo_json_alter_attlog_create_by_and_more.py @@ -0,0 +1,117 @@ +# Generated by Django 4.2.27 on 2026-01-16 07:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'), + ('wpm', '0126_auto_20251208_1337'), + ] + + operations = [ + migrations.AddField( + model_name='handoverb', + name='oinfo_json', + field=models.JSONField(blank=True, default=dict, verbose_name='其他信息'), + ), + migrations.AlterField( + model_name='attlog', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='attlog', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='fmlog', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='fmlog', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='handover', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='handover', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='mlog', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='mlog', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='otherlog', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='otherlog', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='sflog', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='sflog', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='sflogexp', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='sflogexp', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='stlog', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='stlog', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AlterField( + model_name='wmaterial', + name='belong_dept', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'), + ), + migrations.AlterField( + model_name='wmaterial', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AlterField( + model_name='wmaterial', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + ] diff --git a/apps/wpm/models.py b/apps/wpm/models.py index 52f21f3c..48977fde 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -669,6 +669,7 @@ class Handoverb(BaseModel): wm_to = models.ForeignKey(WMaterial, verbose_name='所到车间库存', on_delete=models.SET_NULL, null=True, blank=True, related_name='handoverb_wm_to') count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1) + oinfo_json = models.JSONField('其他信息', default=dict, blank=True) @property def handoverbw(self): From 4bbae8b7df3c129c367deb4e40dac6f3dd587cea Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 21 Jan 2026 11:09:53 +0800 Subject: [PATCH 27/30] =?UTF-8?q?feat:=20wmaterial=E6=A0=B9=E6=8D=AEcurren?= =?UTF-8?q?t=5Fmerged=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/filters.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index 4c400285..801282ce 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -1,8 +1,8 @@ from django_filters import rest_framework as filters from apps.wpm.models import (SfLog, StLog, WMaterial, Mlog, Mlogbw, - Handover, Mgroup, Mlogb, Mtask, BatchSt) + Handover, Mgroup, Mlogb, Mtask, BatchSt, Handoverb) from apps.mtm.models import Route, Material -from django.db.models import Q +from django.db.models import Q, Exists, OuterRef from rest_framework.exceptions import ParseError from datetime import datetime @@ -43,6 +43,7 @@ class WMaterialFilter(filters.FilterSet): material__process__exclude = filters.CharFilter(field_name="material__process", lookup_expr="exact", exclude=True) mlog_date_start = filters.DateFilter(label="产出开始", method="filter_mlog_date_start") mlog_date_end = filters.DateFilter(label="产出结束", method="filter_mlog_date_end") + current_merged = filters.BooleanFilter(label="是否在本工段合批", method="filter_current_merged") def filter_mlog_date_start(self, queryset, name, value): mgroupId = self.data.get("mgroup", None) @@ -101,6 +102,23 @@ class WMaterialFilter(filters.FilterSet): raise ParseError('生产路线不存在!') return queryset.filter(material=route.material_in)|queryset.filter(material__in=route.materials.all()) + def filter_current_merged(self, queryset, name, value): + mgroupxId = self.data.get("mgroupx", None) + if mgroupxId: + pass + else: + raise ParseError("请提供工段查询条件") + sub_qs = Handoverb.objects.filter( + wm=OuterRef("pk"), + handover__mtype=Handover.H_MERGE, + handover__recive_mgroup__id=mgroupxId, + ) + if value is True: + return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=True) + elif value is False: + return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=False) + return queryset + class Meta: model = WMaterial fields = { From 63002f27c8ac02105d2215df42c7315469c94a2f Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 22 Jan 2026 16:00:59 +0800 Subject: [PATCH 28/30] =?UTF-8?q?feat:=20wmaterial=E6=A0=B9=E6=8D=AEcurren?= =?UTF-8?q?t=5Fmerged=E6=9F=A5=E8=AF=A22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index 801282ce..fa17132b 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -112,6 +112,7 @@ class WMaterialFilter(filters.FilterSet): wm=OuterRef("pk"), handover__mtype=Handover.H_MERGE, handover__recive_mgroup__id=mgroupxId, + handover__submit_time__isnull=False ) if value is True: return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=True) From a534bde086d5a918691c4b6d89dda8c3d5187df8 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 22 Jan 2026 16:12:35 +0800 Subject: [PATCH 29/30] =?UTF-8?q?feat:=20wmaterial=E6=A0=B9=E6=8D=AEcurren?= =?UTF-8?q?t=5Fmerged=E6=9F=A5=E8=AF=A23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wpm/filters.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index fa17132b..0b636512 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -43,7 +43,7 @@ class WMaterialFilter(filters.FilterSet): material__process__exclude = filters.CharFilter(field_name="material__process", lookup_expr="exact", exclude=True) mlog_date_start = filters.DateFilter(label="产出开始", method="filter_mlog_date_start") mlog_date_end = filters.DateFilter(label="产出结束", method="filter_mlog_date_end") - current_merged = filters.BooleanFilter(label="是否在本工段合批", method="filter_current_merged") + current_merged = filters.BooleanFilter(label="是否本工段新合成的批", method="filter_current_merged") def filter_mlog_date_start(self, queryset, name, value): mgroupId = self.data.get("mgroup", None) @@ -103,15 +103,9 @@ class WMaterialFilter(filters.FilterSet): return queryset.filter(material=route.material_in)|queryset.filter(material__in=route.materials.all()) def filter_current_merged(self, queryset, name, value): - mgroupxId = self.data.get("mgroupx", None) - if mgroupxId: - pass - else: - raise ParseError("请提供工段查询条件") sub_qs = Handoverb.objects.filter( - wm=OuterRef("pk"), + wm_to=OuterRef("pk"), handover__mtype=Handover.H_MERGE, - handover__recive_mgroup__id=mgroupxId, handover__submit_time__isnull=False ) if value is True: From d29fcce9356f7f75a2fd98f7bc81872d249b5cbe Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 23 Jan 2026 16:18:17 +0800 Subject: [PATCH 30/30] =?UTF-8?q?fix:=20base=20user=5Fexist=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/system/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/system/serializers.py b/apps/system/serializers.py index 20cc9597..d3240795 100755 --- a/apps/system/serializers.py +++ b/apps/system/serializers.py @@ -322,7 +322,7 @@ def phone_exist(phone): def user_exist(username): - if User.objects.filter(username=username).exists(): + if User.objects.get_queryset(all=True).filter(username=username).exists(): raise serializers.ValidationError(**USERNAME_EXIST) return username