diff --git a/fzjgact/db.sqlite3 b/fzjgact/db.sqlite3 index aef337e..0df05bc 100644 Binary files a/fzjgact/db.sqlite3 and b/fzjgact/db.sqlite3 differ diff --git a/fzjgact/huodong/__pycache__/admin.cpython-311.pyc b/fzjgact/huodong/__pycache__/admin.cpython-311.pyc index 4558fb1..6fd6ab4 100644 Binary files a/fzjgact/huodong/__pycache__/admin.cpython-311.pyc and b/fzjgact/huodong/__pycache__/admin.cpython-311.pyc differ diff --git a/fzjgact/huodong/__pycache__/models.cpython-311.pyc b/fzjgact/huodong/__pycache__/models.cpython-311.pyc index 5d5f33f..3b2962f 100644 Binary files a/fzjgact/huodong/__pycache__/models.cpython-311.pyc and b/fzjgact/huodong/__pycache__/models.cpython-311.pyc differ diff --git a/fzjgact/huodong/__pycache__/urls.cpython-311.pyc b/fzjgact/huodong/__pycache__/urls.cpython-311.pyc index 80f766f..4073f15 100644 Binary files a/fzjgact/huodong/__pycache__/urls.cpython-311.pyc and b/fzjgact/huodong/__pycache__/urls.cpython-311.pyc differ diff --git a/fzjgact/huodong/__pycache__/views.cpython-311.pyc b/fzjgact/huodong/__pycache__/views.cpython-311.pyc index 8c0caad..80f06ef 100644 Binary files a/fzjgact/huodong/__pycache__/views.cpython-311.pyc and b/fzjgact/huodong/__pycache__/views.cpython-311.pyc differ diff --git a/fzjgact/huodong/admin.py b/fzjgact/huodong/admin.py index 5118c28..35d55a9 100644 --- a/fzjgact/huodong/admin.py +++ b/fzjgact/huodong/admin.py @@ -1,194 +1,287 @@ -from django.contrib import admin -from django.contrib.admin.actions import delete_selected -from django.urls import reverse -from django.utils.html import format_html -from .models import Branch, EquipmentImage, Drawing, PublicScreen -from .models import Activity, Branch, Event, Contact, VideoTerminal -from django_select2.forms import Select2Widget -from unfold.admin import ModelAdmin -from django.contrib.admin import AdminSite -from django.contrib.auth.models import User, Group -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin -from django.db.models import Count, Q - -from django.utils.html import format_html -from django.urls import reverse -from django import forms - - -# 定义Activity的内联admin类 -class ActivityInline(admin.TabularInline): # 改为更紧凑的表格形式内联(原StackedInline为堆叠形式) - model = Activity - extra = 1 # 在Branch表单中默认显示1个额外的Activity表单(可根据需求调整数值) - fields = ('name', 'scope', 'start_time', 'end_time') # 限制内联表单显示的字段 - readonly_fields = ('start_time',) # 可选:将开始时间设为只读字段(避免误修改) - # 可选:添加帮助文本提示 - help_texts = { - 'name': '请输入活动名称(必填)', - 'scope': '选择活动范围(内部/外部)' - } - - -admin.site.unregister(User) -admin.site.unregister(Group) - - -@admin.register(User) -class UserAdmin(BaseUserAdmin, ModelAdmin): - pass - - -@admin.register(Group) -class GroupAdmin(BaseGroupAdmin, ModelAdmin): - pass - - -@admin.register(Branch) -class BranchAdmin(ModelAdmin): - inlines = [ActivityInline] - list_display = ('name', 'location', 'category', 'display_mature_status', 'background_color') - search_fields = ['name', 'location'] - fieldsets = ( - (None, { - 'fields': ('name', 'location', 'contact_info', 'description', 'category', 'is_mature', 'background_color') - }), - ) - - def display_mature_status(self, obj): - return '💼' if obj.is_mature else '📒' - display_mature_status.short_description = '是否成熟' - - def activity_count(self, obj): - return obj.activity_set.count() - - activity_count.short_description = '活动数量' - - def get_queryset(self, request): - queryset = super().get_queryset(request) - queryset = queryset.annotate( - inspection_person_count=Count('contact', filter=Q(contact__category='机房/设备间巡检人'))) - return queryset - - actions = ['set_branches_to_type_b'] - - def set_branches_to_type_b(self, request, queryset): - queryset.update(category='B型') - - set_branches_to_type_b.short_description = '将选中的分支机构统一改为B型' - - -# 注册Activity模型(如果需要的话,虽然在这个示例中我们主要关注Branch) -@admin.register(Activity) -class ActivityAdmin(ModelAdmin): - list_display = ('branch', 'scope', 'name', 'start_time', 'end_time') - search_fields = ["branch__name"] # 改为关联Branch模型的name字段 - add_form_template = 'admin/huodong/add_form.html' - - autocomplete_fields = ['branch'] - - -@admin.register(Event) -class EventAdmin(ModelAdmin): - list_display = ('name', 'start_time', 'end_time', 'description') - filter_horizontal = ('branches',) - - -class ContactAdminForm(forms.ModelForm): - class Meta: - model = Contact - fields = '__all__' - widgets = { - # 使用复选框实现多选 - 'category': forms.CheckboxSelectMultiple(choices=Contact.CATEGORY_CHOICES) - } - - -class EquipmentImageAdmin(admin.ModelAdmin): - def bulk_delete_selected(self, request, queryset): - count = queryset.count() - queryset.delete() - self.message_user(request, f'已成功删除{count}条设备图片记录') - bulk_delete_selected.short_description = '批量删除选中的设备图片' - actions = ['bulk_delete_selected'] - list_display = ['id', 'branch', 'uploaded_at', 'delete_link'] - - def delete_link(self, obj): - url = reverse('admin:huodong_equipmentimage_delete', args=[obj.id]) - return format_html('删除', url) - delete_link.short_description = '操作' - list_filter = ['branch'] - search_fields = ['branch__name'] - - autocomplete_fields = ['branch'] - - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - # 设置默认分支机构为最后一个设备间图片的分支机构 - try: - latest_image = EquipmentImage.objects.latest('uploaded_at') - form.base_fields['branch'].initial = latest_image.branch_id - except EquipmentImage.DoesNotExist: - pass - return form - - def save_model(self, request, obj, form, change): - super().save_model(request, obj, form, change) - # Count images for the current branch - count = EquipmentImage.objects.filter(branch=obj.branch).count() - self.message_user(request, f"【{obj.branch.name}】已经有{count}张设备间图片。") - -admin.site.register(EquipmentImage, EquipmentImageAdmin) - -class DrawingAdmin(admin.ModelAdmin): - def bulk_delete_selected(self, request, queryset): - count = queryset.count() - queryset.delete() - self.message_user(request, f'已成功删除{count}条图纸记录') - bulk_delete_selected.short_description = '批量删除选中的图纸' - actions = ['bulk_delete_selected'] - list_display = ('id', 'branch', 'uploaded_at', 'delete_link') - - def delete_link(self, obj): - url = reverse('admin:huodong_drawing_delete', args=[obj.id]) - return format_html('删除', url) - delete_link.short_description = '操作' - list_filter = ('branch', 'uploaded_at') - search_fields = ('branch__name',) - autocomplete_fields = ['branch'] - -admin.site.register(Drawing, DrawingAdmin) - -class PublicScreenAdmin(ModelAdmin): - def delete_link(self, obj): - if obj.id: - delete_url = reverse('admin:huodong_publicscreen_delete', args=[obj.id]) - return format_html('删除', delete_url) - return '-' - delete_link.short_description = '操作' - - def bulk_delete_selected(self, request, queryset): - count = queryset.count() - queryset.delete() - self.message_user(request, f'已成功删除{count}条公共电子屏记录') - bulk_delete_selected.short_description = '批量删除选中的公共电子屏' - - list_display = ('id', 'branch', 'screen_type', 'last_drill', 'delete_link') - list_filter = ['branch', 'screen_type'] - autocomplete_fields = ['branch'] - actions = ['bulk_delete_selected'] - -admin.site.register(PublicScreen, PublicScreenAdmin) -@admin.register(Contact) -class ContactAdmin(ModelAdmin): - form = ContactAdminForm # 指定自定义表单 - list_display = ('branch', 'category', 'name', 'phone', 'email') # 可选:显示分类字段 - - autocomplete_fields = ['branch'] - -@admin.register(VideoTerminal) -class VideoTerminalAdmin(ModelAdmin): - list_display = ('branch', 'terminal_type', 'description', 'created_at') - list_filter = ('terminal_type', 'branch') - search_fields = ('branch__name', 'description') - autocomplete_fields = ['branch'] +from django.contrib import admin +from django.contrib.admin.actions import delete_selected +from django.urls import reverse +from django.utils.html import format_html +from .models import Branch, EquipmentImage, Drawing, PublicScreen +from .models import Activity, Branch, Event, Contact, VideoTerminal, Budget, EquipmentBudget, InfrastructureBudget, BudgetTemplate, TemplateEquipmentItem, TemplateInfrastructureItem +from django_select2.forms import Select2Widget +from unfold.admin import ModelAdmin +from django.contrib.admin import AdminSite +from django.contrib.auth.models import User, Group +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin +from django.db.models import Count, Q + +from django.utils.html import format_html +from django.urls import reverse +from django import forms + + +# 定义Activity的内联admin类 +class ActivityInline(admin.TabularInline): # 改为更紧凑的表格形式内联(原StackedInline为堆叠形式) + model = Activity + extra = 1 # 在Branch表单中默认显示1个额外的Activity表单(可根据需求调整数值) + fields = ('name', 'scope', 'start_time', 'end_time') # 限制内联表单显示的字段 + readonly_fields = ('start_time',) # 可选:将开始时间设为只读字段(避免误修改) + # 可选:添加帮助文本提示 + help_texts = { + 'name': '请输入活动名称(必填)', + 'scope': '选择活动范围(内部/外部)' + } + + +admin.site.unregister(User) +admin.site.unregister(Group) + + +@admin.register(User) +class UserAdmin(BaseUserAdmin, ModelAdmin): + pass + + +@admin.register(Group) +class GroupAdmin(BaseGroupAdmin, ModelAdmin): + pass + + +@admin.register(Branch) +class BranchAdmin(ModelAdmin): + inlines = [ActivityInline] + list_display = ('name', 'location', 'category', 'display_mature_status', 'background_color') + search_fields = ['name', 'location'] + fieldsets = ( + (None, { + 'fields': ('name', 'location', 'contact_info', 'description', 'category', 'is_mature', 'background_color') + }), + ) + + def display_mature_status(self, obj): + return '💼' if obj.is_mature else '📒' + display_mature_status.short_description = '是否成熟' + + def activity_count(self, obj): + return obj.activity_set.count() + + activity_count.short_description = '活动数量' + + def get_queryset(self, request): + queryset = super().get_queryset(request) + queryset = queryset.annotate( + inspection_person_count=Count('contact', filter=Q(contact__category='机房/设备间巡检人'))) + return queryset + + actions = ['set_branches_to_type_b'] + + def set_branches_to_type_b(self, request, queryset): + queryset.update(category='B型') + + set_branches_to_type_b.short_description = '将选中的分支机构统一改为B型' + + +# 注册Activity模型(如果需要的话,虽然在这个示例中我们主要关注Branch) +@admin.register(Activity) +class ActivityAdmin(ModelAdmin): + list_display = ('branch', 'scope', 'name', 'start_time', 'end_time') + search_fields = ["branch__name"] # 改为关联Branch模型的name字段 + add_form_template = 'admin/huodong/add_form.html' + + autocomplete_fields = ['branch'] + + +@admin.register(Event) +class EventAdmin(ModelAdmin): + list_display = ('name', 'start_time', 'end_time', 'description') + filter_horizontal = ('branches',) + + +class ContactAdminForm(forms.ModelForm): + class Meta: + model = Contact + fields = '__all__' + widgets = { + # 使用复选框实现多选 + 'category': forms.CheckboxSelectMultiple(choices=Contact.CATEGORY_CHOICES) + } + + +class EquipmentImageAdmin(admin.ModelAdmin): + def bulk_delete_selected(self, request, queryset): + count = queryset.count() + queryset.delete() + self.message_user(request, f'已成功删除{count}条设备图片记录') + bulk_delete_selected.short_description = '批量删除选中的设备图片' + actions = ['bulk_delete_selected'] + list_display = ['id', 'branch', 'uploaded_at', 'delete_link'] + + def delete_link(self, obj): + url = reverse('admin:huodong_equipmentimage_delete', args=[obj.id]) + return format_html('删除', url) + delete_link.short_description = '操作' + list_filter = ['branch'] + search_fields = ['branch__name'] + + autocomplete_fields = ['branch'] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + # 设置默认分支机构为最后一个设备间图片的分支机构 + try: + latest_image = EquipmentImage.objects.latest('uploaded_at') + form.base_fields['branch'].initial = latest_image.branch_id + except EquipmentImage.DoesNotExist: + pass + return form + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + # Count images for the current branch + count = EquipmentImage.objects.filter(branch=obj.branch).count() + self.message_user(request, f"【{obj.branch.name}】已经有{count}张设备间图片。") + +admin.site.register(EquipmentImage, EquipmentImageAdmin) + +class DrawingAdmin(admin.ModelAdmin): + def bulk_delete_selected(self, request, queryset): + count = queryset.count() + queryset.delete() + self.message_user(request, f'已成功删除{count}条图纸记录') + bulk_delete_selected.short_description = '批量删除选中的图纸' + actions = ['bulk_delete_selected'] + list_display = ('id', 'branch', 'uploaded_at', 'delete_link') + + def delete_link(self, obj): + url = reverse('admin:huodong_drawing_delete', args=[obj.id]) + return format_html('删除', url) + delete_link.short_description = '操作' + list_filter = ('branch', 'uploaded_at') + search_fields = ('branch__name',) + autocomplete_fields = ['branch'] + +admin.site.register(Drawing, DrawingAdmin) + +class PublicScreenAdmin(ModelAdmin): + def delete_link(self, obj): + if obj.id: + delete_url = reverse('admin:huodong_publicscreen_delete', args=[obj.id]) + return format_html('删除', delete_url) + return '-' + delete_link.short_description = '操作' + + def bulk_delete_selected(self, request, queryset): + count = queryset.count() + queryset.delete() + self.message_user(request, f'已成功删除{count}条公共电子屏记录') + bulk_delete_selected.short_description = '批量删除选中的公共电子屏' + + list_display = ('id', 'branch', 'screen_type', 'last_drill', 'delete_link') + list_filter = ['branch', 'screen_type'] + autocomplete_fields = ['branch'] + actions = ['bulk_delete_selected'] + +admin.site.register(PublicScreen, PublicScreenAdmin) +@admin.register(Contact) +class ContactAdmin(ModelAdmin): + form = ContactAdminForm # 指定自定义表单 + list_display = ('branch', 'category', 'name', 'phone', 'email') # 可选:显示分类字段 + + autocomplete_fields = ['branch'] + +@admin.register(VideoTerminal) +class VideoTerminalAdmin(ModelAdmin): + list_display = ('branch', 'terminal_type', 'description', 'created_at') + list_filter = ('terminal_type', 'branch') + search_fields = ('branch__name', 'description') + autocomplete_fields = ['branch'] + + +# 预算相关Admin配置 + +# 设备预算内联 +class EquipmentBudgetInline(admin.TabularInline): + model = EquipmentBudget + extra = 1 + fields = ('project', 'model', 'unit_price', 'procurement_method', 'quantity', 'subtotal') + readonly_fields = ('subtotal',) # 小计自动计算,设为只读 + +# 基础设施预算内联 +class InfrastructureBudgetInline(admin.TabularInline): + model = InfrastructureBudget + extra = 1 + fields = ('name', 'remarks', 'unit_price', 'unit', 'quantity', 'subtotal', 'description') + readonly_fields = ('subtotal',) # 小计自动计算,设为只读 + +# 预算Admin +@admin.register(Budget) +class BudgetAdmin(ModelAdmin): + list_display = ('branch', 'name', 'total_budget', 'created_at') + list_filter = ('branch',) + search_fields = ('branch__name', 'name') + autocomplete_fields = ['branch'] + readonly_fields = ('total_budget',) # 总预算自动计算,设为只读 + inlines = [EquipmentBudgetInline, InfrastructureBudgetInline] + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + # 保存后重新计算总预算 + obj.update_total_budget() + +# 设备预算Admin(如果需要单独管理) +@admin.register(EquipmentBudget) +class EquipmentBudgetAdmin(ModelAdmin): + list_display = ('budget', 'project', 'model', 'unit_price', 'procurement_method', 'quantity', 'subtotal') + list_filter = ('budget__branch', 'project', 'procurement_method') + search_fields = ('budget__branch__name', 'project', 'model') + autocomplete_fields = ['budget'] + readonly_fields = ('subtotal',) + +# 基础设施预算Admin(如果需要单独管理) +@admin.register(InfrastructureBudget) +class InfrastructureBudgetAdmin(ModelAdmin): + list_display = ('budget', 'name', 'unit_price', 'unit', 'quantity', 'subtotal') + list_filter = ('budget__branch', 'name') + search_fields = ('budget__branch__name', 'name', 'description') + autocomplete_fields = ['budget'] + readonly_fields = ('subtotal',) + + +# 预算模板相关Admin配置 + +# 模板设备项内联 +class TemplateEquipmentItemInline(admin.TabularInline): + model = TemplateEquipmentItem + extra = 1 + +# 模板基础设施项内联 +class TemplateInfrastructureItemInline(admin.TabularInline): + model = TemplateInfrastructureItem + extra = 1 + +# 模板设备项Admin(如果需要单独管理) +@admin.register(TemplateEquipmentItem) +class TemplateEquipmentItemAdmin(ModelAdmin): + list_display = ('template', 'project', 'model', 'unit_price', 'procurement_method') + list_filter = ('template', 'project', 'procurement_method') + search_fields = ('template__name', 'project', 'model') + autocomplete_fields = ['template'] + +# 模板基础设施项Admin(如果需要单独管理) +@admin.register(TemplateInfrastructureItem) +class TemplateInfrastructureItemAdmin(ModelAdmin): + list_display = ('template', 'name', 'unit_price', 'unit') + list_filter = ('template', 'unit') + search_fields = ('template__name', 'name', 'description') + autocomplete_fields = ['template'] + +# 预算模板Admin +@admin.register(BudgetTemplate) +class BudgetTemplateAdmin(ModelAdmin): + list_display = ('name', 'description', 'is_default', 'created_at') + list_filter = ('is_default',) + search_fields = ('name', 'description') + inlines = [TemplateEquipmentItemInline, TemplateInfrastructureItemInline] + + def save_model(self, request, obj, form, change): + # 如果设置为默认模板,将其他模板的默认状态取消 + if obj.is_default: + BudgetTemplate.objects.exclude(id=obj.id).update(is_default=False) + super().save_model(request, obj, form, change) diff --git a/fzjgact/huodong/migrations/0030_budget_equipmentbudget_infrastructurebudget.py b/fzjgact/huodong/migrations/0030_budget_equipmentbudget_infrastructurebudget.py new file mode 100644 index 0000000..7ea59f3 --- /dev/null +++ b/fzjgact/huodong/migrations/0030_budget_equipmentbudget_infrastructurebudget.py @@ -0,0 +1,64 @@ +# Generated by Django 5.0.6 on 2025-12-03 07:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('huodong', '0029_alter_activity_end_time_alter_activity_start_time_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Budget', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('total_budget', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='总预算')), + ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='huodong.activity', verbose_name='活动')), + ('branch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='huodong.branch', verbose_name='分支机构')), + ], + options={ + 'verbose_name': '预算主表', + 'verbose_name_plural': '预算主表', + }, + ), + migrations.CreateModel( + name='EquipmentBudget', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('project', models.CharField(max_length=255, verbose_name='项目')), + ('model', models.CharField(max_length=255, verbose_name='型号')), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='单价')), + ('procurement_method', models.CharField(choices=[('本地询价采购', '本地询价采购'), ('订单采购', '订单采购'), ('按照总部配置要求本地询价采购', '按照总部配置要求本地询价采购'), ('本地询价采购或订单采购', '本地询价采购或订单采购')], max_length=50, verbose_name='采购方式')), + ('quantity', models.IntegerField(default=1, verbose_name='数量')), + ('subtotal', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='小计')), + ('budget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipment_budgets', to='huodong.budget', verbose_name='预算主表')), + ], + options={ + 'verbose_name': '设备预算明细', + 'verbose_name_plural': '设备预算明细', + }, + ), + migrations.CreateModel( + name='InfrastructureBudget', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='名称')), + ('remarks', models.TextField(blank=True, verbose_name='备注')), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='单价')), + ('unit', models.CharField(max_length=20, verbose_name='单位')), + ('description', models.TextField(blank=True, verbose_name='说明')), + ('quantity', models.IntegerField(default=1, verbose_name='数量')), + ('subtotal', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='小计')), + ('budget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='infrastructure_budgets', to='huodong.budget', verbose_name='预算主表')), + ], + options={ + 'verbose_name': '基础设施预算明细', + 'verbose_name_plural': '基础设施预算明细', + }, + ), + ] diff --git a/fzjgact/huodong/migrations/0031_budgettemplate_templateequipmentitem_and_more.py b/fzjgact/huodong/migrations/0031_budgettemplate_templateequipmentitem_and_more.py new file mode 100644 index 0000000..1d438b0 --- /dev/null +++ b/fzjgact/huodong/migrations/0031_budgettemplate_templateequipmentitem_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0.6 on 2025-12-03 08:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('huodong', '0030_budget_equipmentbudget_infrastructurebudget'), + ] + + operations = [ + migrations.CreateModel( + name='BudgetTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='模板名称')), + ('description', models.TextField(blank=True, verbose_name='模板描述')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('is_default', models.BooleanField(default=False, verbose_name='是否默认模板')), + ], + options={ + 'verbose_name': '预算模板', + 'verbose_name_plural': '预算模板', + }, + ), + migrations.CreateModel( + name='TemplateEquipmentItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('project', models.CharField(max_length=255, verbose_name='项目')), + ('model', models.CharField(max_length=255, verbose_name='型号')), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='参考单价')), + ('procurement_method', models.CharField(choices=[('本地询价采购', '本地询价采购'), ('订单采购', '订单采购'), ('按照总部配置要求本地询价采购', '按照总部配置要求本地询价采购'), ('本地询价采购或订单采购', '本地询价采购或订单采购')], max_length=50, verbose_name='采购方式')), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipment_items', to='huodong.budgettemplate', verbose_name='模板')), + ], + options={ + 'verbose_name': '模板设备项', + 'verbose_name_plural': '模板设备项', + }, + ), + migrations.CreateModel( + name='TemplateInfrastructureItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='名称')), + ('remarks', models.TextField(blank=True, verbose_name='备注')), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='参考单价')), + ('unit', models.CharField(max_length=20, verbose_name='单位')), + ('description', models.TextField(blank=True, verbose_name='说明')), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='infrastructure_items', to='huodong.budgettemplate', verbose_name='模板')), + ], + options={ + 'verbose_name': '模板基础设施项', + 'verbose_name_plural': '模板基础设施项', + }, + ), + ] diff --git a/fzjgact/huodong/migrations/0032_remove_budget_activity_budget_name.py b/fzjgact/huodong/migrations/0032_remove_budget_activity_budget_name.py new file mode 100644 index 0000000..e191601 --- /dev/null +++ b/fzjgact/huodong/migrations/0032_remove_budget_activity_budget_name.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.6 on 2025-12-03 09:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('huodong', '0031_budgettemplate_templateequipmentitem_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='budget', + name='activity', + ), + migrations.AddField( + model_name='budget', + name='name', + field=models.CharField(default=1, max_length=255, verbose_name='预算名称'), + preserve_default=False, + ), + ] diff --git a/fzjgact/huodong/migrations/__pycache__/0030_budget_equipmentbudget_infrastructurebudget.cpython-311.pyc b/fzjgact/huodong/migrations/__pycache__/0030_budget_equipmentbudget_infrastructurebudget.cpython-311.pyc new file mode 100644 index 0000000..46ca288 Binary files /dev/null and b/fzjgact/huodong/migrations/__pycache__/0030_budget_equipmentbudget_infrastructurebudget.cpython-311.pyc differ diff --git a/fzjgact/huodong/migrations/__pycache__/0031_budgettemplate_templateequipmentitem_and_more.cpython-311.pyc b/fzjgact/huodong/migrations/__pycache__/0031_budgettemplate_templateequipmentitem_and_more.cpython-311.pyc new file mode 100644 index 0000000..5b8c5a6 Binary files /dev/null and b/fzjgact/huodong/migrations/__pycache__/0031_budgettemplate_templateequipmentitem_and_more.cpython-311.pyc differ diff --git a/fzjgact/huodong/migrations/__pycache__/0032_remove_budget_activity_budget_name.cpython-311.pyc b/fzjgact/huodong/migrations/__pycache__/0032_remove_budget_activity_budget_name.cpython-311.pyc new file mode 100644 index 0000000..04c64ef Binary files /dev/null and b/fzjgact/huodong/migrations/__pycache__/0032_remove_budget_activity_budget_name.cpython-311.pyc differ diff --git a/fzjgact/huodong/models.py b/fzjgact/huodong/models.py index afbec58..cc496ac 100644 --- a/fzjgact/huodong/models.py +++ b/fzjgact/huodong/models.py @@ -1,182 +1,327 @@ -from django.db import models -from django.utils import timezone - - -class Branch(models.Model): - CATEGORY_CHOICES = ( - ('A型', 'A型'), - ('B型', 'B型'), - ('C型', 'C型'), - ('不适用', '不适用'), - ) - name = models.CharField(max_length=255, unique=True, verbose_name='分支机构名称') - location = models.CharField(max_length=255, verbose_name='所在省份') - contact_info = models.CharField(max_length=255, verbose_name='主要联系人') - description = models.TextField(blank=True, verbose_name='备注') - background_color = models.CharField(max_length=7, default='#EFF6FF', verbose_name='背景色', - help_text='使用#RRGGBB格式的颜色代码') - category = models.CharField(max_length=10, choices=CATEGORY_CHOICES, default='C型', verbose_name='分类') - is_mature = models.BooleanField(default=False, verbose_name='是否成熟') - - - def __str__(self): - return f'{self.name} 💼' if self.is_mature else self.name - - class Meta: - verbose_name = '分支机构' - verbose_name_plural = '分支机构(基础信息)' - - -class Contact(models.Model): - branch = models.ForeignKey(Branch, on_delete=models.CASCADE, verbose_name='分支机构') - CATEGORY_CHOICES = [ - ('机房/设备间巡检人', '机房/设备间巡检人'), - ('信息安全联系人', '信息安全联系人'), - ('兼岗', '兼岗'), - ('安全员', '安全员') - # 可以添加更多类别 - ] - # 修改为支持多选的 CharField - category = models.CharField( - max_length=255, # 增大长度(原50可能不足) - choices=CATEGORY_CHOICES, - verbose_name='联系人分类', - help_text='按住 Ctrl/Command 键多选(值将以逗号分隔存储)' - ) - name = models.CharField(max_length=255, verbose_name='姓名') - phone = models.CharField(max_length=20, verbose_name='电话') - email = models.EmailField(blank=True, verbose_name='邮箱,可不填') - description = models.TextField(blank=True, verbose_name='描述,可不填') - - def __str__(self): - return self.name - - class Meta: - verbose_name = '联系人群' - verbose_name_plural = '联系人群' - - -class Activity(models.Model): - branch = models.ForeignKey(Branch, on_delete=models.CASCADE, verbose_name='分支机构') - name = models.CharField(max_length=255, verbose_name='活动名称') - scope = models.CharField(max_length=255, choices=( - ('新建', '新建'), - ('搬迁', '搬迁'), - ('原址装修', '原址装修'), - ('撤销', '撤销'), - ('其他技术问题', '其他技术问题') - - ), verbose_name='活动类型') - start_time = models.DateField(verbose_name='开始日期') - end_time = models.DateField(blank=True, null=True, verbose_name='结束日期') # 可以为空,表示活动尚未结束 - location = models.CharField(max_length=255, verbose_name='所在地点') - description = models.TextField(verbose_name='其它内容') - - def __str__(self): - return self.name - - class Meta: - verbose_name = '运营活动内容' - verbose_name_plural = '运营活动内容(新建搬迁装修和技术)' - - -class EquipmentImage(models.Model): - branch = models.ForeignKey(Branch, related_name='equipment_images', on_delete=models.CASCADE) - image = models.ImageField(upload_to='equipment_room_images/') - uploaded_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"设备间图片 {self.id} - {self.branch.name}" - - class Meta: - verbose_name = '设备间图' - verbose_name_plural = '设备间图' - - -# 图纸的类 -class Drawing(models.Model): - branch = models.ForeignKey(Branch, related_name='drawings', on_delete=models.CASCADE) - image = models.ImageField(upload_to='drawings/') - uploaded_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"图纸 {self.id} - {self.branch.name}" - - class Meta: - verbose_name = '图纸' - verbose_name_plural = '图纸' - - -# 公共电子屏 -class PublicScreen(models.Model): - SCREEN_TYPES = ( - ('marquee', '跑马灯'), - ('advertisement', '广告屏'), - ('information', '信息发布屏'), - ) - branch = models.ForeignKey(Branch, on_delete=models.CASCADE, related_name='public_screens') - image = models.ImageField(upload_to='public_screen_images/', null=True, blank=True) - screen_type = models.CharField(max_length=20, choices=SCREEN_TYPES, verbose_name='功能类型', null=True, blank=True) - description = models.TextField(blank=True, null=True, verbose_name='功能描述') - last_drill = models.ForeignKey('Event', on_delete=models.SET_NULL, blank=True, null=True, related_name='public_screens', verbose_name='最后演练事件') - created_at = models.DateTimeField(default=timezone.now) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f'{self.branch.name} - {self.get_screen_type_display()} {self.id}' - - class Meta: - verbose_name = '公共电子屏' - verbose_name_plural = '公共电子屏' - - -class Event(models.Model): - branches = models.ManyToManyField(Branch, related_name='events', verbose_name='分支机构') - name = models.CharField(max_length=255, verbose_name='事件名称') - start_time = models.DateField(verbose_name='开始时间') - end_time = models.DateField(blank=True, null=True, verbose_name='结束时间') # 可以为空,表示活动尚未结束 - description = models.TextField(verbose_name='事件描述') - - def __str__(self): - return self.name - - class Meta: - verbose_name = '运营事件' - verbose_name_plural = '运营事件(其它)' - - -class VideoTerminal(models.Model): - TERMINAL_TYPES = ( - ('polycom', '宝利通终端'), - ('zte', '中兴终端'), - ('logitech', '罗技摄像头'), - ('laptop_tv', '笔记本加电视'), - ('laptop_projector', '笔记本加投影仪'), - ('other', '其它'), - ) - branch = models.ForeignKey(Branch, on_delete=models.CASCADE, related_name='video_terminals', verbose_name='分支机构') - terminal_type = models.CharField(max_length=20, choices=TERMINAL_TYPES, verbose_name='设备类型') - description = models.TextField(blank=True, verbose_name='设备描述') - created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') - updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') - - def __str__(self): - return f"{self.branch.name} - {self.get_terminal_type_display()}" - - class Meta: - verbose_name = '视频设备终端' - verbose_name_plural = '视频设备终端' - - -class Evaluation(models.Model): - activity = models.ForeignKey(Activity, on_delete=models.CASCADE) - branch = models.ForeignKey(Branch, on_delete=models.CASCADE) - score = models.DecimalField(max_digits=4, decimal_places=2) - comment = models.TextField() - file_path = models.CharField(max_length=255, blank=True, null=True) - status = models.CharField(max_length=20, - choices=(('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝')), - default='pending') - - def __str__(self): - return f"{self.activity.name} - {self.branch.name}" +from django.db import models +from django.utils import timezone + + +class Branch(models.Model): + CATEGORY_CHOICES = ( + ('A型', 'A型'), + ('B型', 'B型'), + ('C型', 'C型'), + ('不适用', '不适用'), + ) + name = models.CharField(max_length=255, unique=True, verbose_name='分支机构名称') + location = models.CharField(max_length=255, verbose_name='所在省份') + contact_info = models.CharField(max_length=255, verbose_name='主要联系人') + description = models.TextField(blank=True, verbose_name='备注') + background_color = models.CharField(max_length=7, default='#EFF6FF', verbose_name='背景色', + help_text='使用#RRGGBB格式的颜色代码') + category = models.CharField(max_length=10, choices=CATEGORY_CHOICES, default='C型', verbose_name='分类') + is_mature = models.BooleanField(default=False, verbose_name='是否成熟') + + + def __str__(self): + return f'{self.name} 💼' if self.is_mature else self.name + + class Meta: + verbose_name = '分支机构' + verbose_name_plural = '分支机构(基础信息)' + + +class Contact(models.Model): + branch = models.ForeignKey(Branch, on_delete=models.CASCADE, verbose_name='分支机构') + CATEGORY_CHOICES = [ + ('机房/设备间巡检人', '机房/设备间巡检人'), + ('信息安全联系人', '信息安全联系人'), + ('兼岗', '兼岗'), + ('安全员', '安全员') + # 可以添加更多类别 + ] + # 修改为支持多选的 CharField + category = models.CharField( + max_length=255, # 增大长度(原50可能不足) + choices=CATEGORY_CHOICES, + verbose_name='联系人分类', + help_text='按住 Ctrl/Command 键多选(值将以逗号分隔存储)' + ) + name = models.CharField(max_length=255, verbose_name='姓名') + phone = models.CharField(max_length=20, verbose_name='电话') + email = models.EmailField(blank=True, verbose_name='邮箱,可不填') + description = models.TextField(blank=True, verbose_name='描述,可不填') + + def __str__(self): + return self.name + + class Meta: + verbose_name = '联系人群' + verbose_name_plural = '联系人群' + + +class Activity(models.Model): + branch = models.ForeignKey(Branch, on_delete=models.CASCADE, verbose_name='分支机构') + name = models.CharField(max_length=255, verbose_name='活动名称') + scope = models.CharField(max_length=255, choices=( + ('新建', '新建'), + ('搬迁', '搬迁'), + ('原址装修', '原址装修'), + ('撤销', '撤销'), + ('其他技术问题', '其他技术问题') + + ), verbose_name='活动类型') + start_time = models.DateField(verbose_name='开始日期') + end_time = models.DateField(blank=True, null=True, verbose_name='结束日期') # 可以为空,表示活动尚未结束 + location = models.CharField(max_length=255, verbose_name='所在地点') + description = models.TextField(verbose_name='其它内容') + + def __str__(self): + return self.name + + class Meta: + verbose_name = '运营活动内容' + verbose_name_plural = '运营活动内容(新建搬迁装修和技术)' + + +class EquipmentImage(models.Model): + branch = models.ForeignKey(Branch, related_name='equipment_images', on_delete=models.CASCADE) + image = models.ImageField(upload_to='equipment_room_images/') + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"设备间图片 {self.id} - {self.branch.name}" + + class Meta: + verbose_name = '设备间图' + verbose_name_plural = '设备间图' + + +# 图纸的类 +class Drawing(models.Model): + branch = models.ForeignKey(Branch, related_name='drawings', on_delete=models.CASCADE) + image = models.ImageField(upload_to='drawings/') + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"图纸 {self.id} - {self.branch.name}" + + class Meta: + verbose_name = '图纸' + verbose_name_plural = '图纸' + + +# 公共电子屏 +class PublicScreen(models.Model): + SCREEN_TYPES = ( + ('marquee', '跑马灯'), + ('advertisement', '广告屏'), + ('information', '信息发布屏'), + ) + branch = models.ForeignKey(Branch, on_delete=models.CASCADE, related_name='public_screens') + image = models.ImageField(upload_to='public_screen_images/', null=True, blank=True) + screen_type = models.CharField(max_length=20, choices=SCREEN_TYPES, verbose_name='功能类型', null=True, blank=True) + description = models.TextField(blank=True, null=True, verbose_name='功能描述') + last_drill = models.ForeignKey('Event', on_delete=models.SET_NULL, blank=True, null=True, related_name='public_screens', verbose_name='最后演练事件') + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f'{self.branch.name} - {self.get_screen_type_display()} {self.id}' + + class Meta: + verbose_name = '公共电子屏' + verbose_name_plural = '公共电子屏' + + +class Event(models.Model): + branches = models.ManyToManyField(Branch, related_name='events', verbose_name='分支机构') + name = models.CharField(max_length=255, verbose_name='事件名称') + start_time = models.DateField(verbose_name='开始时间') + end_time = models.DateField(blank=True, null=True, verbose_name='结束时间') # 可以为空,表示活动尚未结束 + description = models.TextField(verbose_name='事件描述') + + def __str__(self): + return self.name + + class Meta: + verbose_name = '运营事件' + verbose_name_plural = '运营事件(其它)' + + +class VideoTerminal(models.Model): + TERMINAL_TYPES = ( + ('polycom', '宝利通终端'), + ('zte', '中兴终端'), + ('logitech', '罗技摄像头'), + ('laptop_tv', '笔记本加电视'), + ('laptop_projector', '笔记本加投影仪'), + ('other', '其它'), + ) + branch = models.ForeignKey(Branch, on_delete=models.CASCADE, related_name='video_terminals', verbose_name='分支机构') + terminal_type = models.CharField(max_length=20, choices=TERMINAL_TYPES, verbose_name='设备类型') + description = models.TextField(blank=True, verbose_name='设备描述') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + def __str__(self): + return f"{self.branch.name} - {self.get_terminal_type_display()}" + + class Meta: + verbose_name = '视频设备终端' + verbose_name_plural = '视频设备终端' + + +class Evaluation(models.Model): + activity = models.ForeignKey(Activity, on_delete=models.CASCADE) + branch = models.ForeignKey(Branch, on_delete=models.CASCADE) + score = models.DecimalField(max_digits=4, decimal_places=2) + comment = models.TextField() + file_path = models.CharField(max_length=255, blank=True, null=True) + status = models.CharField(max_length=20, + choices=(('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝')), + default='pending') + + def __str__(self): + return f"{self.activity.name} - {self.branch.name}" + + +# 预算相关模型 +class Budget(models.Model): + """预算主表""" + branch = models.ForeignKey(Branch, on_delete=models.CASCADE, verbose_name='分支机构') + name = models.CharField(max_length=255, verbose_name='预算名称') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + total_budget = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name='总预算') + + def __str__(self): + return f"{self.branch.name} - {self.name} 预算" + + class Meta: + verbose_name = '预算主表' + verbose_name_plural = '预算主表' + + +class EquipmentBudget(models.Model): + """设备预算明细""" + BUDGET_TYPES = ( + ('本地询价采购', '本地询价采购'), + ('订单采购', '订单采购'), + ('按照总部配置要求本地询价采购', '按照总部配置要求本地询价采购'), + ('本地询价采购或订单采购', '本地询价采购或订单采购') + ) + budget = models.ForeignKey(Budget, related_name='equipment_budgets', on_delete=models.CASCADE, verbose_name='预算主表') + project = models.CharField(max_length=255, verbose_name='项目') + model = models.CharField(max_length=255, verbose_name='型号') + unit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='单价') + procurement_method = models.CharField(max_length=50, choices=BUDGET_TYPES, verbose_name='采购方式') + quantity = models.IntegerField(default=1, verbose_name='数量') + subtotal = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='小计') + + def save(self, *args, **kwargs): + self.subtotal = self.unit_price * self.quantity + super().save(*args, **kwargs) + # 更新预算主表的总预算 + self.update_total_budget() + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + # 更新预算主表的总预算 + self.update_total_budget() + + def update_total_budget(self): + """更新预算主表的总预算""" + total = self.budget.equipment_budgets.aggregate(total=models.Sum('subtotal'))['total'] or 0 + total += self.budget.infrastructure_budgets.aggregate(total=models.Sum('subtotal'))['total'] or 0 + self.budget.total_budget = total + self.budget.save() + + def __str__(self): + return f"{self.project} - {self.model}" + + class Meta: + verbose_name = '设备预算明细' + verbose_name_plural = '设备预算明细' + + +class InfrastructureBudget(models.Model): + """基础设施预算明细""" + budget = models.ForeignKey(Budget, related_name='infrastructure_budgets', on_delete=models.CASCADE, verbose_name='预算主表') + name = models.CharField(max_length=255, verbose_name='名称') + remarks = models.TextField(blank=True, verbose_name='备注') + unit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='单价') + unit = models.CharField(max_length=20, verbose_name='单位') + description = models.TextField(blank=True, verbose_name='说明') + quantity = models.IntegerField(default=1, verbose_name='数量') + subtotal = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='小计') + + def save(self, *args, **kwargs): + self.subtotal = self.unit_price * self.quantity + super().save(*args, **kwargs) + # 更新预算主表的总预算 + self.update_total_budget() + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + # 更新预算主表的总预算 + self.update_total_budget() + + def update_total_budget(self): + """更新预算主表的总预算""" + total = self.budget.equipment_budgets.aggregate(total=models.Sum('subtotal'))['total'] or 0 + total += self.budget.infrastructure_budgets.aggregate(total=models.Sum('subtotal'))['total'] or 0 + self.budget.total_budget = total + self.budget.save() + + def __str__(self): + return self.name + + class Meta: + verbose_name = '基础设施预算明细' + verbose_name_plural = '基础设施预算明细' + + +class BudgetTemplate(models.Model): + """预算模板""" + name = models.CharField(max_length=255, verbose_name='模板名称') + description = models.TextField(blank=True, verbose_name='模板描述') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + is_default = models.BooleanField(default=False, verbose_name='是否默认模板') + + def __str__(self): + return self.name + + class Meta: + verbose_name = '预算模板' + verbose_name_plural = '预算模板' + + +class TemplateEquipmentItem(models.Model): + """模板设备项""" + template = models.ForeignKey(BudgetTemplate, related_name='equipment_items', on_delete=models.CASCADE, verbose_name='模板') + project = models.CharField(max_length=255, verbose_name='项目') + model = models.CharField(max_length=255, verbose_name='型号') + unit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='参考单价') + procurement_method = models.CharField(max_length=50, choices=EquipmentBudget.BUDGET_TYPES, verbose_name='采购方式') + + def __str__(self): + return f"{self.project} - {self.model}" + + class Meta: + verbose_name = '模板设备项' + verbose_name_plural = '模板设备项' + + +class TemplateInfrastructureItem(models.Model): + """模板基础设施项""" + template = models.ForeignKey(BudgetTemplate, related_name='infrastructure_items', on_delete=models.CASCADE, verbose_name='模板') + name = models.CharField(max_length=255, verbose_name='名称') + remarks = models.TextField(blank=True, verbose_name='备注') + unit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='参考单价') + unit = models.CharField(max_length=20, verbose_name='单位') + description = models.TextField(blank=True, verbose_name='说明') + + def __str__(self): + return self.name + + class Meta: + verbose_name = '模板基础设施项' + verbose_name_plural = '模板基础设施项' diff --git a/fzjgact/huodong/templates/branch_detail.html b/fzjgact/huodong/templates/branch_detail.html index 2dc67c9..a0d0007 100644 --- a/fzjgact/huodong/templates/branch_detail.html +++ b/fzjgact/huodong/templates/branch_detail.html @@ -11,6 +11,125 @@ + + +
+

投入预算表

+

分支机构项目的预算表,包括设备和基础设施明细

+ + + {% if budget_templates %} +
+

预算模板导入

+

从模板一键导入预算,快速生成预算表

+ +
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ {% endif %} + + {% if budgets %} + {% for budget in budgets %} +
+

{{ budget.activity.name }} - 总预算: ¥{{ budget.total_budget }}

+ + +
+

设备预算明细

+ {% if budget.equipment_budgets.all %} +
+ + + + + + + + + + + + + {% for equipment in budget.equipment_budgets.all %} + + + + + + + + + {% endfor %} + +
项目型号单价采购方式数量小计
{{ equipment.project }}{{ equipment.model }}¥{{ equipment.unit_price }}{{ equipment.get_procurement_method_display }}{{ equipment.quantity }}¥{{ equipment.subtotal }}
+
+ {% else %} +

暂无设备预算明细

+ {% endif %} +
+ + +
+

基础设施预算明细

+ {% if budget.infrastructure_budgets.all %} +
+ + + + + + + + + + + + + + {% for infrastructure in budget.infrastructure_budgets.all %} + + + + + + + + + + {% endfor %} + +
名称备注单价单位数量小计说明
{{ infrastructure.name }}{{ infrastructure.remarks }}¥{{ infrastructure.unit_price }}{{ infrastructure.unit }}{{ infrastructure.quantity }}¥{{ infrastructure.subtotal }}{{ infrastructure.description }}
+
+ {% else %} +

暂无基础设施预算明细

+ {% endif %} +
+
+ {% endfor %} + {% else %} +

暂无预算信息

+ {% endif %} +

基本信息

diff --git a/fzjgact/huodong/urls.py b/fzjgact/huodong/urls.py index 37f4164..9cdced3 100644 --- a/fzjgact/huodong/urls.py +++ b/fzjgact/huodong/urls.py @@ -1,22 +1,23 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter -from . import views - -router = DefaultRouter() -router.register(r'branches', views.BranchViewSet, basename='branches') -router.register(r'activities', views.ActivityViewSet, basename='activities') -router.register(r'evaluations', views.EvaluationViewSet, basename='evaluations') - -urlpatterns = [ - path('api/', include(router.urls)), - path('', views.BranchAll, name='branch-all'), - path('branch//', views.branch_detail, name='branch-detail'), - path('branch/info/', views.Branchinfo, name='branchinfo'), - path('statistics/', views.Statistics, name='statistics'), - path('contact/', views.contact_list, name='contact-list'), - path('equipment-images/', views.equipment_images, name='equipment-images'), - path('public-screens/', views.public_screens, name='public-screens'), - path('video-terminals/', views.video_terminal_list, name='video-terminals'), - path('export/xls/', views.export_branches_xls, name='export-branches-xls'), - path('export/pdf/', views.export_branches_pdf, name='export-branches-pdf'), +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from . import views + +router = DefaultRouter() +router.register(r'branches', views.BranchViewSet, basename='branches') +router.register(r'activities', views.ActivityViewSet, basename='activities') +router.register(r'evaluations', views.EvaluationViewSet, basename='evaluations') + +urlpatterns = [ + path('api/', include(router.urls)), + path('', views.BranchAll, name='branch-all'), + path('branch//', views.branch_detail, name='branch-detail'), + path('branch//import-budget/', views.import_budget_template, name='import-budget-template'), + path('branch/info/', views.Branchinfo, name='branchinfo'), + path('statistics/', views.Statistics, name='statistics'), + path('contact/', views.contact_list, name='contact-list'), + path('equipment-images/', views.equipment_images, name='equipment-images'), + path('public-screens/', views.public_screens, name='public-screens'), + path('video-terminals/', views.video_terminal_list, name='video-terminals'), + path('export/xls/', views.export_branches_xls, name='export-branches-xls'), + path('export/pdf/', views.export_branches_pdf, name='export-branches-pdf'), ] \ No newline at end of file diff --git a/fzjgact/huodong/views.py b/fzjgact/huodong/views.py index acd232a..dd7f4d0 100644 --- a/fzjgact/huodong/views.py +++ b/fzjgact/huodong/views.py @@ -1,5 +1,5 @@ from rest_framework import viewsets -from .models import Branch, Activity, Evaluation, Event, VideoTerminal +from .models import Branch, Activity, Evaluation, Event, VideoTerminal, Budget, EquipmentBudget, InfrastructureBudget, BudgetTemplate, TemplateEquipmentItem, TemplateInfrastructureItem from .serializers import BranchSerializer, ActivitySerializer, EvaluationSerializer from django.shortcuts import render, redirect from .models import PublicScreen @@ -55,6 +55,12 @@ def branch_detail(request, branch_id): equipment_images = branch.equipment_images.all() public_screens = branch.public_screens.all() + # 获取预算数据 + budgets = Budget.objects.filter(branch=branch).select_related('activity').prefetch_related('equipment_budgets', 'infrastructure_budgets').order_by('-created_at') + + # 获取预算模板 + budget_templates = BudgetTemplate.objects.all() + # 准备上下文数据 context = { 'branch': branch, @@ -62,10 +68,62 @@ def branch_detail(request, branch_id): 'events': events, 'equipment_images': equipment_images, 'public_screens': public_screens, + 'budgets': budgets, + 'activities': activities, + 'budget_templates': budget_templates, } return render(request, 'branch_detail.html', context) +def import_budget_template(request, branch_id): + """从模板导入预算""" + if request.method == 'POST': + template_id = request.POST.get('template') + budget_name = request.POST.get('budget_name', '导入预算') + + if template_id: + try: + # 获取模板和分支机构 + template = BudgetTemplate.objects.get(pk=template_id) + branch = Branch.objects.get(pk=branch_id) + + # 创建预算 + budget = Budget.objects.create( + branch=branch, + name=budget_name + ) + + # 导入设备预算项 + for equipment_item in template.equipment_items.all(): + EquipmentBudget.objects.create( + budget=budget, + project=equipment_item.project, + model=equipment_item.model, + unit_price=equipment_item.unit_price, + procurement_method=equipment_item.procurement_method, + quantity=1 # 默认数量为1 + ) + + # 导入基础设施预算项 + for infrastructure_item in template.infrastructure_items.all(): + InfrastructureBudget.objects.create( + budget=budget, + name=infrastructure_item.name, + remarks=infrastructure_item.remarks, + unit_price=infrastructure_item.unit_price, + unit=infrastructure_item.unit, + description=infrastructure_item.description, + quantity=1 # 默认数量为1 + ) + + # 更新总预算 + budget.update_total_budget() + except Exception as e: + print(f"导入预算模板失败: {e}") + + return redirect('branch-detail', branch_id=branch_id) + + # 在页面上显示所有的branch以及active的数量,首页显示 def BranchAll(request): branches = Branch.objects.exclude(activity__isnull=True).order_by('name') @@ -442,3 +500,100 @@ def video_terminal_list(request): 'selected_type_name': selected_type_name, } return render(request, 'video_terminals.html', context) + + +# 预算相关视图 + +def create_budget(request, branch_id): + branch = Branch.objects.get(pk=branch_id) + activities = Activity.objects.filter(branch=branch) + + if request.method == 'POST': + activity_id = request.POST.get('activity') + if activity_id: + activity = Activity.objects.get(pk=activity_id) + budget = Budget.objects.create(branch=branch, activity=activity) + return redirect('budget_detail', branch_id=branch_id, budget_id=budget.id) + + context = { + 'branch': branch, + 'activities': activities, + } + return render(request, 'create_budget.html', context) + + +def budget_detail(request, branch_id, budget_id): + branch = Branch.objects.get(pk=branch_id) + budget = Budget.objects.get(pk=budget_id, branch=branch) + + context = { + 'branch': branch, + 'budget': budget, + } + return render(request, 'budget_detail.html', context) + + +def add_equipment_budget(request, branch_id, budget_id): + branch = Branch.objects.get(pk=branch_id) + budget = Budget.objects.get(pk=budget_id, branch=branch) + + if request.method == 'POST': + project = request.POST.get('project') + model = request.POST.get('model') + unit_price = request.POST.get('unit_price') + procurement_method = request.POST.get('procurement_method') + quantity = request.POST.get('quantity', 1) + + if project and model and unit_price and procurement_method: + EquipmentBudget.objects.create( + budget=budget, + project=project, + model=model, + unit_price=unit_price, + procurement_method=procurement_method, + quantity=quantity + ) + + return redirect('budget_detail', branch_id=branch_id, budget_id=budget_id) + + +def add_infrastructure_budget(request, branch_id, budget_id): + branch = Branch.objects.get(pk=branch_id) + budget = Budget.objects.get(pk=budget_id, branch=branch) + + if request.method == 'POST': + name = request.POST.get('name') + remarks = request.POST.get('remarks') + unit_price = request.POST.get('unit_price') + unit = request.POST.get('unit') + description = request.POST.get('description') + quantity = request.POST.get('quantity', 1) + + if name and unit_price and unit: + InfrastructureBudget.objects.create( + budget=budget, + name=name, + remarks=remarks, + unit_price=unit_price, + unit=unit, + description=description, + quantity=quantity + ) + + return redirect('budget_detail', branch_id=branch_id, budget_id=budget_id) + + +def delete_equipment_budget(request, branch_id, budget_id, equipment_budget_id): + branch = Branch.objects.get(pk=branch_id) + budget = Budget.objects.get(pk=budget_id, branch=branch) + equipment_budget = EquipmentBudget.objects.get(pk=equipment_budget_id, budget=budget) + equipment_budget.delete() + return redirect('budget_detail', branch_id=branch_id, budget_id=budget_id) + + +def delete_infrastructure_budget(request, branch_id, budget_id, infrastructure_budget_id): + branch = Branch.objects.get(pk=branch_id) + budget = Budget.objects.get(pk=budget_id, branch=branch) + infrastructure_budget = InfrastructureBudget.objects.get(pk=infrastructure_budget_id, budget=budget) + infrastructure_budget.delete() + return redirect('budget_detail', branch_id=branch_id, budget_id=budget_id)