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

Django表单ModelChoiceField报错与外键IntegrityError问题求解

问题根因
  • Cannot assign...must be a instance报错:给ForeignKey字段传了对象ID/字符串值而非对应Inventory模型实例,且外键配置default=''属于无效配置——外键存储的是关联表主键值,空值合法取值为None而非空字符串,这也是后续触发数据库约束报错的核心诱因。
  • NOT NULL constraint failed报错:仅给外键字段配置了blank=True,未配置null=True。blank=True仅控制表单层校验允许空值,数据库层字段仍为非空约束,空字符串不符合外键字段的存储类型要求,写入时直接触发约束报错。
模型层修复方案

外键字段必须同时配置null=True、blank=True,空默认值使用None,删除无效的default=''配置,参考代码如下:

# models.py
from django.db import models

class Inventory(models.Model):
    product_number = models.CharField(max_length=50, primary_key=True)
    title = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    detail = models.TextField(blank=True)

    def __str__(self):
        return self.title

class Invoice(models.Model):
    # 10个行字段按相同规则配置,注意related_name不能重复
    line_one = models.ForeignKey(Inventory, on_delete=models.SET_NULL, null=True, blank=True, related_name='inv_line1')
    line_one_qty = models.IntegerField(default=1)
    line_one_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    line_one_subtotal = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    # line_two 到 line_ten 参照line_one结构补全即可
    # 通用业务字段
    customer = models.CharField(max_length=200)
    invoice_date = models.DateField()
    total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    is_paid = models.BooleanField(default=False)

修改完模型必须依次执行python manage.py makemigrations、python manage.py migrate同步数据库结构,否则约束报错不会消失。

表单层修复方案

不要手动重写ModelForm的外键字段,ModelForm自带的ModelChoiceField会自动完成ID到模型实例的转换,之前的转换报错本质是模型配置错误导致的。单价字段不需要提前写死静态选项,通过前端动态填充即可:

# forms.py
from django import forms
from .models import Invoice

class InvoiceForm(forms.ModelForm):
    class Meta:
        model = Invoice
        fields = '__all__'
        widgets = {
            'invoice_date': forms.DateInput(attrs={'type': 'date'}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 给行字段绑定统一class,方便前端做事件绑定
        for i in range(1, 11):
            self.fields[f'line_{i}'].widget.attrs = {
                'class': 'product-select',
                'data-line-num': i
            }
            self.fields[f'line_{i}_price'].widget.attrs = {
                'class': 'product-price',
                'readonly': True
            }
视图层修复方案

表单校验通过后,先计算每行的单价、小计和总金额再保存,空行自动置为NULL不会触发约束报错:

# views.py
from django.shortcuts import render, redirect
from .forms import InvoiceForm
from .models import Inventory

def add_invoice(request):
    if request.method == 'POST':
        form = InvoiceForm(request.POST)
        if form.is_valid():
            invoice = form.save(commit=False)
            total = 0
            for i in range(1, 11):
                line_product = getattr(invoice, f'line_{i}')
                qty = getattr(invoice, f'line_{i}_qty')
                if line_product:
                    price = line_product.price
                    subtotal = price * qty
                    setattr(invoice, f'line_{i}_price', price)
                    setattr(invoice, f'line_{i}_subtotal', subtotal)
                    total += subtotal
                else:
                    setattr(invoice, f'line_{i}_price', 0)
                    setattr(invoice, f'line_{i}_subtotal', 0)
            invoice.total_amount = total
            invoice.save()
            return redirect('invoice_list')
    else:
        form = InvoiceForm()
    # 传递商品价格映射给模板,用于前端联动
    product_list = list(Inventory.objects.all().values('product_number', 'price'))
    return render(request, 'add_invoice.html', {'form': form, 'product_list': product_list})
选择商品自动填充单价的前端实现

不需要额外写接口,直接在模板中嵌入少量原生JS即可实现联动效果:

<!-- 在add_invoice.html模板底部添加以下脚本 -->
<script>
// 构造商品ID到单价的映射对象
const priceMap = {}
{% for product in product_list %}
priceMap['{{product.product_number}}'] = {{product.price}}
{% endfor %}

// 给所有商品选择框绑定切换事件
document.querySelectorAll('.product-select').forEach(selectEl => {
    selectEl.addEventListener('change', function() {
        const lineNo = this.dataset.lineNum
        const selectedPid = this.value
        const priceInput = document.querySelector(`#id_line_${lineNo}_price`)
        priceInput.value = selectedPid && priceMap[selectedPid] ? priceMap[selectedPid] : 0
        // 可在此处扩展自动计算小计、总金额的逻辑
    })
})
</script>
长期结构优化建议

当前固定10个行字段的设计不符合数据库设计范式,扩展性极差,后续如果需要支持超过10行的发票必须修改表结构。推荐拆分为两张表:Invoice主表仅存储发票公共信息(客户、日期、总金额、支付状态等),新增InvoiceLine行项目表通过外键关联Invoice,单张发票可以对应任意数量的行项目,后续维护成本会大幅降低。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 16:45:45