django-orchestra-test/orchestra/contrib/bills/models.py

362 lines
13 KiB
Python
Raw Normal View History

2015-04-20 14:23:10 +00:00
import datetime
from dateutil.relativedelta import relativedelta
2014-08-22 11:28:46 +00:00
2014-10-30 16:34:02 +00:00
from django.core.validators import ValidationError, RegexValidator
2014-07-08 15:19:15 +00:00
from django.db import models
2015-04-14 14:29:22 +00:00
from django.db.models import F, Sum
from django.db.models.functions import Coalesce
2014-08-19 18:59:23 +00:00
from django.template import loader, Context
from django.utils import timezone, translation
2014-09-18 15:07:39 +00:00
from django.utils.encoding import force_text
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 _
2015-04-05 10:46:24 +00:00
from orchestra.contrib.accounts.models import Account
from orchestra.contrib.contacts.models import Contact
2014-10-30 16:34:02 +00:00
from orchestra.core import accounts, validators
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
2014-10-17 10:04:47 +00:00
class BillContact(models.Model):
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
2015-04-05 10:46:24 +00:00
related_name='billcontact')
2014-10-27 14:31:04 +00:00
name = models.CharField(_("name"), max_length=256, blank=True,
2015-04-05 10:46:24 +00:00
help_text=_("Account full name will be used when left blank."))
2014-10-17 10:04:47 +00:00
address = models.TextField(_("address"))
city = models.CharField(_("city"), max_length=128,
2015-04-05 10:46:24 +00:00
default=settings.BILLS_CONTACT_DEFAULT_CITY)
2014-10-30 16:34:02 +00:00
zipcode = models.CharField(_("zip code"), max_length=10,
2015-04-05 10:46:24 +00:00
validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', _("Enter a valid zipcode."))])
2014-10-17 10:04:47 +00:00
country = models.CharField(_("country"), max_length=20,
2015-04-05 10:46:24 +00:00
choices=settings.BILLS_CONTACT_COUNTRIES,
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
2014-10-17 10:04:47 +00:00
vat = models.CharField(_("VAT number"), max_length=64)
2015-04-02 16:14:55 +00:00
def __str__(self):
2014-10-17 10:04:47 +00:00
return self.name
2014-10-27 14:31:04 +00:00
def get_name(self):
return self.name or self.account.get_full_name()
2014-10-30 16:34:02 +00:00
def clean(self):
self.vat = self.vat.strip()
self.city = self.city.strip()
2014-11-05 20:22:01 +00:00
validators.all_valid({
'vat': (validators.validate_vat, self.vat, self.country),
'zipcode': (validators.validate_zipcode, self.zipcode, self.country)
})
2014-10-17 10:04:47 +00:00
2014-07-23 16:24:56 +00:00
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-09-18 15:07:39 +00:00
OPEN = ''
2014-07-23 16:24:56 +00:00
PAID = 'PAID'
2014-09-18 15:07:39 +00:00
PENDING = 'PENDING'
2014-07-23 16:24:56 +00:00
BAD_DEBT = 'BAD_DEBT'
2014-09-18 15:07:39 +00:00
PAYMENT_STATES = (
2014-07-23 16:24:56 +00:00
(PAID, _("Paid")),
2014-09-18 15:07:39 +00:00
(PENDING, _("Pending")),
2014-07-23 16:24:56 +00:00
(BAD_DEBT, _("Bad debt")),
)
2014-09-24 20:09:41 +00:00
BILL = 'BILL'
INVOICE = 'INVOICE'
AMENDMENTINVOICE = 'AMENDMENTINVOICE'
FEE = 'FEE'
AMENDMENTFEE = 'AMENDMENTFEE'
PROFORMA = 'PROFORMA'
2014-07-23 16:24:56 +00:00
TYPES = (
2014-09-24 20:09:41 +00:00
(INVOICE, _("Invoice")),
(AMENDMENTINVOICE, _("Amendment invoice")),
(FEE, _("Fee")),
(AMENDMENTFEE, _("Amendment Fee")),
(PROFORMA, _("Pro forma")),
2014-07-23 16:24:56 +00:00
)
2014-09-24 20:09:41 +00:00
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
2014-07-23 16:24:56 +00:00
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
2015-04-05 10:46:24 +00:00
related_name='%(class)s')
2014-08-19 18:59:23 +00:00
type = models.CharField(_("type"), max_length=16, choices=TYPES)
2014-09-26 15:05:20 +00:00
created_on = models.DateField(_("created on"), auto_now_add=True)
closed_on = models.DateField(_("closed on"), blank=True, null=True)
2014-09-30 10:20:11 +00:00
is_open = models.BooleanField(_("open"), default=True)
is_sent = models.BooleanField(_("sent"), default=False)
2014-09-03 14:51:07 +00:00
due_on = models.DateField(_("due on"), null=True, blank=True)
2014-09-26 15:05:20 +00:00
updated_on = models.DateField(_("updated on"), auto_now=True)
2015-04-14 14:29:22 +00:00
# TODO allways compute total or what?
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()
2014-09-22 15:59:53 +00:00
class Meta:
2014-09-24 20:09:41 +00:00
get_latest_by = 'id'
2014-09-22 15:59:53 +00:00
2015-04-02 16:14:55 +00:00
def __str__(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):
2014-10-17 10:04:47 +00:00
return Account.get_main().billcontact
2014-08-19 18:59:23 +00:00
@cached_property
def buyer(self):
2014-10-17 10:04:47 +00:00
return self.account.billcontact
2014-08-19 18:59:23 +00:00
2014-09-18 15:07:39 +00:00
@cached_property
def payment_state(self):
2014-10-11 16:21:51 +00:00
if self.is_open or self.get_type() == self.PROFORMA:
2014-09-18 15:07:39 +00:00
return self.OPEN
secured = self.transactions.secured().amount()
if secured >= self.total:
return self.PAID
elif self.transactions.exclude_rejected().exists():
return self.PENDING
return self.BAD_DEBT
def get_payment_state_display(self):
value = self.payment_state
return force_text(dict(self.PAYMENT_STATES).get(value, value))
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-09-24 20:09:41 +00:00
if bill_type == self.BILL:
raise TypeError('This method can not be used on BILL instances')
2014-08-22 11:28:46 +00:00
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
2014-09-18 15:07:39 +00:00
if self.is_open:
2014-07-23 16:24:56 +00:00
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-09-24 20:09:41 +00:00
self.number = '{prefix}{year}{number}'.format(prefix=prefix, year=year, number=number)
2014-07-23 16:24:56 +00:00
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):
2014-11-27 19:17:26 +00:00
if not self.is_open:
raise TypeError("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-10-11 16:21:51 +00:00
transaction = None
2014-09-24 20:09:41 +00:00
if self.get_type() != self.PROFORMA:
2014-10-11 16:21:51 +00:00
transaction = self.transactions.create(bill=self, source=payment, amount=self.total)
self.closed_on = timezone.now()
2014-09-18 15:07:39 +00:00
self.is_open = False
self.is_sent = False
2014-09-04 15:55:43 +00:00
self.save()
2014-10-11 16:21:51 +00:00
return transaction
2014-09-04 15:55:43 +00:00
def send(self):
2014-09-30 14:46:29 +00:00
html = self.html or self.render()
2014-09-04 15:55:43 +00:00
self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={
'bill': self,
},
contacts=(Contact.BILLING,),
attachments=[
2014-09-30 14:46:29 +00:00
('%s.pdf' % self.number, html_to_pdf(html), 'application/pdf')
2014-09-04 15:55:43 +00:00
]
)
2014-09-18 15:07:39 +00:00
self.is_sent = True
self.save(update_fields=['is_sent'])
def render(self, payment=False, language=None):
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-09-24 20:09:41 +00:00
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
2014-08-19 18:59:23 +00:00
bill_template = loader.get_template(template)
with translation.override(language or self.account.language):
html = bill_template.render(context)
2014-08-19 18:59:23 +00:00
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()
2014-09-18 15:07:39 +00:00
if not self.number or (self.number.startswith('O') and not self.is_open):
2014-08-22 11:28:46 +00:00
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 = {}
2015-04-14 14:29:22 +00:00
lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)))
for tax, total in lines.values_list('tax', 'totals'):
subtotal, taxes = subtotals.get(tax, (0, 0))
subtotal += total
subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
2014-09-03 13:56:02 +00:00
return subtotals
def get_total(self):
2015-04-14 14:29:22 +00:00
totals = self.lines.annotate(
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
return round(totals.aggregate(Sum('totals'))['totals__sum'], 2)
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
2014-09-11 14:00:20 +00:00
class ProForma(Bill):
2014-07-23 16:24:56 +00:00
class Meta:
proxy = True
2014-09-11 14:00:20 +00:00
class BillLine(models.Model):
""" Base model for bill item representation """
2014-09-11 14:00:20 +00:00
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
description = models.CharField(_("description"), max_length=256)
2014-09-24 20:09:41 +00:00
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
2014-09-18 15:07:39 +00:00
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
2015-04-14 14:29:22 +00:00
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
2014-09-18 15:07:39 +00:00
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
2015-04-14 14:29:22 +00:00
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
2015-04-20 14:23:10 +00:00
start_on = models.DateField(_("start"))
end_on = models.DateField(_("end"), null=True)
2014-09-26 15:05:20 +00:00
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
2015-04-05 10:46:24 +00:00
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
2014-09-26 15:05:20 +00:00
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
created_on = models.DateField(_("created"), auto_now_add=True)
# Amendment
2014-09-11 14:00:20 +00:00
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
2014-08-22 11:28:46 +00:00
2015-04-02 16:14:55 +00:00
def __str__(self):
2014-08-22 11:28:46 +00:00
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
def get_total(self):
""" Computes subline discounts """
2015-04-14 14:29:22 +00:00
if self.pk:
return self.subtotal + sum(self.sublines.values_list('total', flat=True))
def get_verbose_quantity(self):
return self.verbose_quantity or self.quantity
2014-09-10 16:53:09 +00:00
2015-04-20 14:23:10 +00:00
def get_verbose_period(self):
ini = self.start_on.strftime("%b, %Y")
if not self.end_on:
return ini
end = (self.end_on - datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end:
return ini
return _("{ini} to {end}").format(ini=ini, end=end)
2014-09-26 15:05:20 +00:00
def undo(self):
# TODO warn user that undoing bills with compensations lead to compensation lost
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
if not getattr(self, attr):
raise ValidationError(_("Not enough information stored for undoing"))
if self.created_on != self.order.billed_on:
raise ValidationError(_("Dates don't match"))
self.order.billed_until = self.order_billed_until
self.order.billed_on = self.order_billed_on
self.delete()
2015-04-14 14:29:22 +00:00
# def save(self, *args, **kwargs):
# super(BillLine, self).save(*args, **kwargs)
# if self.bill.is_open:
# self.bill.total = self.bill.get_total()
# self.bill.save(update_fields=['total'])
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 """
2014-09-26 15:05:20 +00:00
VOLUME = 'VOLUME'
COMPENSATION = 'COMPENSATION'
OTHER = 'OTHER'
TYPES = (
(VOLUME, _("Volume")),
(COMPENSATION, _("Compensation")),
(OTHER, _("Other")),
)
# TODO: order info for undoing
2014-09-24 20:09:41 +00:00
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
description = models.CharField(_("description"), max_length=256)
# TODO rename to subtotal
total = models.DecimalField(max_digits=12, decimal_places=2)
2014-09-26 15:05:20 +00:00
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
2014-09-10 16:53:09 +00:00
2015-04-14 14:29:22 +00:00
# def save(self, *args, **kwargs):
# # TODO cost of this shit
# super(BillSubline, self).save(*args, **kwargs)
# if self.line.bill.is_open:
# self.line.bill.total = self.line.bill.get_total()
# self.line.bill.save(update_fields=['total'])
2014-09-11 14:00:20 +00:00
accounts.register(Bill)