django-orchestra/orchestra/apps/bills/models.py

288 lines
9.4 KiB
Python
Raw Normal View History

2014-08-22 11:28:46 +00:00
import inspect
from dateutil.relativedelta import relativedelta
2014-08-22 11:28:46 +00:00
2014-07-08 15:19:15 +00:00
from django.db import models
2014-08-19 18:59:23 +00:00
from django.template import loader, Context
2014-07-23 16:24:56 +00:00
from django.utils import timezone
2014-08-19 18:59:23 +00:00
from django.utils.functional import cached_property
2014-07-23 16:24:56 +00:00
from django.utils.translation import ugettext_lazy as _
2014-08-19 18:59:23 +00:00
from orchestra.apps.accounts.models import Account
from orchestra.core import accounts
2014-09-03 13:56:02 +00:00
from orchestra.utils.functional import cached
2014-09-04 15:55:43 +00:00
from orchestra.utils.html import html_to_pdf
2014-07-23 16:24:56 +00:00
from . import settings
class BillManager(models.Manager):
def get_queryset(self):
queryset = super(BillManager, self).get_queryset()
if self.model != Bill:
2014-08-29 12:45:27 +00:00
bill_type = self.model.get_class_type()
2014-08-19 18:59:23 +00:00
queryset = queryset.filter(type=bill_type)
2014-07-23 16:24:56 +00:00
return queryset
2014-07-08 15:19:15 +00:00
class Bill(models.Model):
2014-07-23 16:24:56 +00:00
OPEN = 'OPEN'
CLOSED = 'CLOSED'
2014-09-04 15:55:43 +00:00
SENT = 'SENT'
2014-07-23 16:24:56 +00:00
PAID = 'PAID'
BAD_DEBT = 'BAD_DEBT'
STATUSES = (
(OPEN, _("Open")),
(CLOSED, _("Closed")),
2014-09-04 15:55:43 +00:00
(SENT, _("Sent")),
2014-07-23 16:24:56 +00:00
(PAID, _("Paid")),
(BAD_DEBT, _("Bad debt")),
)
TYPES = (
('INVOICE', _("Invoice")),
('AMENDMENTINVOICE', _("Amendment invoice")),
('FEE', _("Fee")),
('AMENDMENTFEE', _("Amendment Fee")),
('BUDGET', _("Budget")),
)
2014-08-22 11:28:46 +00:00
number = models.CharField(_("number"), max_length=16, unique=True,
2014-07-23 16:24:56 +00:00
blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s')
2014-08-19 18:59:23 +00:00
type = models.CharField(_("type"), max_length=16, choices=TYPES)
2014-07-23 16:24:56 +00:00
status = models.CharField(_("status"), max_length=16, choices=STATUSES,
default=OPEN)
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
2014-09-03 14:51:07 +00:00
due_on = models.DateField(_("due on"), null=True, blank=True)
2014-07-23 16:24:56 +00:00
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
2014-09-10 16:53:09 +00:00
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
2014-07-23 16:24:56 +00:00
comments = models.TextField(_("comments"), blank=True)
html = models.TextField(_("HTML"), blank=True)
objects = BillManager()
def __unicode__(self):
2014-08-22 11:28:46 +00:00
return self.number
2014-07-23 16:24:56 +00:00
2014-08-19 18:59:23 +00:00
@cached_property
def seller(self):
return Account.get_main().invoicecontact
@cached_property
def buyer(self):
return self.account.invoicecontact
@property
def lines(self):
return self.billlines
2014-07-23 16:24:56 +00:00
@classmethod
2014-08-22 11:28:46 +00:00
def get_class_type(cls):
2014-07-23 16:24:56 +00:00
return cls.__name__.upper()
2014-08-22 11:28:46 +00:00
def get_type(self):
return self.type or self.get_class_type()
def set_number(self):
2014-07-23 16:24:56 +00:00
cls = type(self)
2014-08-22 11:28:46 +00:00
bill_type = self.get_type()
2014-07-23 16:24:56 +00:00
if bill_type == 'BILL':
2014-08-22 11:28:46 +00:00
raise TypeError("get_new_number() can not be used on a Bill class")
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
2014-07-23 16:24:56 +00:00
if self.status == self.OPEN:
prefix = 'O{}'.format(prefix)
bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
last_number = bills.order_by('-number').values_list('number', flat=True).first()
if last_number is None:
last_number = 0
2014-07-23 16:24:56 +00:00
else:
last_number = int(last_number[len(prefix)+4:])
number = last_number + 1
year = timezone.now().strftime("%Y")
number_length = settings.BILLS_NUMBER_LENGTH
zeros = (number_length - len(str(number))) * '0'
number = zeros + str(number)
2014-08-22 11:28:46 +00:00
self.number = '{prefix}{year}{number}'.format(
2014-07-23 16:24:56 +00:00
prefix=prefix, year=year, number=number)
def get_due_date(self, payment=None):
now = timezone.now()
if payment:
return now + payment.get_due_delta()
return now + relativedelta(months=1)
def close(self, payment=False):
assert self.status == self.OPEN, "Bill not in Open state"
if payment is False:
payment = self.account.paymentsources.get_default()
if not self.due_on:
self.due_on = self.get_due_date(payment=payment)
2014-09-08 15:10:16 +00:00
self.total = self.get_total()
self.html = self.render(payment=payment)
2014-09-08 15:10:16 +00:00
self.transactions.create(bill=self, source=payment, amount=self.total)
self.closed_on = timezone.now()
2014-09-04 15:55:43 +00:00
self.status = self.CLOSED
self.save()
def send(self):
from orchestra.apps.contacts.models import Contact
self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={
'bill': self,
},
contacts=(Contact.BILLING,),
attachments=[
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
]
)
self.status = self.SENT
self.save()
def render(self, payment=False):
if payment is False:
payment = self.account.paymentsources.get_default()
2014-08-19 18:59:23 +00:00
context = Context({
'bill': self,
'lines': self.lines.all().prefetch_related('sublines'),
2014-08-19 18:59:23 +00:00
'seller': self.seller,
'buyer': self.buyer,
'seller_info': {
'phone': settings.BILLS_SELLER_PHONE,
'website': settings.BILLS_SELLER_WEBSITE,
'email': settings.BILLS_SELLER_EMAIL,
'bank_account': settings.BILLS_SELLER_BANK_ACCOUNT,
2014-08-19 18:59:23 +00:00
},
'currency': settings.BILLS_CURRENCY,
'payment': payment and payment.get_bill_context(),
'default_due_date': self.get_due_date(payment=payment),
'now': timezone.now(),
2014-08-19 18:59:23 +00:00
})
2014-08-29 12:45:27 +00:00
template = getattr(settings, 'BILLS_%s_TEMPLATE' % self.get_type(),
settings.BILLS_DEFAULT_TEMPLATE)
2014-08-19 18:59:23 +00:00
bill_template = loader.get_template(template)
html = bill_template.render(context)
html = html.replace('-pageskip-', '<pdf:nextpage />')
return html
2014-07-23 16:24:56 +00:00
def save(self, *args, **kwargs):
2014-08-19 18:59:23 +00:00
if not self.type:
2014-08-22 11:28:46 +00:00
self.type = self.get_type()
if not self.number or (self.number.startswith('O') and self.status != self.OPEN):
self.set_number()
2014-07-23 16:24:56 +00:00
super(Bill, self).save(*args, **kwargs)
2014-09-03 13:56:02 +00:00
def get_subtotals(self):
subtotals = {}
2014-09-06 10:56:30 +00:00
for line in self.lines.all():
2014-09-03 13:56:02 +00:00
subtotal, taxes = subtotals.get(line.tax, (0, 0))
2014-09-10 16:53:09 +00:00
subtotal += line.get_total()
2014-09-03 13:56:02 +00:00
subtotals[line.tax] = (subtotal, (line.tax/100)*subtotal)
return subtotals
def get_total(self):
total = 0
for tax, subtotal in self.get_subtotals().iteritems():
subtotal, taxes = subtotal
total += subtotal + taxes
return total
2014-07-08 15:19:15 +00:00
2014-07-23 16:24:56 +00:00
class Invoice(Bill):
class Meta:
proxy = True
class AmendmentInvoice(Bill):
class Meta:
proxy = True
class Fee(Bill):
class Meta:
proxy = True
2014-07-08 15:19:15 +00:00
2014-07-23 16:24:56 +00:00
class AmendmentFee(Bill):
class Meta:
proxy = True
class Budget(Bill):
class Meta:
proxy = True
2014-08-19 18:59:23 +00:00
@property
def lines(self):
return self.budgetlines
2014-07-23 16:24:56 +00:00
class BaseBillLine(models.Model):
""" Base model for bill item representation """
2014-07-23 16:24:56 +00:00
bill = models.ForeignKey(Bill, verbose_name=_("bill"),
related_name='%(class)ss')
description = models.CharField(_("description"), max_length=256)
2014-09-03 13:56:02 +00:00
rate = models.DecimalField(_("rate"), blank=True, null=True,
max_digits=12, decimal_places=2)
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
2014-09-03 13:56:02 +00:00
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
tax = models.PositiveIntegerField(_("tax"))
2014-07-23 16:24:56 +00:00
class Meta:
abstract = True
2014-08-22 11:28:46 +00:00
def __unicode__(self):
return "#%i" % self.number
2014-09-03 13:56:02 +00:00
@cached_property
2014-08-22 11:28:46 +00:00
def number(self):
lines = type(self).objects.filter(bill=self.bill_id)
return lines.filter(id__lte=self.id).order_by('id').count()
2014-09-10 16:53:09 +00:00
2014-07-23 16:24:56 +00:00
class BudgetLine(BaseBillLine):
2014-07-08 15:19:15 +00:00
pass
2014-07-23 16:24:56 +00:00
class BillLine(BaseBillLine):
2014-08-19 18:59:23 +00:00
order_id = models.PositiveIntegerField(blank=True, null=True)
2014-07-23 16:24:56 +00:00
order_last_bill_date = models.DateTimeField(null=True)
order_billed_until = models.DateTimeField(null=True)
auto = models.BooleanField(default=False)
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
2014-09-10 16:53:09 +00:00
def get_total(self):
""" Computes subline discounts """
subtotal = self.total
for subline in self.sublines.all():
subtotal += subline.total
return subtotal
def save(self, *args, **kwargs):
# TODO cost of this shit
super(BillLine, self).save(*args, **kwargs)
if self.bill.status == self.bill.OPEN:
self.bill.total = self.bill.get_total()
self.bill.save()
2014-07-23 16:24:56 +00:00
2014-09-03 13:56:02 +00:00
class BillSubline(models.Model):
""" Subline used for describing an item discount """
bill_line = models.ForeignKey(BillLine, verbose_name=_("bill line"),
related_name='sublines')
description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2)
# TODO type ? Volume and Compensation
2014-09-10 16:53:09 +00:00
def save(self, *args, **kwargs):
# TODO cost of this shit
super(BillSubline, self).save(*args, **kwargs)
if self.line.bill.status == self.line.bill.OPEN:
self.line.bill.total = self.line.bill.get_total()
self.line.bill.save()
accounts.register(Bill)