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

Django 3.2中如何基于含多对多字段的模型渲染可批量输入的表格

Django 3.2 多产品销售计划批量录入视图实现方案

前置优化提示:当前SalesPlan模型使用OneToOneField关联Customer和Product不符合业务逻辑,会限制一个客户/一个产品全局只能有一条销售计划记录。建议先调整模型字段:

class SalesPlan(models.Model):
    # 把OneToOneField改为ForeignKey
    customer = models.ForeignKey(Customer, null=True, on_delete=RESTRICT)
    period = models.DateField()
    product = models.ForeignKey(Product, null=True, on_delete=RESTRICT)
    qty = models.DecimalField(max_digits=10, decimal_places=2, null=True)
    
    # 新增联合唯一约束,保证同一个客户、同一个周期、同一个产品只有一条计划
    class Meta:
        unique_together = [['customer', 'period', 'product']]

具体实现步骤

步骤1:定义仅开放数量字段的ModelForm

创建仅暴露qty字段的表单,其余关联字段设为隐藏控件,避免用户修改:

from django import forms
from .models import SalesPlan

class SalesPlanForm(forms.ModelForm):
    class Meta:
        model = SalesPlan
        fields = ['qty', 'customer', 'period', 'product']
        widgets = {
            'customer': forms.HiddenInput(),
            'period': forms.HiddenInput(),
            'product': forms.HiddenInput(),
        }

步骤2:视图层构造动态Formset

使用modelformset_factory构造表单集,表单数量和客户关联的产品数量完全匹配:

from django.forms import modelformset_factory
from django.shortcuts import render, get_object_or_404, redirect
from .models import Customer, SalesPlan

def sales_plan_create(request, customer_code, period):
    # 获取当前操作的客户实例
    customer = get_object_or_404(Customer, customer_code=customer_code)
    # 获取客户名下所有可售产品
    portfolio_products = customer.customer_portfolio.all()
    # 构造Formset,关闭自动查询已有数据(新增场景),extra数量等于产品数
    SalesPlanFormSet = modelformset_factory(
        SalesPlan,
        form=SalesPlanForm,
        extra=len(portfolio_products),
        can_delete=False
    )
    
    if request.method == 'POST':
        formset = SalesPlanFormSet(request.POST, queryset=SalesPlan.objects.none())
        if formset.is_valid():
            instances = formset.save(commit=False)
            # 可选:校验每个实例的product属于当前客户的portfolio
            for instance in instances:
                instance.save()
            # 保存成功后跳转逻辑自行补充
            return redirect('sales_plan_list')
    else:
        formset = SalesPlanFormSet(queryset=SalesPlan.objects.none())
        # 为每个表单预填充固定字段
        for form, product in zip(formset, portfolio_products):
            form.initial['customer'] = customer
            form.initial['period'] = period
            form.initial['product'] = product
    
    # 把产品列表传到模板,用于渲染产品名称、规格等只读信息
    return render(request, 'sales_plan_form.html', {
        'formset': formset,
        'customer': customer,
        'period': period,
        'portfolio_products': portfolio_products
    })

如果是编辑已有销售计划的场景,仅需将Formset的queryset改为SalesPlan.objects.filter(customer=customer, period=period),同时将extra设为0即可。

步骤3:模板层渲染为表格结构

在模板中将表单集和产品信息一一对应,渲染为每行一个产品的表格,仅数量列可编辑:

<form method="post">
    {% csrf_token %}
    {{ formset.management_form }}
    <table>
        <thead>
            <tr>
                <th>产品编码</th>
                <th>产品名称</th>
                <th>单位</th>
                <th>计划数量</th>
            </tr>
        </thead>
        <tbody>
            {% for form, product in formset|zip:portfolio_products %}
            <tr>
                <td>{{ product.item_code }}</td>
                <td>{{ product.item_name }}</td>
                <td>{{ product.unit_of_measure }}</td>
                <td>
                    {{ form.qty }}
                    <!-- 渲染隐藏字段 -->
                    {% for hidden in form.hidden_fields %}
                        {{ hidden }}
                    {% endfor %}
                    <!-- 显示字段校验错误 -->
                    {% for error in form.qty.errors %}
                        <span class="error">{{ error }}</span>
                    {% endfor %}
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    <button type="submit">保存</button>
</form>

注:如果模板不支持zip过滤器,可在Django后台注册自定义过滤器,或者将form和product提前打包为元组列表传到模板。


内容的提问来源于stack exchange,提问作者Alexander Radovich

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 21:48:03