This commit is contained in:
caoqianming 2025-11-18 09:46:26 +08:00
commit 62d0ce87ea
9 changed files with 306 additions and 53 deletions

View File

@ -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")

View File

@ -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'),
),
]

View File

@ -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")

View File

@ -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 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)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Vehicle
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['actual_km']
# 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)

View File

@ -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')

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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"]
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)