Django试剂订单管理:动态字段添加与订单合并实现问询
Hey there! Let's work through your reagent order tracking system problem step by step—focusing on fixing model relationships first, then handling forms and order merging logic.
1. Model Structure Overhaul (The Core Fix)
The root issue here is that your current model setup mixes order metadata, product details, and manufacturer info together, leading to duplicate data and merge headaches. Let's split things into clean, reusable models:
a. Manufacturer & Product Models (Fix Data Duplication)
First, isolate manufacturer details (since their delivery address and payment account are fixed) and standardize product data:
from django.db import models from django.contrib.auth.models import User class Manufacturer(models.Model): name = models.CharField(max_length=255, unique=True) delivery_address = models.TextField() payment_account = models.CharField(max_length=255) contact_person = models.CharField(max_length=100, blank=True) def __str__(self): return self.name class Product(models.Model): name = models.CharField(max_length=255) manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) spec = models.CharField(max_length=100, blank=True) # e.g., "100mL" reference_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) class Meta: # Prevent duplicate products from the same manufacturer unique_together = ('name', 'manufacturer', 'spec') def __str__(self): return f"{self.name} ({self.spec}) - {self.manufacturer.name}"
This way, you only enter manufacturer and product details once, and users select from pre-existing entries (no more messy, inconsistent product data).
b. Order & OrderItem Models (Separate Orders from Line Items)
Split orders into a parent Order (tracking user/submission status) and child OrderItem (tracking individual reagents in an order):
class Order(models.Model): STATUS_CHOICES = [ ('submitted', 'Submitted'), ('processed', 'Processed'), ('merged', 'Merged into Invoice'), ] user = models.ForeignKey(User, on_delete=models.CASCADE) submitted_at = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted') # Add user-specific delivery address here ONLY if it's different from the manufacturer's # user_delivery_address = models.TextField(blank=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.PositiveIntegerField() actual_price = models.DecimalField(max_digits=10, decimal_places=2) def __str__(self): return f"{self.quantity}x {self.product.name}"
c. Invoice & InvoiceItem Models (Handle Merged Orders)
Create an Invoice model to group eligible OrderItems together, tied to a single manufacturer:
class Invoice(models.Model): manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) invoice_number = models.CharField(max_length=50, unique=True, blank=True) total_amount = models.DecimalField(max_digits=12, decimal_places=2, editable=False) def save(self, *args, **kwargs): # Auto-calculate total when saving the invoice if not self.invoice_number: # Generate a simple invoice number (customize as needed) self.invoice_number = f"INV-{self.created_at.strftime('%Y%m%d')}-{self.pk}" self.total_amount = sum(item.quantity * item.actual_price for item in self.invoice_items.all()) super().save(*args, **kwargs) class InvoiceItem(models.Model): invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='invoice_items') order_item = models.ForeignKey(OrderItem, on_delete=models.CASCADE) def __str__(self): return f"{self.order_item.product.name} (from Order #{self.order_item.order.pk})"
2. Form Implementation (Fix Duplicate Data Entry)
You mentioned concerns about formsets—inline formsets are exactly what you need here, because they let users add multiple reagent line items to a single order, no repeated order metadata.
User Order Submission Form
from django import forms from .models import Order, OrderItem class OrderItemForm(forms.ModelForm): class Meta: model = OrderItem fields = ['product', 'quantity', 'actual_price'] widgets = { 'product': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'min': 1, 'class': 'form-control'}), 'actual_price': forms.NumberInput(attrs={'step': '0.01', 'class': 'form-control'}), } # Create an inline formset for OrderItems tied to an Order OrderItemFormSet = forms.inlineformset_factory( Order, OrderItem, form=OrderItemForm, extra=1, # Show 1 line item by default can_delete=True, # Let users remove unwanted line items min_num=1, # Require at least 1 line item validate_min=True ) class OrderForm(forms.ModelForm): class Meta: model = Order fields = [] # No extra fields—user and status are set automatically # In your view: def submit_order(request): if request.method == 'POST': order_form = OrderForm(request.POST) formset = OrderItemFormSet(request.POST) if order_form.is_valid() and formset.is_valid(): # Save the order first (without committing to DB yet) order = order_form.save(commit=False) order.user = request.user order.save() # Save all line items tied to the new order formset.instance = order formset.save() # Redirect to order confirmation page return redirect('order_detail', order_id=order.pk) else: order_form = OrderForm() formset = OrderItemFormSet() return render(request, 'submit_order.html', { 'order_form': order_form, 'formset': formset })
In your template, you can render the formset with a loop, and add a "Add More Reagent" button using JavaScript to dynamically add new line items (plenty of simple Django formset JS examples out there).
3. Order Merging Logic (For Admins)
Now, merging orders becomes straightforward—you just need to group OrderItems by their manufacturer, then link them to a new Invoice:
Admin Merge View Example
from django.contrib import messages from itertools import groupby def merge_orders(request): if request.method == 'POST': order_ids = request.POST.getlist('selected_orders') pending_orders = Order.objects.filter(id__in=order_ids, status='submitted').prefetch_related('items__product__manufacturer') # Validate all selected orders belong to the same manufacturer manufacturers = set() for order in pending_orders: for item in order.items.all(): manufacturers.add(item.product.manufacturer.id) if len(manufacturers) != 1: messages.error(request, "Only orders from the same manufacturer can be merged.") return redirect('merge_orders') # Create the invoice target_manufacturer = Manufacturer.objects.get(id=next(iter(manufacturers))) new_invoice = Invoice.objects.create(manufacturer=target_manufacturer) # Link all eligible OrderItems to the invoice for order in pending_orders: for item in order.items.all(): InvoiceItem.objects.create(invoice=new_invoice, order_item=item) # Update order status to mark as merged order.status = 'merged' order.save() messages.success(request, f"Successfully merged orders into Invoice #{new_invoice.invoice_number}") return redirect('invoice_detail', invoice_id=new_invoice.pk) # Get all pending orders, grouped by manufacturer for easy selection pending_orders = Order.objects.filter(status='submitted').prefetch_related('items__product__manufacturer') # Sort orders by manufacturer first to use groupby sorted_orders = sorted(pending_orders, key=lambda o: o.items.first().product.manufacturer.id if o.items.exists() else '') grouped_orders = groupby(sorted_orders, key=lambda o: o.items.first().product.manufacturer if o.items.exists() else None) return render(request, 'merge_orders.html', {'grouped_orders': grouped_orders})
In the admin template, you can display orders grouped by manufacturer, with checkboxes to select which ones to merge into a single invoice.
Quick Extra Tips
- Product Data Standardization: Add an admin page for
ProductandManufacturerso lab admins can pre-enter all common reagents—users just select from the list instead of typing, eliminating messy data. - Dynamic Product Addition: If users need to request a reagent not in the system, add a small modal form to the order page that lets them submit a new product request (which an admin can approve later).
内容的提问来源于stack exchange,提问作者jpm92

