В одной таблице должны храниться документы, во второй - строки табличной части (всех документов).
models.py:
from django.db import models
from django.db.models import Max, Sum
from django.utils.translation import ugettext_lazy as _
class Product(models.Model):
name = models.CharField(
_('Name'),
max_length=200,
db_index=True,
)
def __str__(self):
return self.name
class Document(models.Model):
INVOICE = 'I'
WAYBILL = 'W'
TYPE_CHOICES = (
(INVOICE, _('Invoice')),
(WAYBILL, _('Waybill')),
)
type = models.CharField(
_('Type'),
max_length=1,
choices=TYPE_CHOICES,
)
number = models.CharField(
_('Number'),
blank=True,
max_length=50,
)
created_at = models.DateTimeField(
_('Created'),
auto_now_add=True,
db_index=True,
)
@property
def total(self):
return self.items.aggregate(sum=Sum('total'))['sum']
class Meta:
ordering = ['-created_at']
class DocumentItem(models.Model):
document = models.ForeignKey(
Document,
models.CASCADE,
related_name='items',
)
position = models.PositiveIntegerField(
verbose_name=_('Position'),
editable=False,
db_index=True,
)
product = models.ForeignKey(
Product,
models.PROTECT,
)
price = models.DecimalField(
_('Price'),
max_digits=12,
decimal_places=2,
)
quantity = models.DecimalField(
_('Quantity'),
max_digits=10,
decimal_places=3,
)
total = models.DecimalField(
_('Total'),
max_digits=12,
decimal_places=2,
)
def save(self, *args, **kwargs):
if not self.position:
position = self.document.items.aggregate(Max('position'))['position__max'] or 0
self.position = position + 1
super(DocumentItem, self).save(*args, **kwargs)
admin.py:
from django.contrib import admin
from .models import Product, Document, DocumentItem
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
pass
class DocumentItemInline(admin.TabularInline):
model = DocumentItem
fields = (
'position',
'product',
'price',
'quantity',
'total',
)
readonly_fields = (
'position',
)
ordering = ['position']
@admin.register(Document)
class DocumentAdmin(admin.ModelAdmin):
inlines = [
DocumentItemInline,
]
list_display = (
'type',
'number',
'created_at',
'total',
)
list_filter = (
'type',
)
search_fields = (
'=number',
)
Стандартная админка - для примера, вообще она не для этого (обычным пользователям она не должна быть доступна). Но реальные формы строятся по тому же принципу.
Код накидал прям здесь, не проверял, поэтому возможны незначительные ошибки.