Billing or orders management is a typical use case in most applications, there are many Saas solutions for this but still there can be situations where you need an in-house solution, and it can be tricky and time-consuming to make a design thats efficient and needs least queries. This is a basic model design you may add or modify this as per requirement.

The design includes 4 models

  • Items : Stores the items that can be order. Not required but provides consistency.
  • Order: Stores order information and functions ( Total, Subtotal, etc).
  • Order Items: Stores order items information.


import datetime

class Items(models.Model):
  name = models.CharField(max_length=200, null=False)
  price = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)
  active = models.BooleanField(default=True)

  # Records
  created = models.DateTimeField(auto_now_add=True)
  time = models.DateTimeField(auto_now=True)

def increment_order_id():
  """ Creates Order ID of format  ABCD  . Change as per neeed."""
  prefix = 'ABCD'  
  # if you are gonna change it but its not 4 character long 
  # make sure to change the numbers below too
  last_order = Order.objects.all().order_by('id').last()
  if not last_order:
    return str(prefix) + str(datetime.date.today().year) + str(
        datetime.date.today().month).zfill(2) + str(
        datetime.date.today().day).zfill(2) + '0000'
  order_id = last_order.order_id
  order_id_int = int(order_id[12:16])
  new_order_id_int = order_id_int + 1
  new_order_id =  str(prefix) + str(str(datetime.date.today().year)) + str(
      datetime.date.today().month).zfill(2) + str(
      datetime.date.today().day).zfill(2) + str(new_order_id_int).zfill(4)
  return new_order_id


class Order(models.Model):
    """ Order Model """

  NOTPAID = 0
  PAID = 1
  PARTPAID = 2

  PAYMENT_STATUS = (
    (NOTPAID, 'Not Paid'),
    (PARTPAID, 'Partial Paid'),
    (PAID, 'Paid'),
  )

  order_id = models.CharField(max_length=20, default=increment_order_id, null=True, blank=True, editable=False)
  payment_status = models.IntegerField(default=NOTPAID, choices=PAYMENT_STATUS, null=False)
  lock = models.BooleanField(default=False, null=False)
  discount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
  created = models.DateTimeField(auto_now_add=True)
  time = models.DateTimeField(auto_now=True)

  class Meta:
    ordering = ('-created', '-id')

  def __str__(self):
    return '%s' % (self.order_id)

  def subtotal(self):
    total = Decimal('0.00').quantize(Decimal('0.01'))
    for item in self.orderitems.all().filter(status=True):
      total = total + Decimal(item.subtotal()).quantize(Decimal('0.01'))
    return str(total)

  def total(self):
    total = Decimal('0.00').quantize(Decimal('0.01'))
    for item in self.orderitems.all().filter(status=True):
      total = total + Decimal(item.total()).quantize(Decimal('0.01'))

    return str(total - self.discount)


class OrderItems(models.Model):
  order = models.ForeignKey(Order, on_delete=models.PROTECT,
                            related_name='orderitems', null=True)
  desc = models.CharField(max_length=500, null=True)

  # Item
  item = models.ForeignKey(Items, on_delete=models.PROTECT, null=True)
  price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
  quantity = models.IntegerField(default=1, null=False)
  tax = models.DecimalField(max_digits=10, decimal_places=4, default=0.00)
  discount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)

  # Records
  created = models.DateTimeField(auto_now_add=True)
  time = models.DateTimeField(auto_now=True)

  def __str__(self):
    return '%s' % (self.id)

  def description(self):
    # If items is linked, return item's description
    if self.item:
      return str(self.item.name)
    else:
      return 'Order Item'

  def subtotal(self):
    total = ((self.price * Decimal(self.quantity)).quantize(
        Decimal('0.01')) - self.discount)
    return str(total)

  def taxtotal(self):
    subtotal = Decimal(self.subtotal()).quantize(Decimal('0.01'))
    total = subtotal * self.tax
    return str(Decimal(total).quantize(Decimal('0.01')))

  def total(self):
    total = Decimal(self.subtotal()).quantize(Decimal('0.01')) + Decimal(
        self.taxtotal()).quantize(Decimal('0.01'))
    return str(total)