diff --git a/apps/ofm/filters.py b/apps/ofm/filters.py index 279d88bd..0caa8c47 100644 --- a/apps/ofm/filters.py +++ b/apps/ofm/filters.py @@ -1,5 +1,5 @@ from django_filters import rest_framework as filters -from apps.ofm.models import MroomBooking, BorrowRecord +from apps.ofm.models import MroomBooking, BorrowRecord, Vehicle from .models import LendingSeal from apps.utils.filters import MyJsonListFilter @@ -15,6 +15,16 @@ class MroomBookingFilterset(filters.FilterSet): "id": ["exact"] } +class VehicleFilterset(filters.FilterSet): + class Meta: + model = Vehicle + fields = { + 'slot_vehicle__vehreg': ['exact', 'in'], + 'slot_vehicle__vdate': ['exact', 'gte', 'lte'], + 'create_by': ['exact'], + "id": ["exact"] + } + class SealFilter(filters.FilterSet): seal = MyJsonListFilter(label='按印章名称查询', field_name="seal") diff --git a/apps/ofm/migrations/0032_auto_20251117_1615.py b/apps/ofm/migrations/0032_auto_20251117_1615.py new file mode 100644 index 00000000..0095bac9 --- /dev/null +++ b/apps/ofm/migrations/0032_auto_20251117_1615.py @@ -0,0 +1,70 @@ +# Generated by Django 3.2.12 on 2025-11-17 08:15 + +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), + ('ofm', '0031_auto_20251106_1608'), + ] + + operations = [ + migrations.CreateModel( + name='VehicleReg', + 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.CharField(max_length=50, verbose_name='车辆名称')), + ('brand', models.CharField(blank=True, max_length=50, null=True, verbose_name='品牌')), + ('plate', models.CharField(max_length=50, verbose_name='车牌号')), + ('km', models.PositiveIntegerField(blank=True, null=True, verbose_name='行驶里程')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehiclereg_create_by', 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='vehiclereg_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='vehicle', + name='end_time', + ), + migrations.RemoveField( + model_name='vehicle', + name='start_time', + ), + migrations.AddField( + model_name='vehicle', + name='reception', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='接待人'), + ), + migrations.CreateModel( + name='VehicleSlot', + 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='删除标记')), + ('vdate', models.DateField(db_index=True, verbose_name='使用日期')), + ('slot', models.PositiveIntegerField(help_text='0-47', verbose_name='时段')), + ('is_inuse', models.BooleanField(default=True, verbose_name='是否占用')), + ('vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_vehicle', to='ofm.vehicle')), + ('vehreg', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_record', to='ofm.vehiclereg')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='vehicle', + name='vehiclereg', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicle_record', to='ofm.vehiclereg'), + ), + ] diff --git a/apps/ofm/models.py b/apps/ofm/models.py index 1c856e6d..c20e8db2 100644 --- a/apps/ofm/models.py +++ b/apps/ofm/models.py @@ -46,10 +46,46 @@ class MroomSlot(BaseModel): is_inuse = models.BooleanField('是否占用', default=True) -# class Seal(BaseModel): -# """TN: 印章类型""" -# name = models.CharField('印章名称', max_length=50, unique=True) +class VehicleReg(CommonADModel): + """TN: 车辆台账""" + name = models.CharField('车辆名称', max_length=50) + brand = models.CharField('品牌', max_length=50, null=True, blank=True) + plate = models.CharField('车牌号', max_length=50) + km = models.PositiveIntegerField('行驶里程', null=True, blank=True) +class Vehicle(CommonBDModel): + """TN: 用车申请""" + vehiclereg = models.ForeignKey(VehicleReg, on_delete=models.CASCADE, related_name="vehicle_record", null=True, blank=True) + location = 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) + start_km = models.PositiveIntegerField('出发公里数') + end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True) + actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False) + is_city = models.BooleanField('是否市内用车', default=True) + reason = models.CharField('用车事由', max_length=100) + reception = models.CharField('接待人', max_length=50, blank=True, null=True) + ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单', + on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False) + def save(self, *args, **kwargs): + if self.end_km: + if self.start_km <= self.end_km: + self.actual_km = self.end_km - self.start_km + with transaction.atomic(): + super().save(*args, **kwargs) + VehicleReg.objects.filter(id=self.vehiclereg.id).update(km=self.end_km) + else: + raise ParseError('归还公里数不能小于出发公里数') + else: + self.actual_km = 0 + return super().save(*args, **kwargs) + +class VehicleSlot(BaseModel): + vehreg = models.ForeignKey(VehicleReg, on_delete=models.CASCADE, related_name="slot_record") + vehicle = models.ForeignKey(Vehicle, on_delete=models.CASCADE, related_name="slot_vehicle") + vdate = models.DateField('使用日期', db_index=True) + slot = models.PositiveIntegerField('时段', help_text='0-47') + is_inuse = models.BooleanField('是否占用', default=True) class LendingSeal(CommonBDModel): """TN: 印章外出用印信息""" @@ -69,32 +105,6 @@ class LendingSeal(CommonBDModel): on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False) note = models.TextField('备注', null=True, blank=True) - -class Vehicle(CommonBDModel): - """TN: 用车申请""" - start_time = models.DateTimeField('出车时间', blank=True, null=True) - end_time = models.DateTimeField('还车时间', blank=True, null=True) - location = 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) - start_km = models.PositiveIntegerField('出发公里数') - end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True) - actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False) - is_city = models.BooleanField('是否市内用车', default=True) - reason = models.CharField('用车事由', max_length=100) - ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单', - on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False) - def save(self, *args, **kwargs): - if self.end_km: - if self.start_km <= self.end_km: - self.actual_km = self.end_km - self.start_km - else: - raise ParseError('归还公里数不能小于出发公里数') - else: - self.actual_km = 0 - return super().save(*args, **kwargs) - - class FileRecord(CommonBDModel): """TN: 档案台账""" name = models.CharField('资料名称', max_length=100) @@ -105,7 +115,6 @@ class FileRecord(CommonBDModel): reciver = models.CharField('接收人(综合办)', max_length=50, null=True, blank=True) remark = models.TextField('备注', max_length=200, null=True, blank=True) - class BorrowRecord(CommonBDModel): """TN: 借阅、复印、查阅记录""" borrow_file = models.ManyToManyField(FileRecord, related_name="borrow_records") diff --git a/apps/ofm/serializers.py b/apps/ofm/serializers.py index 38395d23..3150e559 100644 --- a/apps/ofm/serializers.py +++ b/apps/ofm/serializers.py @@ -1,4 +1,4 @@ -from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity) +from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, VehicleReg, VehicleSlot) from apps.utils.serializers import CustomModelSerializer from rest_framework import serializers from django.db import transaction @@ -57,7 +57,6 @@ class MroomBookingSerializer(CustomModelSerializer): MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True) return booking - class MroomSlotSerializer(CustomModelSerializer): booking_title = serializers.CharField(source='booking.title', read_only=True) class Meta: @@ -65,6 +64,62 @@ class MroomSlotSerializer(CustomModelSerializer): fields = '__all__' +class VehicleRecordSerializer(CustomModelSerializer): + class Meta: + model = VehicleReg + fields = '__all__' + + +class VehicleSerializer(CustomModelSerializer): + vehreg = serializers.PrimaryKeyRelatedField(queryset=VehicleReg.objects.all(), write_only=True, label="车辆信息") + vdate = serializers.DateField(write_only=True, label="预订日期") + slots = serializers.ListField(child=serializers.IntegerField(), write_only=True, label="时段索引") + create_by_name = serializers.CharField(source='create_by.username', read_only=True) + create_by_phone = serializers.CharField(source='create_by.phone', read_only=True) + belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) + ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) + class Meta: + model = Vehicle + fields = '__all__' + read_only_fields = EXCLUDE_FIELDS + ['actual_km'] + extra_kwargs = {'belong_dept': {'required': True}} + + def create(self, validated_data): + vehreg = validated_data.pop('vehreg') + slots = validated_data.pop('slots') + vdate = validated_data.pop('vdate') + vehicle = super().create(validated_data) + VehicleSlot.objects.filter(vehicle=vehicle).delete() + for slot in slots: + if slot < 0 or slot > 47: + raise ParseError("时段索引超出范围") + ms_exists = VehicleSlot.objects.filter(vehreg=vehreg, vdate=vdate, slot=slot, is_inuse=True).exists() + if ms_exists: + raise ParseError("时段已预订,请刷新重选") + VehicleSlot.objects.create(vehicle=vehicle, slot=slot, vdate=vdate, vehreg=vehreg, is_inuse=True) + return vehicle + + def update(self, instance, validated_data): + vehreg = validated_data.pop('vehreg') + slots = validated_data.pop('slots') + vdate = validated_data.pop('vdate') + vehicle = super().update(instance, validated_data) + VehicleSlot.objects.filter(vehicle=vehicle).delete() + for slot in slots: + if slot < 0 or slot > 47: + raise ParseError("时段索引超出范围") + ms_exists = VehicleSlot.objects.filter(vehreg=vehreg, vdate=vdate, slot=slot, is_inuse=True).exists() + if ms_exists: + raise ParseError("时段已预订,请刷新重选") + VehicleSlot.objects.create(vehicle=vehicle, slot=slot, vdate=vdate, vehreg=vehreg, is_inuse=True) + return vehicle + +class VehSlotSerializer(CustomModelSerializer): + veh_name = serializers.CharField(source='vehreg.name', read_only=True) + class Meta: + model = VehicleSlot + fields = '__all__' + class LendingSealSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) @@ -74,16 +129,20 @@ class LendingSealSerializer(CustomModelSerializer): fields = '__all__' read_only_fields = EXCLUDE_FIELDS - -class VehicleSerializer(CustomModelSerializer): - create_by_name = serializers.CharField(source='create_by.name', read_only=True) - belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) - ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) - class Meta: - model = Vehicle - fields = '__all__' - read_only_fields = EXCLUDE_FIELDS + ['actual_km'] +# class VehicleRecordSerializer(CustomModelSerializer): +# class meta: +# model = VehicleReg +# fields = '__all__' +# class VehicleSerializer(CustomModelSerializer): +# create_by_name = serializers.CharField(source='create_by.name', read_only=True) +# belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) +# vehiclerd = serializers.PrimaryKeyRelatedField(queryset=VehicleReg.objects.all(), write_only=True, label="车辆记录") +# ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) +# class Meta: +# model = Vehicle +# fields = '__all__' +# read_only_fields = EXCLUDE_FIELDS + ['actual_km'] class FileRecordSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) diff --git a/apps/ofm/urls.py b/apps/ofm/urls.py index ea494a51..44688de4 100644 --- a/apps/ofm/urls.py +++ b/apps/ofm/urls.py @@ -1,6 +1,7 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet, FileborrowViewSet, PublicityViewSet) +from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, + VehicleRegViewSet, VehicleSlotViewSet, FilerecordViewSet, FileborrowViewSet, PublicityViewSet) API_BASE_URL = 'api/ofm/' HTML_BASE_URL = 'dhtml/ofm/' @@ -12,6 +13,8 @@ router.register('mroomslot', MroomSlotViewSet, basename='mroomslot') # router.register('sealmanage', SealManageViewSet, basename='sealmanage') router.register('lendingseal', LendingSealViewSet, basename='lendingseal') router.register('vehicle', VehicleViewSet, basename='vehicle') +router.register('vsolt', VehicleSlotViewSet, basename='vslot') +router.register('vreg', VehicleRegViewSet, basename='vreg') router.register('filerecord', FilerecordViewSet, basename='filerecord') router.register('fileborrow', FileborrowViewSet, basename='fileborrow') router.register('publicity', PublicityViewSet, basename='publicity') diff --git a/apps/ofm/views.py b/apps/ofm/views.py index 5b3639e4..7d5082a0 100644 --- a/apps/ofm/views.py +++ b/apps/ofm/views.py @@ -1,12 +1,12 @@ from django.shortcuts import render from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet -from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity +from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, VehicleSlot, VehicleReg, FileRecord, BorrowRecord, Publicity from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer, - VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer) + VehicleSerializer, VehicleRecordSerializer, VehSlotSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer) from apps.utils.mixins import CustomListModelMixin from rest_framework.exceptions import ParseError -from apps.ofm.filters import MroomBookingFilterset, SealFilter, BorrowRecordFilter +from apps.ofm.filters import MroomBookingFilterset, SealFilter, BorrowRecordFilter, VehicleFilterset from rest_framework.response import Response class MroomViewSet(CustomModelViewSet): @@ -124,14 +124,105 @@ class LendingSealViewSet(CustomModelViewSet): data_filter = True -class VehicleViewSet(CustomModelViewSet): +class VehicleRegViewSet(CustomModelViewSet): """list: 车辆 车辆 """ + queryset = VehicleReg.objects.all() + serializer_class = VehicleRecordSerializer + ordering = ["-create_time"] + +class VehicleViewSet(CustomModelViewSet): + """list: 会议室预订 + + 会议室预订 + """ queryset = Vehicle.objects.all() serializer_class = VehicleSerializer - ordering = ["-create_time"] + select_related_fields = ["create_by", "ticket", "belong_dept"] + filterset_class = VehicleFilterset + + def add_info_for_list(self, data): + vehicle_ids = [d["id"] for d in data] + slots = VehicleSlot.objects.filter(vehicle__in=vehicle_ids).order_by("vehreg", "vehicle", "vdate", "slot") + vehicle_info = {} + for slot in slots: + vehicle_id = slot.vehicle.id + + if vehicle_id not in vehicle_info: + vehicle_info[vehicle_id] = { + "vdate": slot.vdate.strftime("%Y-%m-%d"), # 格式化日期 + "vehreg": slot.vehreg.id, + "vehreg_name": slot.vehreg.name, # 会议室名称 + "time_ranges": [], # 存储时间段(如 ["8:00-9:00", "10:00-11:30"]) + "current_slots": [], # 临时存储连续的slot(用于合并) + } + + # 检查是否连续(当前slot是否紧接上一个slot) + current_slots = vehicle_info[vehicle_id]["current_slots"] + if not current_slots or slot.slot == current_slots[-1] + 1: + current_slots.append(slot.slot) + else: + # 如果不连续,先把当前连续的slot转换成时间段 + if current_slots: + start_time = self._slot_to_time(current_slots[0]) + end_time = self._slot_to_time(current_slots[-1] + 1) + vehicle_info[vehicle_id]["time_ranges"].append(f"{start_time}-{end_time}") + current_slots.clear() + current_slots.append(slot.slot) + + # 处理最后剩余的连续slot + for info in vehicle_info.values(): + if info["current_slots"]: + start_time = self._slot_to_time(info["current_slots"][0]) + end_time = self._slot_to_time(info["current_slots"][-1] + 1) + info["time_ranges"].append(f"{start_time}-{end_time}") + info["slots"] = info.pop("current_slots") # 清理临时数据 + + for item in data: + item.update(vehicle_info.get(item["id"], {})) + return data + + @staticmethod + def _slot_to_time(slot): + """将slot (0-47) 转换为 HH:MM 格式的时间字符串""" + hours = slot // 2 + minutes = (slot % 2) * 30 + return f"{hours:02d}:{minutes:02d}" + + def perform_update(self, serializer): + ins:Vehicle = self.get_object() + ticket = ins.ticket + if ticket is None or ticket.state.type == 1: + pass + else: + raise ParseError("存在审批单,不允许修改") + if ins.create_by and ins.create_by != self.request.user: + raise ParseError("只允许创建者修改") + return super().perform_update(serializer) + + def perform_destroy(self, instance): + if instance.create_by and instance.create_by != self.request.user: + raise ParseError("只允许创建者删除") + ticket = instance.ticket + if ticket is None or ticket.state.type == 1: + pass + else: + raise ParseError("存在审批单,不允许删除") + if ticket: + ticket.delete() + instance.delete() + + +class VehicleSlotViewSet(CustomListModelMixin, CustomGenericViewSet): + """list: + + 会议室预订时段 + """ + queryset = VehicleSlot.objects.all() + serializer_class = VehSlotSerializer + filterset_fields = ["vehreg", "vehicle", "vdate", "is_inuse"] class FilerecordViewSet(CustomModelViewSet): diff --git a/apps/srm/models.py b/apps/srm/models.py index 39dd6394..84f7eaf1 100644 --- a/apps/srm/models.py +++ b/apps/srm/models.py @@ -115,6 +115,7 @@ class PaperRecord(CommonADModel): class Platform(CommonADModel): """TN: 平台信息表""" + name = models.CharField("名称", max_length=50, null=True, blank=True) p_date = models.DateField(null=True, blank=True, verbose_name="日期") p_dept = models.CharField("归口部门", max_length=200, null=True, blank=True) city_p = models.BooleanField("市级平台", default=False) @@ -128,7 +129,6 @@ class Platform(CommonADModel): class Platstanding(CommonADModel): """TN: 平台台账登记""" number = models.CharField("发文号", max_length=50, null=True, blank=True) - name = models.CharField("名称", max_length=50, null=True, blank=True) p_type = models.CharField("平台类型", max_length=50, null=True, blank=True) org = models.CharField("单位", max_length=100, null=True, blank=True) period = models.CharField("建设期", max_length=200, null=True, blank=True) diff --git a/apps/srm/serializers.py b/apps/srm/serializers.py index cf7a7347..719adea0 100644 --- a/apps/srm/serializers.py +++ b/apps/srm/serializers.py @@ -54,6 +54,7 @@ class PlatformSerializer(CustomModelSerializer): class PlatstandingSerializer(CustomModelSerializer): platinfo_ = PlatformSerializer(source='platinfo', read_only=True) + plat_name = serializers.CharField(source='platinfo.name', read_only=True, label="平台名称") create_by_name = serializers.CharField(source='create_by.name', read_only=True) class Meta: model = Platstanding diff --git a/apps/srm/views.py b/apps/srm/views.py index 96db9b10..31d81d46 100644 --- a/apps/srm/views.py +++ b/apps/srm/views.py @@ -103,12 +103,22 @@ class PlatformViewSet(CustomModelViewSet): class PlatstandingViewSet(CustomModelViewSet): - """list: 平台审批 + """list: 平台台账 平台审批 """ queryset = Platstanding.objects.all() serializer_class = PlatstandingSerializer - filterset_fields = ["name", "city_p", "province_p"] - ordering = ["-create_time", "name"] - search_fields = ["name", "city_p", "province_p"] \ No newline at end of file + filterset_fields = ["platinfo", "city_p", "province_p"] + ordering = ["-create_time"] + search_fields = ["platinfo", "city_p", "province_p"] + + @action(detail=False, methods=['get']) + def plat_name(self, request): + """获取台账列表""" + search = request.query_params.get('search', '') + queryset = Platform.objects.all() + if search: + queryset = queryset.filter(name__icontains=search) + papers = [{'id': p.id, 'name': p.name} for p in queryset] + return Response(papers) \ No newline at end of file