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
相关产品推荐
相关产品推荐

