You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题根源

  1. 模板字段命名错误:所有行的表单字段name属性完全相同,提交时浏览器仅保留最后一行的字段值,覆盖了前面行的数据。
  2. 表单集初始化错误:GET请求时未根据lpo_line_item的数量预创建对应数量的表单实例,表单集无法识别多行数据结构。
  3. 视图处理逻辑错误:直接通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 04:55:07