Django表单集(Formset)仅保存最后一行数据问题求助
解决Django表单集仅保存最后一行数据的问题
问题场景
开发Django项目的GrnCreate视图时,主表单GrnForm的数据填充与保存功能正常,但表单集Grn_LineItemFormset存在异常:提交表单后仅能保存最后一行数据,模板可正常填充数据,但提交后仅主表单和表单集的最后一行数据被持久化。
以下是相关代码:
原视图代码
def GrnCreate(request, **kwargs): pk = kwargs.get('pk') grn_no = GrnModel.objects.filter(company_grn = request.user.first_name).order_by('-id')[:1] form_lpo = get_object_or_404(PurchaseOrderModel, pk = pk) lpo_line_item = form_lpo.purchaseorder_lineitem_set.all() currencies = CurrencyModel.objects.all().filter(company_currency = request.user.first_name) if request.method == 'GET': form = GrnForm(request.GET or None, instance=form_lpo) formset = Grn_LineItemFormset(request.GET or None, queryset=PurchaseOrder_LineItem.objects.none(), instance=form_lpo) elif request.method == 'POST': formset = Grn_LineItemFormset(request.POST) form = GrnForm(request.POST) if form.is_valid(): grnmodel= GrnModel.objects.create(vendor_name=form.data["vendor_name"], grn_number=form.data["grn_number"], lpo_number=form.data["lpo_number"], vat=form.data["vat"], address = form.data["address"], date =form.data["date"], descriptions=form.data["descriptions"], currency = form.data["currency"], ) if formset.is_valid(): for form in formset: type_of_service = form.data["type_of_service"] item = form.data["item"] quantity = form.data["quantity"] rate = form.data["rate"] amount = form.data["amount"] if type_of_service and item and quantity and rate and amount: Grn_LineItem(vendor_name=grnmodel, type_of_service=type_of_service, item=item, quantity=quantity, rate=rate, amount=amount).save() grnmodel.save() return redirect('purchase_order_list') #else: # return redirect('grn_create') context = { "formset": formset, "form": form, "grn_no":grn_no, "form_lpo":form_lpo, "lpo_line_item":lpo_line_item, "currencies":currencies } return render(request, 'inventory/grn_create.html', context)
原模型代码
class GrnModel(models.Model): grn_number = models.CharField(max_length=50) lpo_number = models.CharField(max_length=50) vendor_name = models.CharField(max_length=50) vat = models.CharField(max_length=50) address = models.CharField(max_length=50) date = models.DateField(max_length=50) descriptions = models.CharField(max_length=50) currency = models.CharField(max_length=50) def __str__(self): return str(self.vendor_name) class Meta: db_table = 'grnmodel' class Grn_LineItem(models.Model): vendor_name = models.ForeignKey(GrnModel, on_delete=models.CASCADE) type_of_service = models.TextField(max_length=100, null=True, blank=True) item = models.TextField(max_length=100, null=True, blank=True) quantity = models.DecimalField(max_digits=9, decimal_places=1) rate = models.DecimalField(max_digits=9, decimal_places=2) amount = models.DecimalField(max_digits=9, decimal_places=2) def __str__(self): return str(self.vendor_name) class Meta: db_table="grn_lineitem"
原模板代码
<tbody> {% for lpo_line_item in lpo_line_item %} <tr class="{% cycle row1 row2 %} formset_row-{{ formset.prefix }}"> <td><input name="type_of_service" class="form-control input" value="{{ lpo_line_item.type_of_service }}"></td> <td><input name="item" class="form-control input" value="{{ lpo_line_item.item }}"></td> <td><input name="quantity" class="form-control input quantity_qty_rec" id="qty_rec" value="{{lpo_line_item.quantity}}"></td> <td><input name="rate" class="form-control input rate" value="{{ lpo_line_item.rate }}"></td> <td><input name="amount" class="input grn_amount form-control formset-field" id="grn_amount" value="{{lpo_line_item.amount}}"></td> </tr> {% endfor %} </tbody> </table> {{ formset.management_form }}
问题根源
- 模板字段命名错误:所有行的表单字段
name属性完全相同,提交时浏览器仅保留最后一行的字段值,覆盖了前面行的数据。 - 表单集初始化错误:GET请求时未根据
lpo_line_item的数量预创建对应数量的表单实例,表单集无法识别多行数据结构。 - 视图处理逻辑错误:直接通过
form.data获取值,未使用表单集的cleaned_data;手动创建实例的方式未利用表单集的内置验证与保存机制,容易遗漏数据。
修复方案
1. 修复模板代码
必须使用表单集的前缀和索引确保每行字段名唯一,推荐直接使用表单集的表单字段渲染:
<tbody> {% for form in formset %} {{ form.id }} <!-- 必须包含该字段,表单集依赖它识别实例 --> <tr class="{% cycle row1 row2 %} formset_row-{{ formset.prefix }}"> <td>{{ form.type_of_service }}</td> <td>{{ form.item }}</td> <td>{{ form.quantity }}</td> <td>{{ form.rate }}</td> <td>{{ form.amount }}</td> </tr> {% endfor %} </tbody> </table> {{ formset.management_form }}
若需保留自定义样式手动渲染input,需遵循表单集命名规则设置name属性:
<input name="{{ formset.prefix }}-{{ forloop.counter0 }}-type_of_service" class="form-control input" value="{{ form.type_of_service.value }}">
注:{{ forloop.counter0 }}是从0开始的索引,对应表单集的实例下标;formset.prefix为表单集的默认前缀(通常是form)。
2. 修复视图代码
- GET请求时,根据PO行项数据初始化表单集,预填每行内容
- POST请求时,使用表单集
save(commit=False)方法处理数据,关联外键后保存 - 主表单直接调用
form.save()创建实例,无需手动提取form.data
修改后的视图代码:
def GrnCreate(request, **kwargs): pk = kwargs.get('pk') grn_no = GrnModel.objects.filter(company_grn = request.user.first_name).order_by('-id')[:1] form_lpo = get_object_or_404(PurchaseOrderModel, pk = pk) lpo_line_items = form_lpo.purchaseorder_lineitem_set.all() currencies = CurrencyModel.objects.all().filter(company_currency=request.user.first_name) if request.method == 'GET': # 初始化主表单,从PO实例获取数据 form = GrnForm(instance=form_lpo) # 根据PO行项生成表单集初始化数据 formset_initial = [ { 'type_of_service': item.type_of_service, 'item': item.item, 'quantity': item.quantity, 'rate': item.rate, 'amount': item.amount } for item in lpo_line_items ] formset = Grn_LineItemFormset(initial=formset_initial) elif request.method == 'POST': form = GrnForm(request.POST) formset = Grn_LineItemFormset(request.POST) if form.is_valid() and formset.is_valid(): # 保存主表单实例 grnmodel = form.save() # 处理表单集,关联GrnModel外键后保存 for line_form in formset: if line_form.cleaned_data: # 跳过空行 line_item = line_form.save(commit=False) line_item.vendor_name = grnmodel line_item.save() return redirect('purchase_order_list') context = { "formset": formset, "form": form, "grn_no": grn_no, "form_lpo": form_lpo, "lpo_line_item": lpo_line_items, "currencies": currencies } return render(request, 'inventory/grn_create.html', context)
3. 额外检查
确保表单集定义正确,指定关联模型并设置合适的extra值:
from django.forms import modelformset_factory Grn_LineItemFormset = modelformset_factory( Grn_LineItem, fields=('type_of_service', 'item', 'quantity', 'rate', 'amount'), extra=0 # 无需额外空行,根据PO行项数量初始化 )
内容的提问来源于stack exchange,提问作者Barak
相关产品推荐
相关产品推荐

