Compare commits

...

55 Commits

Author SHA1 Message Date
caoqianming d29fcce935 fix: base user_exist完善 2026-01-23 16:18:17 +08:00
caoqianming a534bde086 feat: wmaterial根据current_merged查询3 2026-01-22 16:12:35 +08:00
caoqianming 63002f27c8 feat: wmaterial根据current_merged查询2 2026-01-22 16:00:59 +08:00
caoqianming 4bbae8b7df feat: wmaterial根据current_merged查询 2026-01-21 11:09:53 +08:00
caoqianming dc26c7cc46 feat: handoverb添加oinfo_json字段 2026-01-16 15:29:58 +08:00
caoqianming 0d80e182cd feat: base user增加has_perm筛选条件 2026-01-16 14:48:08 +08:00
caoqianming 2759114ede feat: base 升级后同步数据库 2026-01-16 14:42:42 +08:00
caoqianming 80f832aa85 feat: 光子添加工段数据统计 2026-01-16 14:01:13 +08:00
caoqianming 70e49eb27e fix: batchst支持返回source_near修复 2026-01-16 14:00:52 +08:00
caoqianming e99b2ecbbc fix: base complexquerymixin支持add_info_for_list 2026-01-16 14:00:11 +08:00
caoqianming 146e842642 feat: with_source_near筛选体现在swagger里 2026-01-15 16:47:15 +08:00
caoqianming 47b1887c4b feat: base 调整asgi导入以保证正常启动 2026-01-15 09:13:09 +08:00
caoqianming 1ffbe0cc44 feat: wpr 返回wpr_from_ 2026-01-13 15:02:52 +08:00
caoqianming 3e173f7a72 feat: base cquery支持add_info_for_list 2026-01-13 14:57:13 +08:00
caoqianming fce66da1d9 feat: wpr 添加筛选条件wpr_from 2026-01-13 14:15:16 +08:00
caoqianming feb8bd6770 feat: wpr_bxerp优化 2026-01-13 10:29:41 +08:00
caoqianming 43f5f11ca8 feat: 未有ftest的也触发单个统计 2026-01-13 09:05:19 +08:00
caoqianming d5ea72a021 feat: 交接记录子项需保证工段/车间一致2 2026-01-12 15:44:48 +08:00
caoqianming 143d9cb719 fix: base locked_get_or_create优化 2026-01-12 15:30:55 +08:00
caoqianming cf6633592a feat: 交接记录子项需保证工段/车间一致 2026-01-12 13:44:08 +08:00
caoqianming b39b0e7923 fix: mlog并发优化的bug 2026-01-12 13:27:29 +08:00
caoqianming 70563a6c02 feat: mlog 并发优化 2026-01-12 11:16:04 +08:00
caoqianming def22f6b18 feat: handover可以查看仅交接到车间的记录 2026-01-12 10:28:51 +08:00
caoqianming f9eee5a523 feat: handover_revert 并发优化 2026-01-12 10:21:15 +08:00
caoqianming 2ecaeadff7 feat: handover_submit 并发优化2 2026-01-09 16:59:53 +08:00
caoqianming 6eee0e1e53 feat: handover_submit 并发优化 2026-01-09 16:54:24 +08:00
caoqianming 3417515e72 feat: base 添加locked_get_or_create 2026-01-09 16:53:57 +08:00
caoqianming 43abcbaa48 feat: 查询-n批次从正则改用like以优化性能 2026-01-09 15:55:56 +08:00
caoqianming e2a92b6faa feat: 固定依赖包 2026-01-08 10:40:00 +08:00
caoqianming 02e3265133 feat: 升级依赖包 2026-01-08 09:59:39 +08:00
caoqianming 65cdeb0e7c Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2026-01-07 16:26:42 +08:00
caoqianming 238d1dd074 release: 3.0.2026010716 2026-01-07 16:26:42 +08:00
TianyangZhang f7a78431c5 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2026-01-07 16:03:19 +08:00
TianyangZhang 5b60318bfb feat: gx-人员请假申请与审批 2026-01-07 16:03:18 +08:00
caoqianming 0127e2a149 feat: get_shift需要报错 2026-01-07 14:19:43 +08:00
caoqianming f5b1b13a63 feat: 添加rem模块 2026-01-06 14:19:37 +08:00
caoqianming f7b09ab1df fix: wpr list annotate明确number指向 2026-01-06 09:22:38 +08:00
caoqianming b362fc3b89 feat: 捕获除0异常 2026-01-05 08:19:38 +08:00
caoqianming 8f791ac8de feat: 按需求修改光芯批次统计分析 2026-01-04 15:55:03 +08:00
caoqianming afa3b8b9ad Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2026-01-04 14:41:37 +08:00
caoqianming 3d6fcfac8a feat: 批次统计数据支持返回source_near 2026-01-04 14:41:36 +08:00
TianyangZhang db910f0804 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2026-01-04 14:16:43 +08:00
TianyangZhang 7dc7a78695 feat:光芯OA 审批系统新增报价单审核 2026-01-04 14:16:41 +08:00
caoqianming 952cdb1bc7 feat: get_batch_dag还是只返回直接前后级别 2026-01-04 13:55:09 +08:00
caoqianming 9ed78f8d32 feat: mlogbbpatch修改批次号 2026-01-04 11:10:54 +08:00
caoqianming 52ebac68a0 feat: 出入库记录返回子表部分信息 2026-01-04 10:10:26 +08:00
caoqianming 81770e89fa feat: 统一撤回和撤销的表述2 2025-12-30 14:34:34 +08:00
caoqianming 6ac7c020bd feat: 统一撤回和撤销的表述 2025-12-30 14:32:29 +08:00
caoqianming e385a558e9 feat: 车间库存检验支持撤回 2025-12-30 14:28:20 +08:00
caoqianming c37e71d60f feat: wpr添加material_name查询条件 2025-12-30 10:07:41 +08:00
caoqianming c3c7675ac5 feat: 优化mlog_submit 返工后产品放在本工段下2 2025-12-29 15:54:14 +08:00
caoqianming 29f4e2f76a feat: 优化mlog_submit 返工后产品放在本工段下 2025-12-29 15:09:45 +08:00
caoqianming ec13b8b166 fix: 正常交接支持new_wm且支持不合格品 2025-12-29 14:41:06 +08:00
caoqianming c56f908b42 feat: mlogbin qct 可依据fix选择 2025-12-26 17:00:00 +08:00
caoqianming a8ae8ee32a feat: base dept filter支持parent isnull查询 2025-12-26 14:52:54 +08:00
52 changed files with 1155 additions and 271 deletions

View File

@ -11,7 +11,7 @@ router.register('question', QuestionViewSet, basename='question')
router.register('paper', PaperViewSet, basename='paper') router.register('paper', PaperViewSet, basename='paper')
router.register('exam', ExamViewSet, basename='exam') router.register('exam', ExamViewSet, basename='exam')
router.register('examrecord', ExamRecordViewSet, basename='examrecord') router.register('examrecord', ExamRecordViewSet, basename='examrecord')
router.register('training', TrainRecordViewSet, basename='examrecord') router.register('training', TrainRecordViewSet, basename='training')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
] ]

View File

@ -0,0 +1,40 @@
# Generated by Django 3.2.12 on 2026-01-07 01:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('wf', '0006_auto_20251215_1645'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('hrm', '0024_emppersoninfo_post'),
]
operations = [
migrations.CreateModel(
name='Leave',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('start_date', models.DateTimeField(verbose_name='开始日期')),
('end_date', models.DateTimeField(verbose_name='结束日期')),
('leave_type', models.PositiveSmallIntegerField(blank=True, choices=[(10, '事假'), (20, '病假'), (30, '婚假'), (40, '丧假'), (50, '公假'), (60, '工伤'), (70, '产假'), (80, '护理假'), (90, '其他')], null=True, verbose_name='请假类型')),
('reason', models.TextField(verbose_name='请假事由')),
('hour', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='请假时长')),
('file', models.TextField(blank=True, null=True, verbose_name='证明')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='leave_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hrm.employee', verbose_name='员工')),
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='leave_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -245,4 +245,29 @@ class EmpPersonInfo(CommonADModel):
phone = models.CharField('手机号', max_length=20,validators=[PHONE_VALIDATOR], null=True, blank=True) phone = models.CharField('手机号', max_length=20,validators=[PHONE_VALIDATOR], null=True, blank=True)
post = models.CharField('岗位', max_length=20, null=True, blank=True) post = models.CharField('岗位', max_length=20, null=True, blank=True)
note = models.TextField('备注', null=True, blank=True) note = models.TextField('备注', null=True, blank=True)
class Leave(CommonADModel):
"""
TN:员工请假
"""
E_TYPE_CHOISE = (
(10, '事假'),
(20, '病假'),
(30, '婚假'),
(40, '丧假'),
(50, '公假'),
(60, '工伤'),
(70, '产假'),
(80, '护理假'),
(90, '其他'),
)
employee = models.ForeignKey(Employee, verbose_name='员工', on_delete=models.CASCADE)
start_date = models.DateTimeField('开始日期')
end_date = models.DateTimeField('结束日期')
leave_type = models.PositiveSmallIntegerField('请假类型', choices=E_TYPE_CHOISE, null=True, blank=True)
reason = models.TextField('请假事由')
hour = models.PositiveSmallIntegerField('请假时长', null=True, blank=True)
file = models.TextField('证明', null=True, blank=True)
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='leave_ticket', null=True, blank=True)

View File

@ -9,7 +9,7 @@ from django.utils import timezone
from apps.utils.serializers import CustomModelSerializer from apps.utils.serializers import CustomModelSerializer
from apps.utils.constants import EXCLUDE_FIELDS from apps.utils.constants import EXCLUDE_FIELDS
from apps.hrm.models import (Certificate, ClockRecord, Employee, from apps.hrm.models import (Certificate, ClockRecord, Employee,
NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo) NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo, Leave)
from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer
from django.db import transaction from django.db import transaction
from django.core.cache import cache from django.core.cache import cache
@ -368,3 +368,12 @@ class EmpPersonInfoSerializer(CustomModelSerializer):
'note', 'note',
) )
class LeaveSerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
employee_name = serializers.CharField(source='employee.name', read_only=True)
post_name = serializers.CharField(source="employee.post.name", read_only=True)
belong_dept_name = serializers.CharField(source='employee.belong_dept.name', read_only=True)
class Meta:
model = Leave
fields = '__all__'

View File

@ -1,5 +1,5 @@
from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet, from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet,
AttendanceViewSet, ResignationViewSet, EmpJoinViewSet) AttendanceViewSet, ResignationViewSet, EmpJoinViewSet, LeaveViewSet)
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
@ -16,6 +16,7 @@ router.register('attendance', AttendanceViewSet, basename='attendance')
router.register('resignation', ResignationViewSet, basename='resignation') router.register('resignation', ResignationViewSet, basename='resignation')
router.register('empneed', EmpNeedViewSet, basename='empneed') router.register('empneed', EmpNeedViewSet, basename='empneed')
router.register('empjoin', EmpJoinViewSet, basename='empjoin') router.register('empjoin', EmpJoinViewSet, basename='empjoin')
router.register('leave', LeaveViewSet, basename='leave')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
] ]

View File

@ -12,7 +12,7 @@ from rest_framework.response import Response
from apps.hrm.errors import NO_NEED_LEVEL_REMARK from apps.hrm.errors import NO_NEED_LEVEL_REMARK
from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet, from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet,
NotWorkRemarkFilterSet) NotWorkRemarkFilterSet)
from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, Leave
from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer, from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer,
ClockRecordListSerializer, ClockRecordListSerializer,
EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer, EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer,
@ -20,7 +20,7 @@ from apps.hrm.serializers import (CertificateCreateUpdateSerializer, Certificate
EmployeeSerializer, EmployeeSerializer,
ClockRecordSimpleSerializer, ClockRecordCreateSerializer, ClockRecordSimpleSerializer, ClockRecordCreateSerializer,
NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer, NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer,
ResignationSerializer, EmpNeedSerializer) ResignationSerializer, EmpNeedSerializer, LeaveSerializer)
from apps.hrm.services import HrmService from apps.hrm.services import HrmService
from apps.third.dahua import dhClient from apps.third.dahua import dhClient
@ -453,3 +453,19 @@ class EmpJoinViewSet(TicketMixin, EuModelViewSet):
serializer = EmpPersonInfoSerializer(data=person, many=True) serializer = EmpPersonInfoSerializer(data=person, many=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
class LeaveViewSet(TicketMixin, EuModelViewSet):
select_related_fields = [
'employee',
'employee__belong_dept',
'ticket',
]
queryset = Leave.objects.all()
serializer_class = LeaveSerializer
filterset_fields = ['leave_type', 'employee__belong_dept']
search_fields = ["employee__name", "leave_type"]
workflow_key = "wf_leave"
def gen_other_ticket_data(self, instance):
return {"hour": instance.hour if instance.hour else None}

View File

@ -247,6 +247,15 @@ class MIOItemAListSerializer(CustomModelSerializer):
read_only_fields = EXCLUDE_FIELDS_BASE read_only_fields = EXCLUDE_FIELDS_BASE
class MIOItemListSimpleSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
material_name = serializers.StringRelatedField(
source='material', read_only=True)
class Meta:
model = MIOItem
fields = ["id", "mio", "material", "warehouse", "material_name", "warehouse_name", "batch", "count", "test_date", "count_notok"]
class MIOItemSerializer(CustomModelSerializer): class MIOItemSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True) material_ = MaterialSerializer(source='material', read_only=True)

View File

@ -13,10 +13,10 @@ router.register('warehouse', WarehouseVIewSet, basename='warehouse')
router.register('materialbatch', MaterialBatchViewSet, router.register('materialbatch', MaterialBatchViewSet,
basename='materialbatch') basename='materialbatch')
router.register('mio', MIOViewSet, basename='mio') router.register('mio', MIOViewSet, basename='mio')
router.register('mio/do', MioDoViewSet) router.register('mio/do', MioDoViewSet, basename='mio_do')
router.register('mio/sale', MioSaleViewSet) router.register('mio/sale', MioSaleViewSet, basename='mio_sale')
router.register('mio/pur', MioPurViewSet) router.register('mio/pur', MioPurViewSet, basename='mio_pur')
router.register('mio/other', MioOtherViewSet) router.register('mio/other', MioOtherViewSet, basename='mio_other')
router.register('mioitem', MIOItemViewSet, basename='mioitem') router.register('mioitem', MIOItemViewSet, basename='mioitem')
router.register('mioitemw', MIOItemwViewSet, basename='mioitemw') router.register('mioitemw', MIOItemwViewSet, basename='mioitemw')
# router.register('pack', PackViewSet, basename='pack') # router.register('pack', PackViewSet, basename='pack')

View File

@ -14,7 +14,7 @@ from apps.inm.serializers import (
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer, MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer, MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer, MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer) MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer, MIOItemListSimpleSerializer)
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.inm.services import InmService from apps.inm.services import InmService
@ -163,20 +163,39 @@ class MIOViewSet(CustomModelViewSet):
return mio return mio
def add_info_for_list(self, data): def add_info_for_list(self, data):
# 获取检验状态 # 1. 收集所有mio的ID
mio_dict = {} mio_ids = [item['id'] for item in data]
# 2. 预初始化mio字典和items列表
mio_dict = {item['id']: {
**item,
'is_tested': False, # 默认值设为False
'mioitems': []
} for item in data}
# 3. 批量查询MIOItem数据
if mio_ids: # 避免空查询
mioitems = MIOItemListSimpleSerializer(
instance=MIOItem.objects.filter(
mio__id__in=mio_ids
).select_related("warehouse", "material"),
many=True
).data
# 4. 单次循环处理所有item
for item in mioitems:
mio_id = item['mio']
if mio_id in mio_dict:
mio_dict[mio_id]['mioitems'].append(item)
# 更新is_tested状态只要有一个item有test_date就为True
if item.get('test_date'):
mio_dict[mio_id]['is_tested'] = True
# 5. 直接返回原始data列表避免额外转换
for item in data: for item in data:
item['is_tested'] = None item.update(mio_dict[item['id']])
mio_dict[item['id']] = item
mioitems = list(MIOItem.objects.filter(mio__id__in=mio_dict.keys()).values_list("mio__id", "test_date")) return data
for item in mioitems:
mioId, test_date = item
is_tested = False
if test_date:
is_tested = True
mio_dict[mioId]['is_tested'] = is_tested
datax = [mio_dict[key] for key in mio_dict.keys()]
return datax
def get_serializer_class(self): def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']: if self.action in ['create', 'update', 'partial_update']:

View File

@ -229,7 +229,7 @@ class Mgroup(CommonBModel):
# 如果当前时间在结束时间之前,属于前一天 # 如果当前时间在结束时间之前,属于前一天
else: else:
return (w_s_time - timedelta(days=1)).date(), shift return (w_s_time - timedelta(days=1)).date(), shift
# return w_s_time.date(), None raise ParseError(f"工段{self.name}的班次规则未覆盖时间点{w_s_time.strftime('%H:%M:%S')}")
class TeamMember(BaseModel): class TeamMember(BaseModel):

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2026-01-04 06:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0004_auto_20251218_1636'),
]
operations = [
migrations.AlterField(
model_name='vehicleuse',
name='end_km',
field=models.PositiveIntegerField(default=0, verbose_name='归还公里数'),
),
]

View File

@ -60,7 +60,7 @@ class VehicleUse(CommonBDModel):
via = models.CharField('途经地点', null=True, blank=True, max_length=100) via = models.CharField('途经地点', null=True, blank=True, max_length=100)
destination = models.CharField('到达地点', null=True, blank=True, max_length=100) destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
start_km = models.PositiveIntegerField('出发公里数', null=True, blank=True) start_km = models.PositiveIntegerField('出发公里数', null=True, blank=True)
end_km = models.PositiveIntegerField('归还公里数') end_km = models.PositiveIntegerField('归还公里数', default=0)
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False) actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
is_city = models.BooleanField('是否市内用车', default=True) is_city = models.BooleanField('是否市内用车', default=True)
reason = models.CharField('用车事由', max_length=100) reason = models.CharField('用车事由', max_length=100)

View File

@ -0,0 +1,44 @@
# Generated by Django 3.2.12 on 2025-12-26 07:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0006_auto_20251215_1645'),
('pum', '0009_supplieraudit'),
]
operations = [
migrations.CreateModel(
name='QuotationApply',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('customer_name', models.CharField(max_length=50, verbose_name='客户名称')),
('product_name', models.CharField(max_length=50, verbose_name='产品名称')),
('contact_person', models.CharField(max_length=50, verbose_name='联系人')),
('contact_phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='联系电话')),
('receive_address', models.CharField(blank=True, max_length=100, null=True, verbose_name='收件地址')),
('product_spec_quantity', models.TextField(blank=True, null=True, verbose_name='产品规格/数量')),
('quotation_basis', models.TextField(blank=True, null=True, verbose_name='报价依据')),
('suggested_price_calc', models.TextField(blank=True, null=True, verbose_name='建议价格及计算方式')),
('quotation_range', models.CharField(blank=True, max_length=100, null=True, verbose_name='报价区间')),
('quoter', models.CharField(blank=True, max_length=50, null=True, verbose_name='报价人')),
('apply_date', models.DateField(auto_now_add=True, verbose_name='申请日期')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotationapply_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quo_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotationapply_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -107,3 +107,22 @@ class PuPlanItem(CommonBDModel):
null=True, blank=True, related_name='item_puplan') null=True, blank=True, related_name='item_puplan')
pu_order = models.ForeignKey(PuOrder, verbose_name='关联采购订单', pu_order = models.ForeignKey(PuOrder, verbose_name='关联采购订单',
on_delete=models.SET_NULL, null=True, blank=True, related_name='puplan_item_puorder') on_delete=models.SET_NULL, null=True, blank=True, related_name='puplan_item_puorder')
class QuotationApply(CommonADModel):
"""
TN:报价申请表
"""
customer_name = models.CharField(max_length=50,verbose_name="客户名称")
product_name = models.CharField(max_length=50,verbose_name="产品名称")
contact_person = models.CharField(max_length=50,verbose_name="联系人")
contact_phone = models.CharField(max_length=20,verbose_name="联系电话", null=True, blank=True)
receive_address = models.CharField(max_length=100,verbose_name="收件地址", null=True, blank=True)
product_spec_quantity = models.TextField(verbose_name="产品规格/数量", null=True, blank=True)
quotation_basis = models.TextField(verbose_name="报价依据", null=True, blank=True)
suggested_price_calc = models.TextField(verbose_name="建议价格及计算方式", null=True, blank=True)
quotation_range = models.CharField(max_length=100,verbose_name="报价区间", null=True, blank=True)
quoter = models.CharField(max_length=50,verbose_name="报价人", null=True, blank=True)
apply_date = models.DateField(verbose_name="申请日期",auto_now_add=True)
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='quo_ticket', null=True, blank=True)

View File

@ -3,7 +3,7 @@ from apps.utils.serializers import CustomModelSerializer
from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS
from rest_framework.exceptions import ValidationError, ParseError from rest_framework.exceptions import ValidationError, ParseError
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
from django.db import transaction from django.db import transaction
from .services import PumService from .services import PumService
@ -157,4 +157,11 @@ class SupplierAuditSerializer(CustomModelSerializer):
name = validated_data["name"] name = validated_data["name"]
if Supplier.objects.filter(name=name).exists(): if Supplier.objects.filter(name=name).exists():
raise ParseError('供应商名称已存在') raise ParseError('供应商名称已存在')
return super().create(validated_data) return super().create(validated_data)
class QuotationApplySerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = QuotationApply
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS

View File

@ -1,6 +1,7 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet) from apps.pum.views import (SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet, SupplierAuditViewSet, QuotationApplyViewSet)
# from apps.pum.views import SupplierViewSet, PuPlanViewSet, PuPlanItemViewSet, PuOrderViewSet, PuOrderItemViewSet
API_BASE_URL = 'api/pum/' API_BASE_URL = 'api/pum/'
HTML_BASE_URL = 'dhtml/pum/' HTML_BASE_URL = 'dhtml/pum/'
@ -12,6 +13,7 @@ router.register('pu_plan', PuPlanViewSet, basename='pu_plan')
router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem') router.register('pu_planitem', PuPlanItemViewSet, basename='pu_planitem')
router.register('pu_order', PuOrderViewSet, basename='pu_order') router.register('pu_order', PuOrderViewSet, basename='pu_order')
router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem') router.register('pu_orderitem', PuOrderItemViewSet, basename='pu_orderitem')
router.register('quotation', QuotationApplyViewSet, basename='quotation')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
] ]

View File

@ -1,7 +1,7 @@
from django.shortcuts import render from django.shortcuts import render
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem, SupplierAudit, QuotationApply
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer, from apps.pum.serializers import (SupplierSerializer, PuPlanSerializer, PuPlanItemSerializer, QuotationApplySerializer,
PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer) PuOrderSerializer, PuOrderItemSerializer, AddSerializer, SupplierAuditSerializer)
from rest_framework.exceptions import ParseError, PermissionDenied from rest_framework.exceptions import ParseError, PermissionDenied
from rest_framework.decorators import action from rest_framework.decorators import action
@ -210,3 +210,16 @@ class PuOrderItemViewSet(CustomModelViewSet):
item.pu_order = puorder item.pu_order = puorder
item.save() item.save()
return Response() return Response()
class QuotationApplyViewSet(TicketMixin, CustomModelViewSet):
"""
list: 报价申请
报价申请
"""
queryset = QuotationApply.objects.all()
serializer_class = QuotationApplySerializer
filterset_fields = ['product_name', 'customer_name','apply_date', 'quoter']
search_fields = ['product_name', 'customer_name','contact_person']
ordering = ['create_time']
workflow_key = "wf_quotation"

View File

@ -157,6 +157,43 @@ def ftestwork_submit(ins:FtestWork, user: User):
# 触发批次统计分析 # 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch]) ana_batch_thread(xbatchs=[ins.batch])
def ftestwork_revert(ins: FtestWork):
wm:WMaterial = ins.wm
if wm and ins.need_update_wm:
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
for item in fwd_qs:
item:FtestworkDefect = item
if item.count > 0:
wm.count = wm.count + item.count
wm.save()
wmstate = WMaterial.WM_OK
if item.defect.okcate == Defect.DEFECT_NOTOK:
wmstate = WMaterial.WM_NOTOK
try:
wmx = WMaterial.objects.get(
material=wm.material,
batch=wm.batch,
mgroup=wm.mgroup,
belong_dept=wm.belong_dept,
state=wmstate,
notok_sign=None,
defect=item.defect,
)
except Exception as e:
raise ParseError(f'找不到{item.defect.name}的车间库存: {str(e)}')
wmx.count = wmx.count - item.count
if wmx.count < 0:
raise ParseError("数量不足,撤回失败")
wmx.save()
else:
raise ParseError("该检验工作不支持撤回")
ins.submit_user = None
ins.submit_time = None
ins.save()
# 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch])
def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict): def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
ins = FtestWork.objects.get(id=new_ticket_data['t_id']) ins = FtestWork.objects.get(id=new_ticket_data['t_id'])
if ins.submit_time is not None: if ins.submit_time is not None:

View File

@ -17,7 +17,7 @@ from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.wpm.models import SfLog from apps.wpm.models import SfLog
from apps.qm.filters import QuaStatFilter, TestItemFilter, FtestWorkFilter, QctFilter, FtestFilter from apps.qm.filters import QuaStatFilter, TestItemFilter, FtestWorkFilter, QctFilter, FtestFilter
from django.db import transaction from django.db import transaction
from apps.qm.services import ftestwork_submit from apps.qm.services import ftestwork_submit, ftestwork_revert
from apps.wpm.services_2 import ana_batch_thread from apps.wpm.services_2 import ana_batch_thread
from apps.wf.models import State from apps.wf.models import State
# Create your views here. # Create your views here.
@ -327,4 +327,20 @@ class FtestWorkViewSet(CustomModelViewSet):
ftestwork_submit(ins, request.user) ftestwork_submit(ins, request.user)
else: else:
raise ParseError('该检验工作已提交') raise ParseError('该检验工作已提交')
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'ftestwork.submit'}, serializer_class=Serializer)
@transaction.atomic
def revert(self, request, *args, **kwargs):
"""撤回检验工作
撤回检验工作
"""
ins:FtestWork = self.get_object()
if ins.submit_time:
if self.request.user != ins.submit_user:
raise ParseError('只能由提交人撤回')
ftestwork_revert(ins)
else:
raise ParseError('该检验工作未提交')
return Response() return Response()

0
apps/rem/__init__.py Normal file
View File

3
apps/rem/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/rem/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RemConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.rem'

View File

@ -0,0 +1,41 @@
# Generated by Django 3.2.12 on 2026-01-06 01:49
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('system', '0006_auto_20241213_1249'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.TextField(verbose_name='项目名称')),
('description', models.TextField(verbose_name='项目介绍')),
('start_date', models.DateField(verbose_name='开始日期')),
('end_date', models.DateField(verbose_name='结束日期')),
('participants', models.TextField(blank=True, null=True, verbose_name='项目成员')),
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('files', models.ManyToManyField(blank=True, to='system.File', verbose_name='附件')),
('leader', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='项目负责人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

15
apps/rem/models.py Normal file
View File

@ -0,0 +1,15 @@
from django.db import models
from apps.utils.models import CommonADModel
from apps.system.models import User, File
# Create your models here.
class Project(CommonADModel):
name = models.TextField("项目名称")
description = models.TextField("项目介绍")
start_date = models.DateField("开始日期")
end_date = models.DateField("结束日期")
leader = models.ForeignKey(User, verbose_name="项目负责人", on_delete=models.CASCADE)
participants = models.TextField("项目成员", blank=True, null=True)
files = models.ManyToManyField(File, verbose_name="附件", blank=True)
note = models.TextField("备注", blank=True, null=True)

17
apps/rem/serializers.py Normal file
View File

@ -0,0 +1,17 @@
from apps.rem.models import Project
from apps.utils.serializers import CustomModelSerializer
from apps.system.serializers import FileSerializer
from rest_framework import serializers
class ProjectSerializer(CustomModelSerializer):
leader_name = serializers.CharField(source="leader.name", read_only=True)
files_ = FileSerializer(source="files", many=True, read_only=True)
class Meta:
model = Project
fields = '__all__'
class ProjectUpdateSerializer(CustomModelSerializer):
class Meta:
model = Project
fields = ["id", "participants", "files", "note"]

3
apps/rem/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
apps/rem/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet
API_BASE_URL = 'api/rem/'
HTML_BASE_URL = 'dhtml/rem/'
router = DefaultRouter()
router.register('project', ProjectViewSet, basename='research_project')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

12
apps/rem/views.py Normal file
View File

@ -0,0 +1,12 @@
from django.shortcuts import render
from apps.utils.viewsets import CustomModelViewSet
from apps.rem.models import Project
from apps.rem.serializers import ProjectSerializer, ProjectUpdateSerializer
# Create your views here.
class ProjectViewSet(CustomModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
update_serializer_class = ProjectUpdateSerializer
select_related_fields = ['leader']
search_fields = ["name", "description", "leader__name"]

View File

@ -7,6 +7,7 @@ from rest_framework.exceptions import ParseError
class UserFilterSet(filters.FilterSet): class UserFilterSet(filters.FilterSet):
ubelong_dept__name = filters.CharFilter(label='归属于该部门及以下(按名称)', method='filter_ubelong_dept__name') ubelong_dept__name = filters.CharFilter(label='归属于该部门及以下(按名称)', method='filter_ubelong_dept__name')
ubelong_dept = filters.CharFilter(label='归属于该部门及以下', method='filter_ubelong_dept') ubelong_dept = filters.CharFilter(label='归属于该部门及以下', method='filter_ubelong_dept')
has_perm = filters.CharFilter(label='拥有指定权限标识', method='filter_has_perm')
class Meta: class Meta:
model = User model = User
@ -37,6 +38,9 @@ class UserFilterSet(filters.FilterSet):
except Exception as e: except Exception as e:
raise ParseError(f"部门ID错误: {value} {str(e)}") raise ParseError(f"部门ID错误: {value} {str(e)}")
return queryset.filter(belong_dept__in=depts) return queryset.filter(belong_dept__in=depts)
def filter_has_perm(self, queryset, name, value):
return queryset.filter(up_user__post__pr_post__role__perms__codes__contains=value)
class DeptFilterSet(filters.FilterSet): class DeptFilterSet(filters.FilterSet):
@ -45,5 +49,6 @@ class DeptFilterSet(filters.FilterSet):
model = Dept model = Dept
fields = { fields = {
'type': ['exact', 'in'], 'type': ['exact', 'in'],
'name': ['exact', 'in', 'contains'] 'name': ['exact', 'in', 'contains'],
"parent": ['exact', 'isnull'],
} }

View File

@ -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='最后编辑人'),
),
]

View File

@ -54,7 +54,7 @@ class Dept(ParentModel, CommonAModel):
name = models.CharField('名称', max_length=60) name = models.CharField('名称', max_length=60)
type = models.CharField('类型', max_length=20, default='dept') type = models.CharField('类型', max_length=20, default='dept')
sort = models.PositiveSmallIntegerField('排序标记', default=1) sort = models.PositiveSmallIntegerField('排序标记', default=1)
third_info = models.JSONField('三方系统信息', default=dict) third_info = models.JSONField('三方系统信息', default=dict, blank=True)
class Meta: class Meta:
verbose_name = '部门' verbose_name = '部门'
@ -107,9 +107,9 @@ class PostRole(BaseModel):
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices, data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices,
default=DataFilter.THISLEVEL_AND_BELOW) default=DataFilter.THISLEVEL_AND_BELOW)
post = models.ForeignKey(Post, verbose_name='关联岗位', post = models.ForeignKey(Post, verbose_name='关联岗位',
on_delete=models.CASCADE) on_delete=models.CASCADE, related_name="pr_post")
role = models.ForeignKey(Role, verbose_name='关联角色', role = models.ForeignKey(Role, verbose_name='关联角色',
on_delete=models.CASCADE) on_delete=models.CASCADE, related_name='pr_role')
class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager): class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager):
@ -132,7 +132,7 @@ class User(AbstractUser, CommonBModel):
posts = models.ManyToManyField( posts = models.ManyToManyField(
Post, through='system.userpost', related_name='user_posts') Post, through='system.userpost', related_name='user_posts')
depts = models.ManyToManyField(Dept, through='system.userpost') depts = models.ManyToManyField(Dept, through='system.userpost')
roles = models.ManyToManyField(Role, verbose_name='关联角色') roles = models.ManyToManyField(Role, verbose_name='关联角色', blank=True)
# 关联账号 # 关联账号
secret = models.CharField('密钥', max_length=100, null=True, blank=True) secret = models.CharField('密钥', max_length=100, null=True, blank=True)

View File

@ -322,7 +322,7 @@ def phone_exist(phone):
def user_exist(username): def user_exist(username):
if User.objects.filter(username=username).exists(): if User.objects.get_queryset(all=True).filter(username=username).exists():
raise serializers.ValidationError(**USERNAME_EXIST) raise serializers.ValidationError(**USERNAME_EXIST)
return username return username

View File

@ -300,9 +300,15 @@ class ComplexQueryMixin:
page = self.paginate_queryset(new_qs) page = self.paginate_queryset(new_qs)
if page is not None: if page is not None:
serializer = self.get_serializer(page, many=True) serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data) rdata = serializer.data
if hasattr(self, 'add_info_for_list'):
rdata = self.add_info_for_list(rdata)
return self.get_paginated_response(rdata)
serializer = self.get_serializer(new_qs, many=True) serializer = self.get_serializer(new_qs, many=True)
return Response(serializer.data) rdata = serializer.data
if hasattr(self, 'add_info_for_list'):
rdata = self.add_info_for_list(rdata)
return Response(rdata)
class MyLoggingMixin(object): class MyLoggingMixin(object):
"""Mixin to log requests""" """Mixin to log requests"""

View File

@ -150,7 +150,33 @@ class BaseModel(models.Model):
raise raise
time.sleep(0.1 * (attempt + 1)) time.sleep(0.1 * (attempt + 1))
@classmethod
def locked_get_or_create(cls, defaults: dict, **kwargs):
"""
仅用于事务内
并发安全的 get_or_create
"""
if not connection.in_atomic_block:
raise RuntimeError("locked_get_or_create 必须在事务中调用")
defaults = defaults or {}
qs = cls.objects.select_for_update().filter(**kwargs)
cnt = qs.count()
if cnt > 1:
raise RuntimeError(
f"{cls.__name__} 数据异常:定位条件 {kwargs} 命中 {cnt}"
)
if cnt == 1:
return qs.get(), False
params = {**kwargs, **defaults}
obj = cls.objects.create(**params)
return obj, True
def handle_parent(self): def handle_parent(self):
pass pass

View File

@ -1,8 +1,8 @@
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from apps.wpm.models import (SfLog, StLog, WMaterial, Mlog, Mlogbw, from apps.wpm.models import (SfLog, StLog, WMaterial, Mlog, Mlogbw,
Handover, Mgroup, Mlogb, Mtask, BatchSt) Handover, Mgroup, Mlogb, Mtask, BatchSt, Handoverb)
from apps.mtm.models import Route, Material from apps.mtm.models import Route, Material
from django.db.models import Q from django.db.models import Q, Exists, OuterRef
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from datetime import datetime from datetime import datetime
@ -43,6 +43,7 @@ class WMaterialFilter(filters.FilterSet):
material__process__exclude = filters.CharFilter(field_name="material__process", lookup_expr="exact", exclude=True) material__process__exclude = filters.CharFilter(field_name="material__process", lookup_expr="exact", exclude=True)
mlog_date_start = filters.DateFilter(label="产出开始", method="filter_mlog_date_start") mlog_date_start = filters.DateFilter(label="产出开始", method="filter_mlog_date_start")
mlog_date_end = filters.DateFilter(label="产出结束", method="filter_mlog_date_end") mlog_date_end = filters.DateFilter(label="产出结束", method="filter_mlog_date_end")
current_merged = filters.BooleanFilter(label="是否本工段新合成的批", method="filter_current_merged")
def filter_mlog_date_start(self, queryset, name, value): def filter_mlog_date_start(self, queryset, name, value):
mgroupId = self.data.get("mgroup", None) mgroupId = self.data.get("mgroup", None)
@ -101,6 +102,18 @@ class WMaterialFilter(filters.FilterSet):
raise ParseError('生产路线不存在!') raise ParseError('生产路线不存在!')
return queryset.filter(material=route.material_in)|queryset.filter(material__in=route.materials.all()) return queryset.filter(material=route.material_in)|queryset.filter(material__in=route.materials.all())
def filter_current_merged(self, queryset, name, value):
sub_qs = Handoverb.objects.filter(
wm_to=OuterRef("pk"),
handover__mtype=Handover.H_MERGE,
handover__submit_time__isnull=False
)
if value is True:
return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=True)
elif value is False:
return queryset.annotate(_has_merge=Exists(sub_qs)).filter(_has_merge=False)
return queryset
class Meta: class Meta:
model = WMaterial model = WMaterial
fields = { fields = {
@ -154,10 +167,16 @@ class MlogFilter(filters.FilterSet):
class HandoverFilter(filters.FilterSet): class HandoverFilter(filters.FilterSet):
cbatch = filters.CharFilter(label='批次号', method='filter_cbatch') cbatch = filters.CharFilter(label='批次号', method='filter_cbatch')
mgroup = filters.CharFilter(label='MgroupId', method='filter_mgroup') mgroup = filters.CharFilter(label='MgroupId', method='filter_mgroup')
mgroupx = filters.CharFilter(label='MgroupId', method='filter_mgroupx')
dept = filters.CharFilter(label='DeptId', method='filter_dept') dept = filters.CharFilter(label='DeptId', method='filter_dept')
def filter_mgroup(self, queryset, name, value): def filter_mgroup(self, queryset, name, value):
return queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value) return queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value)
def filter_mgroupx(self, queryset, name, value):
dept = Mgroup.objects.get(id=value).belong_dept
return (queryset.filter(send_mgroup__id=value)|queryset.filter(recive_mgroup__id=value)|
queryset.filter(send_dept=dept, send_mgroup__isnull=True)|queryset.filter(recive_dept=dept, recive_mgroup__isnull=True))
def filter_dept(self, queryset, name, value): def filter_dept(self, queryset, name, value):
return queryset.filter(send_dept__id=value)|queryset.filter(recive_dept__id=value) return queryset.filter(send_dept__id=value)|queryset.filter(recive_dept__id=value)
@ -223,7 +242,11 @@ class MlogbFilter(filters.FilterSet):
class BatchStFilter(filters.FilterSet): class BatchStFilter(filters.FilterSet):
batch__startswith__in = filters.CharFilter(method='filter_batch') batch__startswith__in = filters.CharFilter(method='filter_batch')
data__has_key = filters.CharFilter(method='filter_data') data__has_key = filters.CharFilter(method='filter_data')
with_source_near = filters.CharFilter(label='来源', method='filter_source_near')
def filter_source_near(self, queryset, name, value):
return queryset
def filter_data(self, queryset, name, value): def filter_data(self, queryset, name, value):
return queryset.filter(data__has_key=value) return queryset.filter(data__has_key=value)

View File

@ -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='最后编辑人'),
),
]

View File

@ -14,9 +14,11 @@ from django.db.models import Count
from django.db import transaction from django.db import transaction
from django.db.models import Max from django.db.models import Max
import re import re
from django.db.models import Q from django.db.models import Q, F
import django.utils.timezone as timezone import django.utils.timezone as timezone
from apps.utils.sql import query_all_dict from apps.utils.sql import query_all_dict
import logging
myLogger = logging.getLogger('log')
# Create your models here. # Create your models here.
class SfLog(CommonADModel): class SfLog(CommonADModel):
@ -125,6 +127,10 @@ class WMaterial(CommonBDModel):
material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom') material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom')
number_from = models.TextField("来源于个号", null=True, blank=True) number_from = models.TextField("来源于个号", null=True, blank=True)
@property
def belong_dept_or_mgroup_id(self):
return self.mgroup.id if self.mgroup else self.belong_dept.id
@property @property
def count_working(self): def count_working(self):
return Mlogb.objects.filter(wm_in=self, mlog__submit_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0 return Mlogb.objects.filter(wm_in=self, mlog__submit_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0
@ -161,6 +167,30 @@ class WMaterial(CommonBDModel):
), ),
state__in=[WMaterial.WM_OK, WMaterial.WM_REPAIR] state__in=[WMaterial.WM_OK, WMaterial.WM_REPAIR]
) )
@classmethod
def increase(cls, wm_id: str, user:User, count, count_eweight=None):
updates = {}
if count:
updates['count'] = F('count') + count
if count_eweight:
updates['count_eweight'] = count_eweight
if not updates:
return 0
updates["update_by"] = user
updates['update_time'] = timezone.now()
return cls.objects.filter(id=wm_id).update(**updates)
@classmethod
def decrease(cls, wm_id: str, user:User, count):
if not count:
return 0
updated = cls.objects.filter(id=wm_id, count__gte= count).update(
count=F('count') - count, update_by=user, update_time=timezone.now())
if updated == 0:
batch = WMaterial.objects.get(id=wm_id).batch
raise ParseError(f'{batch}_库存不足无法完成扣减')
return updated
class Fmlog(CommonADModel): class Fmlog(CommonADModel):
"""TN: 父级生产日志 """TN: 父级生产日志
@ -639,6 +669,7 @@ class Handoverb(BaseModel):
wm_to = models.ForeignKey(WMaterial, verbose_name='所到车间库存', on_delete=models.SET_NULL, wm_to = models.ForeignKey(WMaterial, verbose_name='所到车间库存', on_delete=models.SET_NULL,
null=True, blank=True, related_name='handoverb_wm_to') null=True, blank=True, related_name='handoverb_wm_to')
count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1) count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1)
oinfo_json = models.JSONField('其他信息', default=dict, blank=True)
@property @property
def handoverbw(self): def handoverbw(self):
@ -837,41 +868,46 @@ class BatchLog(BaseModel):
@classmethod @classmethod
def batches_to(cls, batch:str): def batches_to(cls, batch:str):
# query = """
# SELECT batch FROM wpm_batchst
# WHERE batch ~ %s
# """
query = """ query = """
SELECT batch SELECT
FROM wpm_batchst batch,
WHERE batch ~ %s CAST(substring(batch FROM LENGTH(%s) + 2) AS INTEGER) AS batch_num
ORDER BY FROM wpm_batchst
-- 先按前缀部分排序例如 'A' WHERE batch LIKE %s AND translate(
SUBSTRING(batch FROM '^(.*)-') DESC, substring(batch FROM LENGTH(%s) + 2),
-- 再按后缀的数值部分排序 '2', '11' 转为整数 '0123456789',
CAST(SUBSTRING(batch FROM '-([0-9]+)$') AS INTEGER) DESC ''
""" # 排序可在sql层处理 ) = ''
query_ = """SELECT batch FROM wpm_batchst WHERE batch ~ %s""" ORDER BY batch_num DESC
pattern = f'^{batch}-[0-9]+$' """
"""可以用如下方法直接查询 prefix = batch
""" params = (
# batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct() prefix,
# batches = sorted(list(batches), key=custom_key) f"{prefix}-%",
batches_r = query_all_dict(query_, params=(pattern,)) prefix
batches = [b["batch"] for b in batches_r] )
batches = sorted(list(batches), key=custom_key)
last_batch_num = None try:
if batches: rows = query_all_dict(query, params=params)
last_batch = batches[-1] except Exception as e:
last_batch_list = last_batch.split("-") myLogger.error(f"BatchLog.batches_to error: {(str(e), query, params)}")
if last_batch_list: raise
try:
last_batch_num = int(last_batch_list[-1]) if not rows:
except Exception: return {
pass "batches": [],
return {"batches": batches, "last_batch_num": last_batch_num, "last_batch": last_batch} "last_batch_num": None,
return {"batches": [], "last_batch_num": None, "last_batch": None} "last_batch": None,
}
batches = [r["batch"] for r in rows]
last = rows[0]
return {
"batches": batches,
"last_batch_num": last["batch_num"],
"last_batch": last["batch"],
}

View File

@ -1,6 +1,6 @@
from apps.wpm.models import BatchSt from apps.wpm.models import BatchSt
import logging import logging
from apps.qm.models import Defect from apps.qm.models import Defect, FtestWork, FtestworkDefect
from apps.wpm.models import Mlogb, MlogbDefect from apps.wpm.models import Mlogb, MlogbDefect
from apps.mtm.models import Mgroup from apps.mtm.models import Mgroup
import decimal import decimal
@ -117,24 +117,53 @@ def main(batch: str, mgroup_obj):
data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"] data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"]
data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2) data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2)
# 车间库存抽检
ft_qs = FtestWork.objects.filter(type2=FtestWork.TYPE2_SOME, wm__mgroup__name="外观检验", batch=batch, submit_time__isnull=False)
if ft_qs.exists():
data["外观检验_车间库存抽检_日期"] = []
data["外观检验_车间库存抽检_操作人"] = []
data["外观检验_车间库存抽检_count_notok"] = 0
for item in ft_qs:
if item.test_user:
data["外观检验_车间库存抽检_操作人"].append(item.test_user)
if item.test_date:
data["外观检验_车间库存抽检_日期"].append(item.test_date)
data["外观检验_车间库存抽检_count_notok"] += item.count_notok if item.count_notok else 0
data["外观检验_车间库存抽检_日期"] = list(set(data["外观检验_车间库存抽检_日期"]))
data["外观检验_车间库存抽检_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["外观检验_车间库存抽检_日期"]])
data["外观检验_车间库存抽检_操作人"] = list(set(data["外观检验_车间库存抽检_操作人"]))
data["外观检验_车间库存抽检_操作人"] = ";".join([item.name for item in data["外观检验_车间库存抽检_操作人"]])
# 车间库存抽检缺陷
ftd_qs = FtestworkDefect.objects.filter(ftestwork__in=ft_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
for item in ftd_qs:
data[f"外观检验_车间库存抽检_缺陷_{item['defect__name']}"] = item["total"]
if "外观检验_count_ok" in data: if "外观检验_count_ok" in data:
data["外观检验_总合格数"] = data["外观检验_count_ok"] + data["外观检验_返修_count_ok"] if "外观检验_返修_count_ok" in data else 0 data["外观检验_总合格数"] = data["外观检验_count_ok"] + data.get("外观检验_返修_count_ok", 0)
try: try:
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2) data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
except decimal.InvalidOperation: except decimal.InvalidOperation:
data["外观检验_总合格率"] = 0 data["外观检验_总合格率"] = 0
data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + data["外观检验_返修_count_ok_full"] if "外观检验_返修_count_ok_full" in data else 0 data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + data.get("外观检验_返修_count_ok_full", 0)
try: try:
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2) data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
except decimal.InvalidOperation: except decimal.InvalidOperation:
data["外观检验_完全总合格率"] = 0 data["外观检验_完全总合格率"] = 0
data["外观检验_直通合格数"] = data["外观检验_总合格数"] - data.get("外观检验_车间库存抽检_count_notok", 0)
if "尺寸检验_合格率" in data: if "尺寸检验_合格率" in data:
try: try:
data["外观检验_直通合格率"] = round((data["外观检验_总合格率"]* data["尺寸检验_合格率"])/100, 2) data["外观检验_直通合格率"] = round((data["外观检验_总合格率"]* data["尺寸检验_合格率"])/100, 2)
except decimal.InvalidOperation: except decimal.InvalidOperation:
data["外观检验_直通合格率"] = 0 data["外观检验_直通合格率"] = 0
try:
data["外观检验_直通合格率2"] = round((data["外观检验_直通合格数"]/data["尺寸检验_count_use"])*100, 2)
except (decimal.InvalidOperation, ZeroDivisionError):
data["外观检验_直通合格率2"] = 0
if "尺寸检验_完全合格率" in data: if "尺寸检验_完全合格率" in data:
try: try:

View File

@ -292,7 +292,7 @@ def main(batch: str, mgroup_obj=None):
data["六车间交接领料_接料人"] = ";".join([item.name for item in data["六车间交接领料_接料人"]]) data["六车间交接领料_接料人"] = ";".join([item.name for item in data["六车间交接领料_接料人"]])
# 六车间工段生产数据 # 六车间工段生产数据
mgroup_list = ["平头", "粘铁头", "粗中细磨", "平磨", "掏管", "抛光", "开槽", "倒角"] mgroup_list = ["平头", "粘铁头", "粗中细磨", "平磨", "掏管", "抛光", "开槽", "倒角", "加工前检验", "中检"]
for mgroup_name in mgroup_list: for mgroup_name in mgroup_list:
if mgroup_name == '粗中细磨': if mgroup_name == '粗中细磨':
mgroups = Mgroup.objects.filter(name__in=['粗磨', '粗中磨', '粗中细磨']) mgroups = Mgroup.objects.filter(name__in=['粗磨', '粗中磨', '粗中细磨'])

View File

@ -1,36 +1,67 @@
from apps.wpmw.models import Wpr from apps.wpmw.models import Wpr
from apps.wpm.models import Mlogbw from apps.wpm.models import Mlogbw, Mlog, MlogUser
from apps.qm.models import Ftest, FtestDefect, FtestItem from apps.qm.models import Ftest, FtestDefect, FtestItem, TestItem
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from apps.mtm.models import Mgroup from apps.mtm.models import Mgroup
def main(wprId, mgroup:Mgroup): def main(wprId, mgroup:Mgroup=None):
wpr = Wpr.objects.get(id=wprId) wpr = Wpr.objects.get(id=wprId)
if mgroup is None:
mgroup_ids = Mlogbw.objects.filter(
wpr=wpr,
mlogb__mlog__submit_time__isnull=False,
mlogb__mlog__is_fix=False
).values_list(
'mlogb__mlog__mgroup',
flat=True
).distinct()
mgroups = Mgroup.objects.filter(id__in=mgroup_ids)
else:
mgroups = [mgroup]
data = {} data = {}
mgroup_name = mgroup.name for mgroup in mgroups:
mlogbw = Mlogbw.objects.filter(wpr=wpr, mlogb__mlog__mgroup=mgroup, mlogb__mlog__submit_time__isnull=False).order_by("-update_time").first() mgroup_name = mgroup.name
if mlogbw: mlogbw = Mlogbw.objects.filter(wpr=wpr,
data[f"{mgroup_name}_批次号"] = mlogbw.mlogb.batch mlogb__mlog__mgroup=mgroup,
data[f"{mgroup_name}_日期"] = mlogbw.mlogb.mlog.handle_date.strftime("%Y-%m-%d") mlogb__mlog__submit_time__isnull=False, mlogb__mlog__is_fix=False).order_by("-update_time").first()
ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr, if mlogbw:
mlog:Mlog = mlogbw.mlogb.mlog
data[f"{mgroup_name}_批次号"] = mlogbw.mlogb.batch
data[f"{mgroup_name}_设备编号"] = mlog.equipment.number if mlog.equipment else None
data[f"{mgroup_name}_操作人"] = mlog.handle_user.name if mlog.handle_user else None
data[f"{mgroup_name}_日期"] = mlog.handle_date.strftime("%Y-%m-%d")
# 日志操作数据
if mlog.oinfo_json:
oinfo_keys = list(mlog.oinfo_json.keys())
oinfo_keys_qs = TestItem.objects.filter(id__in=oinfo_keys)
for item in oinfo_keys_qs:
data[f"{mgroup_name}_操作项_{item.name}"] = mlog.oinfo_json[item.id]
# 子工序操作人和日期
mlogusers = MlogUser.objects.filter(mlog=mlog)
if mlogusers.exists():
datab = mlogusers.values("handle_user__name", "process__name", "handle_date")
for ind, item in enumerate(datab):
data[f"{mgroup_name}_{item['process__name']}_操作人"] = item["handle_user__name"]
data[f"{mgroup_name}_{item['process__name']}_日期"] = item["handle_date"].strftime("%Y-%m-%d")
# 检测数据
ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False,
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False)
for ftestitem in ftestitems:
data[f"{mgroup_name}_检测项_{ftestitem.testitem.name}"] = ftestitem.test_val_json
ftestdefects = FtestDefect.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup, ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False, ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False,
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False) ftest__mlogbw_ftest__mlogb__mlog__is_fix=False)
for ftestitem in ftestitems: for ftestdefect in ftestdefects:
data[f"{mgroup_name}_检测项_{ftestitem.testitem.name}"] = ftestitem.test_val_json data[f"{mgroup_name}_缺陷项_{ftestdefect.defect.name}"] = 1 if ftestdefect.has is True else 0
old_data:dict = wpr.data
ftestdefects = FtestDefect.objects.filter(ftest__mlogbw_ftest__wpr=wpr, if old_data:
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup, for item in list(old_data.keys()):
ftest__mlogbw_ftest__mlogb__mlog__submit_time__isnull=False, if f'{mgroup_name}_' in item:
ftest__mlogbw_ftest__mlogb__mlog__is_fix=False) del old_data[item]
for ftestdefect in ftestdefects: old_data.update(data)
data[f"{mgroup_name}_缺陷项_{ftestdefect.defect.name}"] = 1 if ftestdefect.has is True else 0 wpr.data = old_data
wpr.save(update_fields=["data"])
old_data:dict = wpr.data
if old_data:
for item in list(old_data.keys()):
if f'{mgroup_name}_' in item:
del old_data[item]
old_data.update(data)
wpr.data = old_data
wpr.save(update_fields=["data"])

View File

@ -1049,6 +1049,11 @@ class MlogbwStartTestSerializer(serializers.Serializer):
test_equip=test_equip test_equip=test_equip
) )
class MlogbOutPatchUpdateSerializer(CustomModelSerializer):
class Meta:
model = Mlogb
fields = ["batch"]
class MlogbOutUpdateSerializer(CustomModelSerializer): class MlogbOutUpdateSerializer(CustomModelSerializer):
mlogbdefect = MlogbDefectSerializer(many=True, required=False) mlogbdefect = MlogbDefectSerializer(many=True, required=False)
count_json = CountJsonSerializer(required=False, many=True) count_json = CountJsonSerializer(required=False, many=True)
@ -1256,12 +1261,18 @@ class HandoverSerializer(CustomModelSerializer):
next_mat = new_wm.material next_mat = new_wm.material
next_state = new_wm.state next_state = new_wm.state
next_defect = new_wm.defect next_defect = new_wm.defect
deptOrmgroupId = None
for ind, item in enumerate(attrs['handoverb']): for ind, item in enumerate(attrs['handoverb']):
if item["count"] > 0: if item["count"] > 0:
pass pass
else: else:
raise ParseError(f'{ind+1}行-交接数量必须大于0') raise ParseError(f'{ind+1}行-交接数量必须大于0')
wm = item["wm"] wm: WMaterial = item["wm"]
current_mdept_id = wm.belong_dept_or_mgroup_id
if deptOrmgroupId is None:
deptOrmgroupId = current_mdept_id
elif deptOrmgroupId != current_mdept_id:
raise ParseError(f'{ind+1}行-交接物料所属工段/车间不一致')
if mtype == Handover.H_MERGE: if mtype == Handover.H_MERGE:
if next_mat is None: if next_mat is None:
next_mat = wm.material next_mat = wm.material

View File

@ -22,7 +22,7 @@ from ..qm.models import Defect, Ftest
from django.db.models import Count, Q from django.db.models import Count, Q
from apps.utils.tasks import ctask_run from apps.utils.tasks import ctask_run
from apps.mtm.models import Process from apps.mtm.models import Process
from apps.utils.lock import lock_model_record_d_func from django.db.models import F
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
@ -150,11 +150,12 @@ def get_pcoal_heat(year_s: int, month_s: int, day_s: int):
myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True) myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True)
return 25000 return 25000
# @lock_model_record_d_func(Mlog)
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
""" """
生产日志提交后需要执行的操作 生产日志提交后需要执行的操作
""" """
mlog = Mlog.objects.select_for_update().get(id=mlog.id)
if mlog.work_start_time and mlog.work_start_time > timezone.now(): if mlog.work_start_time and mlog.work_start_time > timezone.now():
raise ParseError('操作开始时间不能晚于当前时间') raise ParseError('操作开始时间不能晚于当前时间')
if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time: if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time:
@ -172,13 +173,15 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mgroup = mlog.mgroup mgroup = mlog.mgroup
process = mgroup.process process = mgroup.process
into_wm_mgroup = process.into_wm_mgroup stored_mgroup = process.into_wm_mgroup
need_store_notok = process.store_notok stored_notok = process.store_notok
belong_dept = mgroup.belong_dept belong_dept = mgroup.belong_dept
material_out: Material = mlog.material_out material_out: Material = mlog.material_out
material_in: Material = mlog.material_in material_in: Material = mlog.material_in
supplier = mlog.supplier # 外协 supplier = mlog.supplier # 外协
is_fix = mlog.is_fix is_fix = mlog.is_fix
if is_fix: # 如果是返工,直接放到工段下
stored_mgroup = True
m_ins_list = [] m_ins_list = []
m_ins_bl_list = [] m_ins_bl_list = []
@ -221,21 +224,21 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 需要判断领用数是否合理 # 需要判断领用数是否合理
# 优先使用工段库存 # 优先使用工段库存
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in: if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in:
wm_qs = WMaterial.objects.filter(id=mlog_or_b.wm_in.id) wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id)
else: else:
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK) wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK)
if not wm_qs.exists(): if not wm_qs.exists():
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma,
belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK) belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK)
count_x = wm_qs.count() count_x = wm_qs.count()
if count_x == 1: if count_x == 1:
wm = wm_qs.first() wm = WMaterial.objects.select_for_update().get(id=wm_qs.first().id)
elif count_x == 0: elif count_x == 0:
raise ParseError( raise ParseError(
f'{str(mi_ma)}-{mi_batch}-批次库存不存在!') f'{str(mi_ma)}-{mi_batch}-批次库存不存在!')
else: else:
raise ParseError( raise ParseError(
f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!') f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!')
if mi_count > wm.count: if mi_count > wm.count:
raise ParseError( raise ParseError(
@ -252,13 +255,13 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
Wpr.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest) Wpr.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest)
# 针对加工前不良的暂时额外处理 # 针对加工前不良的暂时额外处理
if need_store_notok: if stored_notok:
for item in m_ins_bl_list: for item in m_ins_bl_list:
material, batch, count, defect, mi_ = item material, batch, count, defect, mi_ = item
if count <= 0: if count <= 0:
raise ParseError('存在非正数!') raise ParseError('存在非正数!')
lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'defect': defect, 'state': WMaterial.WM_NOTOK} lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'defect': defect, 'state': WMaterial.WM_NOTOK}
wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={"belong_dept": belong_dept}) wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
wm.count = wm.count + count wm.count = wm.count + count
if is_create: if is_create:
wm.create_by = user wm.create_by = user
@ -275,12 +278,10 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False) mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
stored_mgroup = into_wm_mgroup
stored_notok = need_store_notok
if mlogb_out_qs.exists(): if mlogb_out_qs.exists():
mlogb_out_qs = mlogb_out_qs.filter(need_inout=True) mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()] m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
if need_store_notok: if stored_notok:
for item in mlogb_out_qs: for item in mlogb_out_qs:
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item) mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
if item.qct is not None or mbd_qs.exists(): if item.qct is not None or mbd_qs.exists():
@ -309,7 +310,6 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
if 'count_n_' in f.name and getattr(item, f.name) > 0: if 'count_n_' in f.name and getattr(item, f.name) > 0:
notok_sign = f.name.replace('count_n_', '') notok_sign = f.name.replace('count_n_', '')
m_outs_list.append( (item.material_out, item.batch if item.batch else mlog.batch, getattr(item, f.name), mlog.count_real_eweight, notok_sign, item)) m_outs_list.append( (item.material_out, item.batch if item.batch else mlog.batch, getattr(item, f.name), mlog.count_real_eweight, notok_sign, item))
stored_notok = True
# 这里有一个漏洞在产出物为兄弟件的情况下不合格品的数量是记录在mlog上的 # 这里有一个漏洞在产出物为兄弟件的情况下不合格品的数量是记录在mlog上的
# 而不是mlogb上以上的额外处理就没有效果了, 不过光子不记录不合格品 # 而不是mlogb上以上的额外处理就没有效果了, 不过光子不记录不合格品
else: else:
@ -339,12 +339,12 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
lookup['defect'] = notok_sign_or_defect lookup['defect'] = notok_sign_or_defect
elif notok_sign_or_defect is not None: elif notok_sign_or_defect is not None:
lookup['notok_sign'] = notok_sign_or_defect lookup['notok_sign'] = notok_sign_or_defect
if into_wm_mgroup: if stored_mgroup:
lookup['mgroup'] = mgroup lookup['mgroup'] = mgroup
else: else:
lookup['belong_dept'] = belong_dept lookup['belong_dept'] = belong_dept
wm, is_create2 = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
wm.count = wm.count + mo_count wm.count = wm.count + mo_count
wm.count_eweight = mo_count_eweight wm.count_eweight = mo_count_eweight
wm.update_by = user wm.update_by = user
@ -402,16 +402,17 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup) ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup)
# 触发单个统计 # 触发单个统计
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True)) wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True))
if wprIds: if wprIds:
ana_wpr_thread(wprIds, mlog.mgroup) ana_wpr_thread(wprIds, mlog.mgroup)
# @lock_model_record_d_func(Mlog)
def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
"""日志撤回 """日志撤回
""" """
# if mlog.submit_time is None: # if mlog.submit_time is None:
# return # return
mlog = Mlog.objects.select_for_update().get(id=mlog.id)
if now is None: if now is None:
now = timezone.now() now = timezone.now()
@ -507,6 +508,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
else: else:
raise ParseError( raise ParseError(
f'{str(mo_ma)}-{mo_batch}-存在多个相同批次!') f'{str(mo_ma)}-{mo_batch}-存在多个相同批次!')
wm = WMaterial.objects.select_for_update().get(id=wm.id)
wm.count = wm.count - mo_count wm.count = wm.count - mo_count
if wm.count < 0: if wm.count < 0:
raise ParseError(f'{wm.batch} 车间库存不足, 产物无法回退') raise ParseError(f'{wm.batch} 车间库存不足, 产物无法回退')
@ -529,7 +532,6 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 再生成消耗 # 再生成消耗
m_ins_list = [] m_ins_list = []
m_ins_bl_list = [] m_ins_bl_list = []
into_wm_mgroup = process.into_wm_mgroup
m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False) m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False)
if m_ins.exists(): if m_ins.exists():
m_ins = m_ins.filter(need_inout=True) m_ins = m_ins.filter(need_inout=True)
@ -549,17 +551,17 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
if mi_count <= 0: if mi_count <= 0:
raise ParseError('存在非正数!') raise ParseError('存在非正数!')
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in: if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in:
wm = WMaterial.objects.get(id=mlog_or_b.wm_in.id) wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id)
else: else:
# 针对光子的情况实际上必须需要wm_in # 针对光子的情况实际上必须需要wm_in
lookup = {'batch': mi_batch, 'material': mi_ma, 'mgroup': None, 'state': WMaterial.WM_OK} lookup = {'batch': mi_batch, 'material': mi_ma, 'mgroup': None, 'state': WMaterial.WM_OK}
if into_wm_mgroup: if stored_mgroup:
# 退回到本工段 # 退回到本工段
lookup['mgroup'] = mgroup lookup['mgroup'] = mgroup
else: else:
lookup['belong_dept'] = belong_dept lookup['belong_dept'] = belong_dept
wm, _ = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
wm.count = wm.count + mi_count wm.count = wm.count + mi_count
wm.update_by = user wm.update_by = user
wm.save() wm.save()
@ -581,7 +583,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
lookup['mgroup'] = mgroup lookup['mgroup'] = mgroup
else: else:
lookup['belong_dept'] = belong_dept lookup['belong_dept'] = belong_dept
wm, is_create = WMaterial.objects.get_or_create(**lookup, defaults={**lookup, "belong_dept": belong_dept}) wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
wm.count = wm.count - count wm.count = wm.count - count
if wm.count < 0: if wm.count < 0:
raise ParseError('加工前不良数量大于库存量') raise ParseError('加工前不良数量大于库存量')
@ -621,7 +623,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
ana_batch_thread(xbatches, mgroup=mlog.mgroup) ana_batch_thread(xbatches, mgroup=mlog.mgroup)
# 触发单个统计 # 触发单个统计
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True)) wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True))
if wprIds: if wprIds:
ana_wpr_thread(wprIds, mlog.mgroup) ana_wpr_thread(wprIds, mlog.mgroup)
@ -698,11 +700,15 @@ def update_mtask(mtask: Mtask, fill_way: int = 10):
# utask.state = Utask.UTASK_SUBMIT # utask.state = Utask.UTASK_SUBMIT
utask.save() utask.save()
@lock_model_record_d_func(Handover)
def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]): def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]):
""" """
交接提交后需要执行的操作 交接提交后需要执行的操作
""" """
handover = (
Handover.objects
.select_for_update()
.get(pk=handover.pk)
)
if handover.submit_time is not None: if handover.submit_time is not None:
return return
now = timezone.now() now = timezone.now()
@ -746,7 +752,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
wmId, xcount, handover_or_b = item wmId, xcount, handover_or_b = item
if xcount <= 0: if xcount <= 0:
raise ParseError("存在非正数!") raise ParseError("存在非正数!")
wm_from = WMaterial.objects.get(id=wmId) wm_from = (
WMaterial.objects
.select_for_update()
.get(id=wmId)
)
mids.append(wm_from.material.id) mids.append(wm_from.material.id)
# 合并为新批 # 合并为新批
@ -770,25 +780,17 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
batch = wm_from.batch batch = wm_from.batch
batches.append(batch) batches.append(batch)
if wm_from is None: WMaterial.decrease(wm_id=wm_from.id, user=user, count=xcount)
raise ParseError(f'{wm_from.batch} 找不到车间库存')
count_x = wm_from.count - xcount
if count_x < 0:
raise ParseError(f'{wm_from.batch} 车间库存不足!')
else:
wm_from.count = count_x
wm_from.save()
if need_add: if need_add:
# 开始变动 # 开始变动
if handover.type == Handover.H_NORMAL: if handover.type == Handover.H_NORMAL:
if mtype == Handover.H_MERGE and handover.new_wm: if mtype == Handover.H_MERGE and handover.new_wm:
wm_to = handover.new_wm wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
if wm_to.state != WMaterial.WM_OK or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect: if wm_to.state != wm_from.state or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
raise ParseError("正常合并到的车间库存状态或物料异常") raise ParseError("正常合并到的车间库存状态或物料异常")
else: else:
wm_to, _ = WMaterial.objects.get_or_create( wm_to, _ = WMaterial.locked_get_or_create(
batch=batch, batch=batch,
material=material, material=material,
mgroup=recive_mgroup, mgroup=recive_mgroup,
@ -808,11 +810,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
recive_mgroup = handover.recive_mgroup recive_mgroup = handover.recive_mgroup
wm_state = WMaterial.WM_REPAIR wm_state = WMaterial.WM_REPAIR
if mtype == Handover.H_MERGE and handover.new_wm: if mtype == Handover.H_MERGE and handover.new_wm:
wm_to = handover.new_wm wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect: if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
raise ParseError("返修合并到的车间库存状态或物料异常") raise ParseError("返修合并到的车间库存状态或物料异常")
elif recive_mgroup: elif recive_mgroup:
wm_to, _ = WMaterial.objects.get_or_create( wm_to, _ = WMaterial.locked_get_or_create(
batch=batch, batch=batch,
material=material, material=material,
mgroup=recive_mgroup, mgroup=recive_mgroup,
@ -830,28 +832,13 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
) )
else: else:
raise ParseError("返工交接必须指定接收工段") raise ParseError("返工交接必须指定接收工段")
elif handover.type == Handover.H_TEST:
raise ParseError("检验交接已废弃")
wm_to, _ = WMaterial.objects.get_or_create(
batch=batch,
material=material,
mgroup=recive_mgroup,
state=WMaterial.WM_TEST,
belong_dept=recive_dept,
defaults={
"count_xtest": 0,
"batch_ofrom": wm_from.batch_ofrom,
"material_ofrom": wm_from.material_ofrom,
"create_by": user
},
)
elif handover.type == Handover.H_SCRAP: elif handover.type == Handover.H_SCRAP:
if mtype == Handover.H_MERGE and handover.new_wm: if mtype == Handover.H_MERGE and handover.new_wm:
wm_to = handover.new_wm wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect: if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
raise ParseError("报废合并到的车间库存状态或物料异常") raise ParseError("报废合并到的车间库存状态或物料异常")
elif recive_mgroup: elif recive_mgroup:
wm_to, _ = WMaterial.objects.get_or_create( wm_to, _ = WMaterial.locked_get_or_create(
batch=batch, batch=batch,
material=material, material=material,
mgroup=recive_mgroup, mgroup=recive_mgroup,
@ -870,11 +857,11 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
raise ParseError("不支持非工段报废") raise ParseError("不支持非工段报废")
elif handover.type == Handover.H_CHANGE: elif handover.type == Handover.H_CHANGE:
if mtype == Handover.H_MERGE and handover.new_wm: if mtype == Handover.H_MERGE and handover.new_wm:
wm_to = handover.new_wm wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed: if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed:
raise ParseError("改版合并到的车间库存状态或物料异常") raise ParseError("改版合并到的车间库存状态或物料异常")
elif handover.recive_mgroup: elif handover.recive_mgroup:
wm_to, _ = WMaterial.objects.get_or_create( wm_to, _ = WMaterial.locked_get_or_create(
batch=batch, batch=batch,
material=handover.material_changed, material=handover.material_changed,
state=handover.state_changed, state=handover.state_changed,
@ -897,9 +884,9 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
if wm_from and wm_from.state != WMaterial.WM_OK: if wm_from and wm_from.state != WMaterial.WM_OK:
raise ParseError("仅合格品支持退回") raise ParseError("仅合格品支持退回")
if mtype == Handover.H_MERGE and handover.new_wm: if mtype == Handover.H_MERGE and handover.new_wm:
wm_to = handover.new_wm wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
else: else:
wm_to, _ = WMaterial.objects.get_or_create( wm_to, _ = WMaterial.locked_get_or_create(
batch=batch, batch=batch,
material=material, material=material,
mgroup=recive_mgroup, mgroup=recive_mgroup,
@ -917,9 +904,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
else: else:
raise ParseError("不支持该交接类型") raise ParseError("不支持该交接类型")
wm_to.count = wm_to.count + xcount WMaterial.increase(wm_id=wm_to.id, user=user,count=xcount, count_eweight=handover.count_eweight if handover.count_eweight else None)
wm_to.count_eweight = handover.count_eweight # 这行代码有隐患
wm_to.save()
handover_or_b.wm_to = wm_to handover_or_b.wm_to = wm_to
handover_or_b.save() handover_or_b.save()
if material.tracking == Material.MA_TRACKING_SINGLE: if material.tracking == Material.MA_TRACKING_SINGLE:
@ -934,7 +919,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
for item in handoverbws: for item in handoverbws:
wpr:Wpr = item.wpr wpr:Wpr = item.wpr
Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb) Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
if wm_to.count != Wpr.objects.filter(wm=wm_to).count(): db_count = WMaterial.objects.filter(id=wm_to.id).values_list("count", flat=True).get()
if db_count != Wpr.objects.filter(wm=wm_to).count():
raise ParseError("交接与明细数量不一致2,操作失败") raise ParseError("交接与明细数量不一致2,操作失败")
handover.submit_user = user handover.submit_user = user
@ -945,14 +931,14 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
ana_batch_thread(xbatchs=batches) ana_batch_thread(xbatchs=batches)
@lock_model_record_d_func(Handover)
def handover_revert(handover:Handover, handler:User=None): def handover_revert(handover:Handover, handler:User=None):
handover = Handover.objects.select_for_update().get(id=handover.id)
if handover.submit_time is None: if handover.submit_time is None:
raise ParseError('该交接单未提交!') raise ParseError('该交接单未提交!')
ticket:Ticket = handover.ticket ticket:Ticket = handover.ticket
if ticket: if ticket:
# 首先把ticket改回开始状态 # 首先把ticket改回开始状态
WfService.retreat(ticket=ticket, suggestion="交接单", handler=handler, next_handler=handover.create_by) WfService.retreat(ticket=ticket, suggestion="交接单", handler=handler, next_handler=handover.create_by)
mids = [] mids = []
# handover_type = handover.type # handover_type = handover.type
# handover_mtype = handover.mtype # handover_mtype = handover.mtype
@ -973,21 +959,18 @@ def handover_revert(handover:Handover, handler:User=None):
wm = item.wm wm = item.wm
wm_to = item.wm_to wm_to = item.wm_to
if wm is None or wm_to is None: if wm is None or wm_to is None:
raise ParseError('该交接单不支持撤2!') raise ParseError('该交接单不支持撤2!')
if wm == wm_to: if wm == wm_to:
# 此时是自己交给自己,不需要做任何操作 # 此时是自己交给自己,不需要做任何操作
pass pass
else: else:
wm.count = wm.count + item.count WMaterial.increase(wm_id=wm.id, user=handler, count=item.count)
wm.save() WMaterial.decrease(wm_id=wm_to.id, user=handler, count=item.count)
wm_to.count = wm_to.count - item.count
if wm_to.count < 0:
raise ParseError('库存不足无法撤回!')
wm_to.save()
if material.tracking == Material.MA_TRACKING_SINGLE: if material.tracking == Material.MA_TRACKING_SINGLE:
handoverbws = Handoverbw.objects.filter(handoverb=item) handoverbws = Handoverbw.objects.filter(handoverb=item)
if handoverbws.count() != item.count: if handoverbws.count() != item.count:
raise ParseError("交接与明细数量不一致,操作失败") raise ParseError("交接与明细数量不一致,操作失败")
wm = WMaterial.objects.get(id=wm.id)
for item in handoverbws: for item in handoverbws:
wpr:Wpr = item.wpr wpr:Wpr = item.wpr
Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False) Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False)
@ -1064,6 +1047,7 @@ def get_batch_dag(batch_number: str, method="full"):
} }
if method == "full": if method == "full":
raise ParseError("不支持获取全局关系链条")
# 完整DAG模式 - 收集所有相关批次和边(原逻辑) # 完整DAG模式 - 收集所有相关批次和边(原逻辑)
nodes_set = {batch_ins.id} nodes_set = {batch_ins.id}
edges = [] edges = []
@ -1120,33 +1104,33 @@ def get_batch_dag(batch_number: str, method="full"):
}) })
# 查询作为source的其他关系 # 查询作为source的其他关系
leftLogs = BatchLog.objects.filter(source_id__in=left_source_ids).exclude(id__in=exist_log_ids) # leftLogs = BatchLog.objects.filter(source_id__in=left_source_ids).exclude(id__in=exist_log_ids)
for log in leftLogs: # for log in leftLogs:
source = log.source.id # source = log.source.id
target = log.target.id # target = log.target.id
nodes_set.add(log.target.id) # nodes_set.add(log.target.id)
edges.append({ # edges.append({
'id': log.id, # 'id': log.id,
'source': source, # 'source': source,
'target': target, # 'target': target,
"handover": log.handover.id if log.handover else None, # "handover": log.handover.id if log.handover else None,
"mlog": log.mlog.id if log.mlog else None, # "mlog": log.mlog.id if log.mlog else None,
'label': r_dict.get(log.relation_type, ""), # 'label': r_dict.get(log.relation_type, ""),
}) # })
rightLogs = BatchLog.objects.filter(target_id__in=right_target_ids).exclude(id__in=exist_log_ids) # rightLogs = BatchLog.objects.filter(target_id__in=right_target_ids).exclude(id__in=exist_log_ids)
for log in rightLogs: # for log in rightLogs:
source = log.source.id # source = log.source.id
target = log.target.id # target = log.target.id
nodes_set.add(log.source.id) # nodes_set.add(log.source.id)
edges.append({ # edges.append({
'id': log.id, # 'id': log.id,
'source': source, # 'source': source,
'target': target, # 'target': target,
"handover": log.handover.id if log.handover else None, # "handover": log.handover.id if log.handover else None,
"mlog": log.mlog.id if log.mlog else None, # "mlog": log.mlog.id if log.mlog else None,
'label': r_dict.get(log.relation_type, ""), # 'label': r_dict.get(log.relation_type, ""),
}) # })
else: else:
raise ParseError("不支持的查询方法,请使用'full''direct'") raise ParseError("不支持的查询方法,请使用'full''direct'")

View File

@ -20,9 +20,9 @@ router.register('sflogexp', SfLogExpViewSet, basename='sflogexp')
router.register('wmaterial', WMaterialViewSet, basename='wmaterial') router.register('wmaterial', WMaterialViewSet, basename='wmaterial')
router.register('fmlog', FmlogViewSet, basename='fmlog') router.register('fmlog', FmlogViewSet, basename='fmlog')
router.register('mlog', MlogViewSet, basename='mlog') router.register('mlog', MlogViewSet, basename='mlog')
router.register('mlogb', MlogbViewSet) router.register('mlogb', MlogbViewSet, basename='mlogb')
router.register('mlogb/in', MlogbInViewSet) router.register('mlogb/in', MlogbInViewSet, basename='mlogb_in')
router.register('mlogb/out', MlogbOutViewSet) router.register('mlogb/out', MlogbOutViewSet, basename='mlogb_out')
router.register('handover', HandoverViewSet, basename='handover') router.register('handover', HandoverViewSet, basename='handover')
router.register('attlog', AttlogViewSet, basename='attlog') router.register('attlog', AttlogViewSet, basename='attlog')
router.register('otherlog', OtherLogViewSet, basename='otherlog') router.register('otherlog', OtherLogViewSet, basename='otherlog')

View File

@ -50,7 +50,8 @@ from .serializers import (
MlogQuickSerializer, MlogQuickSerializer,
MlogbwStartTestSerializer, MlogbwStartTestSerializer,
HandoverListSerializer, HandoverListSerializer,
BatchChangeSerializer BatchChangeSerializer,
MlogbOutPatchUpdateSerializer
) )
from .services import mlog_submit, handover_submit, mlog_revert, get_batch_dag, handover_revert from .services import mlog_submit, handover_submit, mlog_revert, get_batch_dag, handover_revert
from apps.wpm.services import mlog_submit_validate, generate_new_batch from apps.wpm.services import mlog_submit_validate, generate_new_batch
@ -446,9 +447,9 @@ class MlogViewSet(CustomModelViewSet):
raise ParseError("该日志存在审批!") raise ParseError("该日志存在审批!")
user = request.user user = request.user
if ins.submit_time is None: if ins.submit_time is None:
raise ParseError("日志未提交不可撤") raise ParseError("日志未提交不可撤")
if user != ins.submit_user: if user != ins.submit_user:
raise ParseError("非提交人不可撤!") raise ParseError("非提交人不可撤!")
now = timezone.now() now = timezone.now()
mlog_revert(ins, user, now) mlog_revert(ins, user, now)
return Response(MlogSerializer(instance=ins).data) return Response(MlogSerializer(instance=ins).data)
@ -836,7 +837,7 @@ class MlogbInViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyMode
"batch": mlogbin.batch, "batch": mlogbin.batch,
"batch_ofrom": wm_in.batch_ofrom, "batch_ofrom": wm_in.batch_ofrom,
"material_ofrom": wm_in.material_ofrom, "material_ofrom": wm_in.material_ofrom,
"qct": Qct.get(material_out, "process", "out"), "qct": Qct.get(material_out, "fix" if mlog.is_fix else "process", "out"),
} }
if mtype == Process.PRO_DIV and material_in.tracking == Material.MA_TRACKING_SINGLE: if mtype == Process.PRO_DIV and material_in.tracking == Material.MA_TRACKING_SINGLE:
pass pass
@ -1020,18 +1021,22 @@ class MlogbInViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyMode
class MlogbOutViewSet(BulkUpdateModelMixin, CustomGenericViewSet): class MlogbOutViewSet(BulkUpdateModelMixin, CustomGenericViewSet):
perms_map = {"put": "mlog.update"} perms_map = {"put": "mlog.update", "patch": "mlog.update"}
queryset = Mlogb.objects.filter(material_out__isnull=False) queryset = Mlogb.objects.filter(material_out__isnull=False)
serializer_class = MlogbOutUpdateSerializer serializer_class = MlogbOutUpdateSerializer
partial_update_serializer_class = MlogbOutPatchUpdateSerializer
def perform_update(self, serializer): def perform_update(self, serializer):
ins: Mlogb = serializer.instance if self.request.method == "PATCH":
mlog = MlogViewSet.lock_and_check_can_update(ins.mlog) serializer.save()
material_out = serializer.validated_data.get("material_out") else:
if material_out and material_out.tracking == Material.MA_TRACKING_SINGLE: ins: Mlogb = serializer.instance
raise ParseError("单件产品不支持直接修改") mlog = MlogViewSet.lock_and_check_can_update(ins.mlog)
ins: Mlogb = serializer.save() material_out = serializer.validated_data.get("material_out")
mlog.cal_mlog_count_from_mlogb() if material_out and material_out.tracking == Material.MA_TRACKING_SINGLE:
raise ParseError("单件产品不支持直接修改")
ins: Mlogb = serializer.save()
mlog.cal_mlog_count_from_mlogb()
class FmlogViewSet(CustomModelViewSet): class FmlogViewSet(CustomModelViewSet):
@ -1072,6 +1077,16 @@ class BatchStViewSet(CustomListModelMixin, ComplexQueryMixin, CustomGenericViewS
ordering = ["batch"] ordering = ["batch"]
filterset_class = BatchStFilter filterset_class = BatchStFilter
def add_info_for_list(self, data):
if (self.request.query_params.get("with_source_near", None) == "yes" or
self.request.data.get("with_source_near", None) == "yes"):
batchstIds = [ins["id"] for ins in data]
batchlog_qs = BatchLog.objects.filter(target__id__in=batchstIds).values("id", "source", "target")
source_data = BatchStSerializer(instance=BatchSt.objects.filter(id__in=[ins["source"] for ins in batchlog_qs]), many=True).data
source_data_dict = {ins["id"]: ins for ins in source_data}
for item in data:
item["source_near"] = [source_data_dict[ins["source"]] for ins in batchlog_qs if ins["target"] == item["id"]]
return data
class MlogbwViewSet(CustomModelViewSet): class MlogbwViewSet(CustomModelViewSet):
perms_map = {"get": "*", "post": "mlog.update", "put": "mlog.update", "delete": "mlog.update", "patch": "mlog.update"} perms_map = {"get": "*", "post": "mlog.update", "put": "mlog.update", "delete": "mlog.update", "patch": "mlog.update"}

View File

@ -17,6 +17,8 @@ class WprFilter(filters.FilterSet):
"mb": ["exact", "isnull"], "mb": ["exact", "isnull"],
"wm": ["exact", "isnull"], "wm": ["exact", "isnull"],
"material__process": ["exact"], "material__process": ["exact"],
"material__name": ["exact", "contains"],
"wpr_from": ["exact", "isnull"],
"state": ["exact"], "state": ["exact"],
"defects": ["exact"], "defects": ["exact"],
"number": ["exact"] "number": ["exact"]

View File

@ -1,6 +1,6 @@
from rest_framework.decorators import action from rest_framework.decorators import action
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
from apps.utils.mixins import CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin from apps.utils.mixins import CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin
from apps.wpmw.models import Wpr, WprDefect from apps.wpmw.models import Wpr, WprDefect
from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer
@ -13,7 +13,7 @@ from apps.utils.sql import query_one_dict
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet): class WprViewSet(CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet):
"""动态产品 """动态产品
动态产品 动态产品
@ -30,10 +30,20 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
ordering_fields = ["number", "create_time", "update_time"] ordering_fields = ["number", "create_time", "update_time"]
search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"] search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"]
annotate_dict = { annotate_dict = {
"number_prefix": RawSQL("regexp_replace(number, '(\\d+)$', '')", []), "number_prefix": RawSQL("regexp_replace(wpmw_wpr.number, '(\\d+)$', '')", []),
"number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []), "number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(wpmw_wpr.number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []),
} }
def add_info_for_list(self, data):
parent_ids = [item["wpr_from"] for item in data if item.get("wpr_from", False)]
if parent_ids:
parent_data = Wpr.objects.filter(id__in=parent_ids).values("id", "number", "data")
parent_map = {item["id"]: item for item in parent_data}
for item in data:
if item["wpr_from"]:
item["wpr_from_"] = parent_map[item["wpr_from"]]
return data
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
qs = super().filter_queryset(queryset) qs = super().filter_queryset(queryset)
if "mb__isnull" in self.request.query_params or "wm__isnull" in self.request.query_params: if "mb__isnull" in self.request.query_params or "wm__isnull" in self.request.query_params:
@ -92,11 +102,20 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
# 使用原始sql # 使用原始sql
query = """ query = """
SELECT id, number_out FROM wpmw_wpr SELECT id, number_out FROM wpmw_wpr
WHERE number_out ~ %s order by number_out desc limit 1 WHERE number_out LIKE %s
AND translate(
substring(number_out FROM LENGTH(%s) + 2),
'0123456789',
''
) = ''
order by number_out desc limit 1
""" """
pattern = f"^{prefix}[0-9]+$" params = (
f"{prefix}-%",
prefix
)
number_outs = [] number_outs = []
wpr_qs_last = query_one_dict(query, [pattern]) wpr_qs_last = query_one_dict(query, [params])
if wpr_qs_last: if wpr_qs_last:
number_outs.append(wpr_qs_last["number_out"]) number_outs.append(wpr_qs_last["number_out"])
# 查找未出库的记录 # 查找未出库的记录
@ -106,9 +125,15 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
query2 = """ query2 = """
select mioitemw.id, mioitemw.number_out from inm_mioitemw mioitemw left join inm_mioitem mioitem on mioitem.id = mioitemw.mioitem_id select mioitemw.id, mioitemw.number_out from inm_mioitemw mioitemw left join inm_mioitem mioitem on mioitem.id = mioitemw.mioitem_id
left join inm_mio mio on mio.id = mioitem.mio_id left join inm_mio mio on mio.id = mioitem.mio_id
where mio.submit_time is null and mioitemw.number_out ~ %s order by mioitemw.number_out desc limit 1 where mio.submit_time is null and mioitemw.number_out LIKE %s
AND translate(
substring(mioitemw.number_out FROM LENGTH(%s) + 2),
'0123456789',
''
) = ''
order by mioitemw.number_out desc limit 1
""" """
mioitemw_last = query_one_dict(query2, [pattern]) mioitemw_last = query_one_dict(query2, [params])
if mioitemw_last: if mioitemw_last:
number_outs.append(mioitemw_last["number_out"]) number_outs.append(mioitemw_last["number_out"])
if number_outs: if number_outs:

View File

@ -1,3 +1,23 @@
## 3.0.2026010716
- feat: 新增功能
- get_shift需要报错 [caoqianming]
- 添加rem模块 [caoqianming]
- 捕获除0异常 [caoqianming]
- 按需求修改光芯批次统计分析 [caoqianming]
- 批次统计数据支持返回source_near [caoqianming]
- 光芯OA 审批系统新增报价单审核 [TianyangZhang]
- get_batch_dag还是只返回直接前后级别 [caoqianming]
- mlogbbpatch修改批次号 [caoqianming]
- 出入库记录返回子表部分信息 [caoqianming]
- 统一撤回和撤销的表述 [caoqianming]
- 车间库存检验支持撤回 [caoqianming]
- wpr添加material_name查询条件 [caoqianming]
- 优化mlog_submit 返工后产品放在本工段下 [caoqianming]
- mlogbin qct 可依据fix选择 [caoqianming]
- base dept filter支持parent isnull查询 [caoqianming]
- fix: 问题修复
- wpr list annotate明确number指向 [caoqianming]
- 正常交接支持new_wm且支持不合格品 [caoqianming]
## 3.0.2025122514 ## 3.0.2025122514
- feat: 新增功能 - feat: 新增功能
- mlogbw patch权限 [caoqianming] - mlogbw patch权限 [caoqianming]

View File

@ -1,37 +1,82 @@
celery==5.2.3 # =======================
Django==3.2.12 # Core
django-celery-beat==2.3.0 # =======================
django-celery-results==2.4.0 Django==4.2.27
django-cors-headers==3.11.0
django-filter==21.1 djangorestframework==3.16.1
djangorestframework==3.13.1 django-filter==23.5
djangorestframework-simplejwt==5.1.0 django-cors-headers==4.9.0
drf-yasg==1.21.7
psutil==5.9.0 djangorestframework-simplejwt==5.5.1
pillow==9.0.1
opencv-python==4.5.5.62
redis==4.4.0
django-redis==5.2.0
user-agents==2.2.0
daphne==4.0.0
channels-redis==4.0.0
django-restql==0.15.2 django-restql==0.15.2
# =======================
# Celery
# =======================
celery==5.6.2
django-celery-beat==2.8.1
django-celery-results==2.6.0
redis==7.1.0
django-redis==6.0.0
cron-descriptor==1.2.35
# =======================
# Channels / ASGI
# =======================
channels==4.3.2
daphne==4.0.0
channels-redis==4.3.0
# =======================
# API Docs
# =======================
drf-yasg==1.21.7
# =======================
# Auth / Utils
# =======================
user-agents==2.2.0
psutil==5.9.0
# =======================
# Media / Image / CV
# =======================
pillow==9.5.0
opencv-python==4.5.5.62
shapely==1.8.3 shapely==1.8.3
aliyun-python-sdk-core==2.13.36
baidu-aip==4.16.6 # =======================
chardet==5.0.0 # Network / RPC
requests==2.28.1 # =======================
requests==2.32.5
grpcio==1.47.0 grpcio==1.47.0
grpcio-tools==1.47.0 grpcio-tools==1.47.0
protobuf==3.20.1 protobuf==3.20.1
pycryptodome==3.15.0
# =======================
# Cloud SDK
# =======================
aliyun-python-sdk-core==2.13.36 aliyun-python-sdk-core==2.13.36
baidu-aip==4.16.6
# =======================
# Crypto
# =======================
pycryptodome==3.15.0
# =======================
# Excel / Docs
# =======================
xlwt==1.3.0 xlwt==1.3.0
openpyxl==3.1.0 openpyxl==3.1.5
cron-descriptor==1.2.35
pymysql==1.0.3
# face-recognition==1.3.0
docxtpl==0.16.7 docxtpl==0.16.7
# =======================
# DB
# =======================
pymysql==1.0.3
# =======================
# IoT / MQTT
# =======================
paho-mqtt==2.0.0 paho-mqtt==2.0.0
# deepface==0.0.79
# edge-tts==6.1.12

View File

@ -8,13 +8,16 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
""" """
import os import os
import django
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
django.setup()
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
from apps.utils.middlewares import TokenAuthMiddleware from apps.utils.middlewares import TokenAuthMiddleware
import apps.ws.routing import apps.ws.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
"http": get_asgi_application(), "http": get_asgi_application(),
"websocket": TokenAuthMiddleware( "websocket": TokenAuthMiddleware(

View File

@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
SYS_NAME = '星途工厂综合管理系统' SYS_NAME = '星途工厂综合管理系统'
SYS_VERSION = '3.0.2025122514' SYS_VERSION = '3.0.2026010716'
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# Application definition # Application definition
@ -88,6 +88,7 @@ INSTALLED_APPS = [
'apps.ofm', 'apps.ofm',
'apps.srm', 'apps.srm',
'apps.asm', 'apps.asm',
'apps.rem'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -78,6 +78,7 @@ urlpatterns = [
path('', include('apps.ofm.urls')), path('', include('apps.ofm.urls')),
path('', include('apps.srm.urls')), path('', include('apps.srm.urls')),
path('', include('apps.asm.urls')), path('', include('apps.asm.urls')),
path('', include('apps.rem.urls')),
# 前端页面入口 # 前端页面入口
path('', TemplateView.as_view(template_name="index.html")), path('', TemplateView.as_view(template_name="index.html")),