Django inline_formset:基于另一字段设置字段的QuerySet
嘿,我来帮你搞定Django inline formset里动态设置字段QuerySet的问题!结合你给出的模型结构,我猜你应该是想在InvoiceItem的行内表单里,选了某个Item之后,对应的税组选项只显示这个商品关联的那些——这应该是最常见的场景,如果不是你想要的,随时调整方向哈~
首先先补全你没写完的InvoiceItem模型(毕竟要做关联得先有这个模型):
class InvoiceItem(models.Model): invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE) item = models.ForeignKey(Item, on_delete=models.CASCADE) tax_group = models.ForeignKey(TaxGroup, on_delete=models.CASCADE) quantity = models.IntegerField(default=1) # 可以加单价、小计这类业务字段
接下来分四步实现动态更新QuerySet:
1. 自定义InvoiceItemForm处理初始QuerySet
我们需要在表单初始化时,根据已有实例(编辑场景)或者默认情况,设置tax_group的初始可选范围:
from django import forms from .models import InvoiceItem, TaxGroup class InvoiceItemForm(forms.ModelForm): class Meta: model = InvoiceItem fields = ['item', 'tax_group', 'quantity'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 编辑场景:如果已经选了Item,只显示该Item关联的税组 if self.instance.pk: self.fields['tax_group'].queryset = self.instance.item.tax_group.all() # 新建场景:默认空选项,等用户选Item后再加载 else: self.fields['tax_group'].queryset = TaxGroup.objects.none() # 后端验证!防止前端篡改数据,必须加 def clean(self): cleaned_data = super().clean() selected_item = cleaned_data.get('item') selected_tax = cleaned_data.get('tax_group') if selected_item and selected_tax: if selected_tax not in selected_item.tax_group.all(): raise forms.ValidationError("该税组不属于你选中的商品哦") return cleaned_data
2. 创建关联的Inline Formset
用Django的inlineformset_factory把自定义表单和Invoice、InvoiceItem关联起来:
from django.forms import inlineformset_factory from .models import Invoice from .forms import InvoiceItemForm InvoiceItemFormSet = inlineformset_factory( Invoice, InvoiceItem, form=InvoiceItemForm, extra=1, # 默认显示1个空的行内表单 can_delete=True # 允许删除行内项 )
3. 视图层传递前端需要的映射数据
我们要把所有Item对应的TaxGroup数据传给前端,方便JS动态更新选项:
from django.shortcuts import render from .models import Invoice, Item def invoice_edit(request, pk): invoice = Invoice.objects.get(pk=pk) formset = InvoiceItemFormSet(instance=invoice) # 构建Item到TaxGroup的映射,转成字典给前端 item_tax_mapping = { item.pk: list(item.tax_group.values('pk', 'name')) for item in Item.objects.all() } return render(request, 'invoice_edit.html', { 'invoice': invoice, 'formset': formset, 'item_tax_mapping': item_tax_mapping, })
4. 模板层添加JS实现动态更新
在模板里监听Item选择框的变化,动态替换tax_group的选项:
<form method="post"> {% csrf_token %} {{ formset.management_form }} {% for form in formset %} <div class="invoice-item-row"> {{ form.as_p }} {% if form.instance.pk %} {{ form.DELETE }} 删除该项 {% endif %} </div> {% endfor %} <button type="submit">保存发票</button> </form> <script> // 从视图拿到的Item-TaxGroup映射 const itemTaxMap = {{ item_tax_mapping|safe }}; // 给每个Item选择框绑定事件 document.querySelectorAll('select[name$="-item"]').forEach(itemSelect => { // 找到当前行对应的tax_group选择框 const taxSelect = itemSelect.form.querySelector(`select[name$="-tax_group"]`); // 页面加载时如果已有选中的Item,初始化TaxGroup选项 if (itemSelect.value) { updateTaxOptions(itemSelect.value, taxSelect); } // 监听Item选择变化 itemSelect.addEventListener('change', () => { updateTaxOptions(itemSelect.value, taxSelect); }); }); // 更新TaxGroup选项的工具函数 function updateTaxOptions(itemId, taxSelect) { // 清空现有选项 taxSelect.innerHTML = ''; if (!itemId) return; // 遍历当前Item对应的TaxGroup,生成新选项 itemTaxMap[itemId].forEach(tax => { const option = document.createElement('option'); option.value = tax.pk; option.textContent = `${tax.name}`; taxSelect.appendChild(option); }); } </script>
小提醒:如果是新建发票的视图,逻辑和编辑几乎一样,只要把invoice = Invoice.objects.get(pk=pk)换成invoice = Invoice()即可。
内容的提问来源于stack exchange,提问作者art
相关产品推荐
相关产品推荐

