Django 新增Bill时如何关联创建Activity实例并导出存储PDF/Word文档
Django 三个需求实现方案
三个需求均可以实现,以下是分步实现指导:
一、前置:先确认模型关系定义正确
首先需要明确三个模型的关联关系,示例定义如下:
from django.db import models from django.utils import timezone class Clients(models.Model): name = models.CharField(max_length=100) # 其他客户字段 class Bill(models.Model): client = models.ForeignKey(Clients, on_delete=models.PROTECT, related_name="bills") create_time = models.DateTimeField(default=timezone.now) # 存储导出文件的字段 word_file = models.FileField(upload_to="bill/word/", null=True, blank=True) pdf_file = models.FileField(upload_to="bill/pdf/", null=True, blank=True) class Activity(models.Model): bill = models.ForeignKey(Bill, on_delete=models.CASCADE, related_name="activities") content = models.TextField() # 活动内容 cost_hour = models.FloatField() # 耗时 cost_amount = models.DecimalField(max_digits=10, decimal_places=2) # 成本
二、需求1:自动生成Bill的ID(对应主键pk)
- Django 默认的自增主键
pk只有在实例写入数据库后才会生成,不需要在用户点击保存前提前生成:- 如果仅需要给用户展示正式ID,可在Bill保存成功后,把生成的pk返回给前端展示即可
- 如果需要提前给用户展示临时单号,可由前端生成随机字符串作为临时标识,保存后替换为正式pk即可
- 如果业务需要自定义ID规则(如
BL202405010001),可以重写Bill的save方法,在首次保存时生成自定义ID赋值给主键字段。
三、需求2:保存Bill时同步存入关联的Activity实例
推荐使用Django自带的FormSet实现多Activity数据的提交和校验,步骤如下:
- 后端定义Form和FormSet:
from django import forms from .models import Bill, Activity class BillForm(forms.ModelForm): class Meta: model = Bill fields = ["client"] # 其他Bill需要填写的字段 # 定义Activity的内联FormSet,绑定Bill实例 ActivityFormSet = forms.inlineformset_factory( Bill, Activity, fields=["content", "cost_hour", "cost_amount"], extra=1, # 默认显示1行空白行 can_delete=True )
- 前端表单处理:
动态新增行时,按照FormSet的命名规则设置输入框的name属性,格式为activities-0-content、activities-1-content,同时要同步更新隐藏输入框id_activities-TOTAL_FORMS的值为当前总行数。 - 后端视图保存逻辑:
from django.shortcuts import render, redirect from django.db import transaction from .forms import BillForm, ActivityFormSet @transaction.atomic # 事务保证数据一致性,Bill保存失败则Activity也不会入库 def create_bill(request): if request.method == "POST": bill_form = BillForm(request.POST) if bill_form.is_valid(): # 先保存Bill实例,此时生成pk bill = bill_form.save() # 绑定Activity数据到FormSet,关联刚保存的Bill实例 activity_formset = ActivityFormSet(request.POST, instance=bill) if activity_formset.is_valid(): activity_formset.save() # 保存成功后跳转到详情页或者返回成功信息 return redirect("bill_detail", pk=bill.pk) else: bill_form = BillForm() activity_formset = ActivityFormSet() return render(request, "bill/create.html", { "bill_form": bill_form, "activity_formset": activity_formset })
四、需求3:保存成功后自动导出Word/PDF并存入数据库
- 依赖库安装:
- 导出Word用
python-docx:pip install python-docx - 导出PDF用
xhtml2pdf(可以直接渲染Django模板生成PDF,更简单):pip install xhtml2pdf
- 导出Word用
- 导出逻辑可以通过
post_save信号触发,或者直接写在视图保存成功的逻辑之后,示例PDF导出逻辑:
from django.core.files.base import ContentFile from xhtml2pdf import pisa from django.template.loader import render_to_string def export_bill_pdf(bill): # 渲染Bill详情模板为HTML html_content = render_to_string("bill/pdf_template.html", {"bill": bill}) # 生成PDF文件 result = ContentFile() pisa.CreatePDF(html_content, dest=result) # 保存到Bill的pdf_file字段 bill.pdf_file.save(f"bill_{bill.pk}.pdf", result, save=False) bill.save(update_fields=["pdf_file"])
在视图的activity_formset.save()之后调用export_bill_pdf(bill)即可完成自动导出存储。
新手注意事项
- 动态新增表格行时不要忘记更新FormSet的总数量隐藏字段,否则后端只能收到默认数量的Activity数据
- 开发环境需要在
settings.py中配置MEDIA_ROOT和MEDIA_URL,才能正常访问存储的导出文件 - 导出的模板可以自己根据业务需要调整格式,不需要依赖外部服务
内容的提问来源于stack exchange,提问作者lag37
相关产品推荐
相关产品推荐

