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

Django CRM系统订单多商品关联:模型与动态视图实现问询

你的CRM订单多商品关联问题解决方案

一、模型设计:ForeignKey的用法是否合理?

嘿,你的思路完全在线!订单和商品属于多对多关系(一个订单包含多个商品,一个商品可属于多个订单),通过OrderItem中间表关联并存储quantity(数量)是行业标准做法。不过可以给你优化下,用Django的ManyToManyField结合through参数更贴合框架设计:

修改Order模型:

class Order(models.Model):
    STATUS = (
        ('New', 'New'),
        ('Work in progress', 'Work in progress'),
        ('completed', 'completed'),
    )
    customer = models.ForeignKey(Customer, null=True, on_delete=models.CASCADE)
    date_created = models.DateTimeField(auto_now_add=True, null=True)
    status = models.CharField(max_length=200, null=True, choices=STATUS)
    # 新增多对多关联,指定中间表为OrderItem
    products = models.ManyToManyField(Product, through='OrderItem')
    
    def date_createdview(self):
        return self.date_created.strftime('%d/%m/%Y')

这样调整的好处:

  • 符合Django多对多关系的语义,你可以直接用order.products.all()获取订单下的所有商品,也能反向用product.order_set.all()查询包含该商品的订单
  • OrderItem作为中间表的功能完全保留,后续还能扩展添加下单时的商品单价(避免商品价格变动影响历史订单)等字段

你的原OrderItem模型不需要修改,through参数就是指定它来维护关联关系。

二、动态添加商品的订单视图与表单实现

要实现「创建订单时动态添加商品、实时显示列表、保存订单后继续添加」的效果,咱们用Django的表单集(FormSet) + 前端JavaScript就能搞定。

1. 定义表单

在forms.py里创建订单和订单商品的表单:

from django import forms
from .models import Order, OrderItem

class OrderForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ['customer', 'status']
        widgets = {
            'customer': forms.Select(attrs={'class': 'form-control'}),
            'status': forms.Select(attrs={'class': 'form-control'}),
        }

class OrderItemForm(forms.ModelForm):
    class Meta:
        model = OrderItem
        fields = ['product', 'quantity']
        widgets = {
            'product': forms.Select(attrs={'class': 'form-control'}),
            'quantity': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
        }

2. 构建视图

编写处理订单创建和商品添加的视图函数:

from django.forms import formset_factory
from django.shortcuts import render, redirect, get_object_or_404
from .models import Order, OrderItem
from .forms import OrderForm, OrderItemForm
from django.contrib.auth.decorators import login_required

@login_required(login_url="login")
def create_order(request):
    # 创建订单商品表单集,默认显示1个商品输入框
    OrderItemFormSet = formset_factory(OrderItemForm, extra=1)
    
    if request.method == 'POST':
        order_form = OrderForm(request.POST)
        item_formset = OrderItemFormSet(request.POST)
        
        if order_form.is_valid() and item_formset.is_valid():
            # 先保存订单(暂不提交到数据库)
            order = order_form.save(commit=False)
            order.save()
            
            # 遍历表单集,保存每个商品项并关联到当前订单
            for form in item_formset:
                if form.cleaned_data:  # 跳过空表单
                    item = form.save(commit=False)
                    item.order = order
                    item.save()
            
            # 跳转至订单详情页,支持继续添加商品
            return redirect('order_detail', order_id=order.id)
    else:
        order_form = OrderForm()
        item_formset = OrderItemFormSet()
    
    context = {
        'order_form': order_form,
        'item_formset': item_formset
    }
    return render(request, 'accounts/create_order.html', context)

# 订单详情页,支持后续添加商品
@login_required(login_url="login")
def order_detail(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    OrderItemFormSet = formset_factory(OrderItemForm, extra=1)
    
    if request.method == 'POST':
        item_formset = OrderItemFormSet(request.POST)
        if item_formset.is_valid():
            for form in item_formset:
                if form.cleaned_data:
                    item = form.save(commit=False)
                    item.order = order
                    item.save()
            return redirect('order_detail', order_id=order.id)
    else:
        item_formset = OrderItemFormSet()
    
    # 获取订单已有的商品项
    order_items = OrderItem.objects.filter(order=order)
    context = {
        'order': order,
        'item_formset': item_formset,
        'order_items': order_items
    }
    return render(request, 'accounts/order_detail.html', context)

3. 前端模板实现动态添加

先写create_order.html的核心内容:

<form method="POST">
    {% csrf_token %}
    <div class="mb-4">
        <h3>订单基础信息</h3>
        {{ order_form.as_p }}
    </div>
    
    <div class="mb-4">
        <h3>订单商品</h3>
        <div id="item-forms">
            {{ item_formset.management_form }}
            {% for form in item_formset %}
                <div class="item-form border p-3 mb-2">
                    {{ form.as_p }}
                    <button type="button" class="remove-form btn btn-sm btn-danger">删除</button>
                </div>
            {% endfor %}
        </div>
        <button type="button" id="add-item" class="btn btn-sm btn-primary mt-2">添加商品</button>
    </div>
    
    <button type="submit" class="btn btn-success">保存订单</button>
</form>

<script>
// 动态添加商品表单逻辑
document.getElementById('add-item').addEventListener('click', function() {
    const itemFormsContainer = document.getElementById('item-forms');
    const totalFormsInput = document.getElementById('id_form-TOTAL_FORMS');
    const currentFormCount = parseInt(totalFormsInput.value);
    
    // 复制第一个表单作为模板
    const formTemplate = document.querySelector('.item-form').cloneNode(true);
    // 修改表单字段的name和id属性,替换索引值
    formTemplate.querySelectorAll('input, select').forEach(element => {
        const newName = element.name.replace('form-0-', `form-${currentFormCount}-`);
        element.name = newName;
        element.id = `id_${newName}`;
        // 清空输入值
        element.value = element.tagName === 'SELECT' ? '' : element.value;
    });
    
    // 绑定删除按钮事件
    formTemplate.querySelector('.remove-form').addEventListener('click', function() {
        this.parentElement.remove();
        totalFormsInput.value = parseInt(totalFormsInput.value) - 1;
    });
    
    itemFormsContainer.appendChild(formTemplate);
    totalFormsInput.value = currentFormCount + 1;
});

// 初始表单的删除按钮事件绑定
document.querySelectorAll('.remove-form').forEach(btn => {
    btn.addEventListener('click', function() {
        this.parentElement.remove();
        const totalFormsInput = document.getElementById('id_form-TOTAL_FORMS');
        totalFormsInput.value = parseInt(totalFormsInput.value) - 1;
    });
});
</script>

order_detail.html可以复用上面的JS逻辑,同时展示已有的订单商品:

<div class="mb-4">
    <h2>订单 #{{ order.id }}</h2>
    <p>客户: {{ order.customer.name }}</p>
    <p>状态: {{ order.status }}</p>
    <p>创建时间: {{ order.date_createdview }}</p>
</div>

<div class="mb-4">
    <h3>已添加商品</h3>
    <ul class="list-group">
        {% for item in order_items %}
            <li class="list-group-item">
                {{ item.product.name }} - 数量: {{ item.quantity }} - 单价: {{ item.product.price }}
            </li>
        {% empty %}
            <li class="list-group-item">暂无商品</li>
        {% endfor %}
    </ul>
</div>

<div class="mb-4">
    <h3>添加新商品</h3>
    <form method="POST">
        {% csrf_token %}
        {{ item_formset.management_form }}
        <div id="item-forms">
            {% for form in item_formset %}
                <div class="item-form border p-3 mb-2">
                    {{ form.as_p }}
                    <button type="button" class="remove-form btn btn-sm btn-danger">删除</button>
                </div>
            {% endfor %}
        </div>
        <button type="button" id="add-item" class="btn btn-sm btn-primary mt-2">添加商品</button>
        <button type="submit" class="btn btn-success mt-2">保存商品</button>
    </form>
</div>

<!-- 复制上面的动态添加JS代码即可 -->
<script>
// 这里粘贴之前的JS逻辑
</script>

额外优化建议

  • 给OrderItem添加price字段,存储下单时的商品价格,避免后续商品调价影响历史订单的金额计算
  • 前端用select2插件优化商品选择下拉框,支持搜索,提升用户体验
  • 给Order模型添加总价计算属性:
class Order(models.Model):
    # ... 其他字段 ...
    @property
    def total_price(self):
        return sum(item.product.price * item.quantity for item in self.orderitem_set.all())

内容的提问来源于stack exchange,提问作者Gorge Tor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 20:52:38