2015-04-20 14:23:10 +00:00
|
|
|
import datetime
|
2014-09-05 14:27:30 +00:00
|
|
|
from dateutil.relativedelta import relativedelta
|
2014-08-22 11:28:46 +00:00
|
|
|
|
2016-05-18 14:08:12 +00:00
|
|
|
from django.core.urlresolvers import reverse
|
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
|
2015-03-31 12:39:08 +00:00
|
|
|
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 _
|
|
|
|
|
2016-04-06 19:00:16 +00:00
|
|
|
from orchestra.admin.utils import change_url
|
2015-04-05 10:46:24 +00:00
|
|
|
from orchestra.contrib.accounts.models import Account
|
|
|
|
from orchestra.contrib.contacts.models import Contact
|
2015-05-07 14:09:37 +00:00
|
|
|
from orchestra.core import validators
|
2015-10-29 18:19:00 +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-28 17:28:00 +00:00
|
|
|
|
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 = ''
|
2015-06-22 14:14:16 +00:00
|
|
|
CREATED = 'CREATED'
|
|
|
|
PROCESSED = 'PROCESSED'
|
|
|
|
AMENDED = 'AMENDED'
|
2014-07-23 16:24:56 +00:00
|
|
|
PAID = 'PAID'
|
2015-07-02 10:49:44 +00:00
|
|
|
EXECUTED = 'EXECUTED'
|
2014-07-23 16:24:56 +00:00
|
|
|
BAD_DEBT = 'BAD_DEBT'
|
2015-07-02 10:49:44 +00:00
|
|
|
INCOMPLETE = 'INCOMPLETE'
|
2014-09-18 15:07:39 +00:00
|
|
|
PAYMENT_STATES = (
|
2015-06-22 14:14:16 +00:00
|
|
|
(OPEN, _("Open")),
|
|
|
|
(CREATED, _("Created")),
|
|
|
|
(PROCESSED, _("Processed")),
|
|
|
|
(AMENDED, _("Amended")),
|
2014-07-23 16:24:56 +00:00
|
|
|
(PAID, _("Paid")),
|
2015-07-02 10:49:44 +00:00
|
|
|
(INCOMPLETE, _('Incomplete')),
|
|
|
|
(EXECUTED, _("Executed")),
|
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'
|
2020-03-18 06:49:04 +00:00
|
|
|
ABONOINVOICE = 'ABONOINVOICE'
|
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")),
|
2020-03-18 06:49:04 +00:00
|
|
|
(ABONOINVOICE, _("Abono Invoice")),
|
2014-09-24 20:09:41 +00:00
|
|
|
(PROFORMA, _("Pro forma")),
|
2014-07-23 16:24:56 +00:00
|
|
|
)
|
2015-07-07 10:41:34 +00:00
|
|
|
AMEND_MAP = {
|
|
|
|
INVOICE: AMENDMENTINVOICE,
|
|
|
|
FEE: AMENDMENTFEE,
|
|
|
|
}
|
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-06-22 14:14:16 +00:00
|
|
|
related_name='%(class)s')
|
2015-07-02 10:49:44 +00:00
|
|
|
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
|
|
|
related_name='amends')
|
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)
|
2015-07-09 10:19:30 +00:00
|
|
|
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=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-07-14 10:19:45 +00:00
|
|
|
# total = models.DecimalField(max_digits=12, decimal_places=2, null=True)
|
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
|
|
|
|
2015-06-02 12:59:49 +00:00
|
|
|
@classmethod
|
|
|
|
def get_class_type(cls):
|
2016-10-11 10:01:56 +00:00
|
|
|
if cls is models.DEFERRED:
|
2016-04-15 09:56:10 +00:00
|
|
|
cls = cls.__base__
|
2015-06-02 12:59:49 +00:00
|
|
|
return cls.__name__.upper()
|
|
|
|
|
2015-07-14 10:19:45 +00:00
|
|
|
@cached_property
|
|
|
|
def total(self):
|
|
|
|
return self.compute_total()
|
|
|
|
|
2014-08-19 18:59:23 +00:00
|
|
|
@cached_property
|
|
|
|
def seller(self):
|
2015-09-04 10:22:14 +00:00
|
|
|
return Account.objects.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
|
|
|
|
2015-06-02 12:59:49 +00:00
|
|
|
@property
|
|
|
|
def has_multiple_pages(self):
|
|
|
|
return self.type != self.FEE
|
|
|
|
|
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
|
2015-07-02 10:49:44 +00:00
|
|
|
secured = 0
|
|
|
|
pending = 0
|
|
|
|
created = False
|
|
|
|
processed = False
|
|
|
|
executed = False
|
|
|
|
rejected = False
|
|
|
|
for transaction in self.transactions.all():
|
|
|
|
if transaction.state == transaction.SECURED:
|
|
|
|
secured += transaction.amount
|
|
|
|
pending += transaction.amount
|
|
|
|
elif transaction.state == transaction.WAITTING_PROCESSING:
|
|
|
|
pending += transaction.amount
|
|
|
|
created = True
|
|
|
|
elif transaction.state == transaction.WAITTING_EXECUTION:
|
|
|
|
pending += transaction.amount
|
|
|
|
processed = True
|
|
|
|
elif transaction.state == transaction.EXECUTED:
|
|
|
|
pending += transaction.amount
|
|
|
|
executed = True
|
|
|
|
elif transaction.state == transaction.REJECTED:
|
|
|
|
rejected = True
|
|
|
|
else:
|
|
|
|
raise TypeError("Unknown state")
|
|
|
|
ongoing = bool(secured != 0 or created or processed or executed)
|
2015-07-13 11:31:32 +00:00
|
|
|
total = self.compute_total()
|
2015-07-02 10:49:44 +00:00
|
|
|
if total >= 0:
|
|
|
|
if secured >= total:
|
|
|
|
return self.PAID
|
|
|
|
elif ongoing and pending < total:
|
|
|
|
return self.INCOMPLETE
|
|
|
|
else:
|
|
|
|
if secured <= total:
|
|
|
|
return self.PAID
|
|
|
|
elif ongoing and pending > total:
|
|
|
|
return self.INCOMPLETE
|
|
|
|
if created:
|
|
|
|
return self.CREATED
|
|
|
|
elif processed:
|
|
|
|
return self.PROCESSED
|
|
|
|
elif executed:
|
|
|
|
return self.EXECUTED
|
2014-09-18 15:07:39 +00:00
|
|
|
return self.BAD_DEBT
|
|
|
|
|
2015-07-07 10:41:34 +00:00
|
|
|
def clean(self):
|
|
|
|
if self.amend_of_id:
|
|
|
|
errors = {}
|
|
|
|
if self.type not in self.AMEND_MAP.values():
|
|
|
|
errors['amend_of'] = _("Type %s is not an amendment.") % self.get_type_display()
|
|
|
|
if self.amend_of.account_id != self.account_id:
|
|
|
|
errors['account'] = _("Amend of related account doesn't match bill account.")
|
|
|
|
if self.amend_of.is_open:
|
|
|
|
errors['amend_of'] = _("Related invoice is in open state.")
|
|
|
|
if self.amend_of.type in self.AMEND_MAP.values():
|
|
|
|
errors['amend_of'] = _("Related invoice is an amendment.")
|
|
|
|
if errors:
|
|
|
|
raise ValidationError(errors)
|
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def get_payment_state_display(self):
|
|
|
|
value = self.payment_state
|
|
|
|
return force_text(dict(self.PAYMENT_STATES).get(value, value))
|
|
|
|
|
2015-05-27 14:05:25 +00:00
|
|
|
def get_current_transaction(self):
|
|
|
|
return self.transactions.exclude_rejected().first()
|
|
|
|
|
2014-08-22 11:28:46 +00:00
|
|
|
def get_type(self):
|
|
|
|
return self.type or self.get_class_type()
|
|
|
|
|
2016-04-15 09:56:10 +00:00
|
|
|
@property
|
|
|
|
def is_amend(self):
|
|
|
|
return self.type in self.AMEND_MAP.values()
|
|
|
|
|
2015-06-22 14:14:16 +00:00
|
|
|
def get_amend_type(self):
|
2015-07-07 10:41:34 +00:00
|
|
|
amend_type = self.AMEND_MAP.get(self.type)
|
2015-06-22 14:14:16 +00:00
|
|
|
if amend_type is None:
|
|
|
|
raise TypeError("%s has no associated amend type." % self.type)
|
|
|
|
return amend_type
|
|
|
|
|
2015-05-30 14:44:05 +00:00
|
|
|
def get_number(self):
|
2014-07-23 16:24:56 +00:00
|
|
|
cls = type(self)
|
2016-10-11 10:01:56 +00:00
|
|
|
if cls is models.DEFERRED:
|
2016-04-15 09:56:10 +00:00
|
|
|
cls = cls.__base__
|
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')
|
2015-05-26 12:59:16 +00:00
|
|
|
bill_type = bill_type.replace('AMENDMENT', 'AMENDMENT_')
|
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)
|
2016-01-27 09:16:33 +00:00
|
|
|
year = timezone.now().strftime("%Y")
|
2016-01-29 14:07:55 +00:00
|
|
|
bills = cls.objects.filter(number__regex=r'^%s%s[0-9]+' % (prefix, year))
|
2014-09-05 14:27:30 +00:00
|
|
|
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:
|
2014-09-05 14:27:30 +00:00
|
|
|
last_number = int(last_number[len(prefix)+4:])
|
|
|
|
number = last_number + 1
|
|
|
|
number_length = settings.BILLS_NUMBER_LENGTH
|
|
|
|
zeros = (number_length - len(str(number))) * '0'
|
|
|
|
number = zeros + str(number)
|
2015-05-30 14:44:05 +00:00
|
|
|
return '{prefix}{year}{number}'.format(prefix=prefix, year=year, number=number)
|
2014-07-23 16:24:56 +00:00
|
|
|
|
2014-09-05 14:27:30 +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)
|
|
|
|
|
2016-05-18 14:08:12 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse('admin:bills_bill_view', args=(self.pk,))
|
|
|
|
|
2014-09-05 14:27:30 +00:00
|
|
|
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.")
|
2014-09-05 14:27:30 +00:00
|
|
|
if payment is False:
|
|
|
|
payment = self.account.paymentsources.get_default()
|
|
|
|
if not self.due_on:
|
|
|
|
self.due_on = self.get_due_date(payment=payment)
|
2015-07-14 10:19:45 +00:00
|
|
|
total = self.compute_total()
|
2014-10-11 16:21:51 +00:00
|
|
|
transaction = None
|
2014-09-24 20:09:41 +00:00
|
|
|
if self.get_type() != self.PROFORMA:
|
2015-07-14 10:19:45 +00:00
|
|
|
transaction = self.transactions.create(bill=self, source=payment, amount=total)
|
2014-09-05 14:27:30 +00:00
|
|
|
self.closed_on = timezone.now()
|
2014-09-18 15:07:39 +00:00
|
|
|
self.is_open = False
|
|
|
|
self.is_sent = False
|
2015-05-30 14:44:05 +00:00
|
|
|
self.number = self.get_number()
|
|
|
|
self.html = self.render(payment=payment)
|
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
|
|
|
|
2016-05-20 08:29:25 +00:00
|
|
|
def get_billing_contact_emails(self):
|
|
|
|
return self.account.get_contacts_emails(usages=(Contact.BILLING,))
|
|
|
|
|
2014-09-04 15:55:43 +00:00
|
|
|
def send(self):
|
2015-06-03 12:49:30 +00:00
|
|
|
pdf = self.as_pdf()
|
2014-09-04 15:55:43 +00:00
|
|
|
self.account.send_email(
|
|
|
|
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
|
|
|
|
context={
|
|
|
|
'bill': self,
|
2015-05-27 14:05:25 +00:00
|
|
|
'settings': settings,
|
2014-09-04 15:55:43 +00:00
|
|
|
},
|
2015-05-27 14:05:25 +00:00
|
|
|
email_from=settings.BILLS_SELLER_EMAIL,
|
2016-05-20 08:29:25 +00:00
|
|
|
usages=(Contact.BILLING,),
|
2014-09-04 15:55:43 +00:00
|
|
|
attachments=[
|
2015-05-28 09:43:57 +00:00
|
|
|
('%s.pdf' % self.number, pdf, 'application/pdf')
|
2014-09-04 15:55:43 +00:00
|
|
|
]
|
|
|
|
)
|
2014-09-18 15:07:39 +00:00
|
|
|
self.is_sent = True
|
2014-09-30 16:06:42 +00:00
|
|
|
self.save(update_fields=['is_sent'])
|
2014-07-28 17:28:00 +00:00
|
|
|
|
2015-03-31 12:39:08 +00:00
|
|
|
def render(self, payment=False, language=None):
|
|
|
|
with translation.override(language or self.account.language):
|
2015-05-30 14:44:05 +00:00
|
|
|
if payment is False:
|
|
|
|
payment = self.account.paymentsources.get_default()
|
|
|
|
context = Context({
|
|
|
|
'bill': self,
|
|
|
|
'lines': self.lines.all().prefetch_related('sublines'),
|
|
|
|
'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,
|
|
|
|
},
|
|
|
|
'currency': settings.BILLS_CURRENCY,
|
|
|
|
'payment': payment and payment.get_bill_context(),
|
|
|
|
'default_due_date': self.get_due_date(payment=payment),
|
|
|
|
'now': timezone.now(),
|
|
|
|
})
|
|
|
|
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
|
|
|
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
|
|
|
bill_template = loader.get_template(template)
|
2015-03-31 12:39:08 +00:00
|
|
|
html = bill_template.render(context)
|
2015-05-30 14:44:05 +00:00
|
|
|
html = html.replace('-pageskip-', '<pdf:nextpage />')
|
2014-08-19 18:59:23 +00:00
|
|
|
return html
|
|
|
|
|
2015-06-03 12:49:30 +00:00
|
|
|
def as_pdf(self):
|
|
|
|
html = self.html or self.render()
|
|
|
|
return html_to_pdf(html, pagination=self.has_multiple_pages)
|
|
|
|
|
2015-10-30 12:09:01 +00:00
|
|
|
def updated(self):
|
|
|
|
self.updated_on = timezone.now()
|
|
|
|
self.save(update_fields=('updated_on',))
|
|
|
|
|
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()
|
2015-05-30 14:44:05 +00:00
|
|
|
if not self.number:
|
|
|
|
self.number = self.get_number()
|
2014-07-23 16:24:56 +00:00
|
|
|
super(Bill, self).save(*args, **kwargs)
|
2014-09-03 13:56:02 +00:00
|
|
|
|
2015-10-29 18:19:00 +00:00
|
|
|
@cached
|
2015-06-02 12:59:49 +00:00
|
|
|
def compute_subtotals(self):
|
2014-09-03 13:56:02 +00:00
|
|
|
subtotals = {}
|
2015-07-13 11:31:32 +00:00
|
|
|
lines = self.lines.annotate(totals=F('subtotal') + Sum(Coalesce('sublines__total', 0)))
|
2015-04-14 14:29:22 +00:00
|
|
|
for tax, total in lines.values_list('tax', 'totals'):
|
2015-07-13 11:31:32 +00:00
|
|
|
try:
|
|
|
|
subtotals[tax] += total
|
|
|
|
except KeyError:
|
|
|
|
subtotals[tax] = total
|
|
|
|
result = {}
|
|
|
|
for tax, subtotal in subtotals.items():
|
2015-10-08 13:54:39 +00:00
|
|
|
result[tax] = [subtotal, round(tax/100*subtotal, 2)]
|
2015-07-13 11:31:32 +00:00
|
|
|
return result
|
2014-09-03 13:56:02 +00:00
|
|
|
|
2015-10-29 18:19:00 +00:00
|
|
|
@cached
|
2015-07-07 10:41:34 +00:00
|
|
|
def compute_base(self):
|
|
|
|
bases = self.lines.annotate(
|
2015-07-13 11:31:32 +00:00
|
|
|
bases=F('subtotal') + Sum(Coalesce('sublines__total', 0))
|
2015-07-07 10:41:34 +00:00
|
|
|
)
|
|
|
|
return round(bases.aggregate(Sum('bases'))['bases__sum'] or 0, 2)
|
|
|
|
|
2015-10-29 18:19:00 +00:00
|
|
|
@cached
|
2015-07-10 13:00:51 +00:00
|
|
|
def compute_tax(self):
|
|
|
|
taxes = self.lines.annotate(
|
2015-07-13 11:31:32 +00:00
|
|
|
taxes=(F('subtotal') + Coalesce(Sum('sublines__total'), 0)) * (F('tax')/100)
|
2015-07-10 13:00:51 +00:00
|
|
|
)
|
|
|
|
return round(taxes.aggregate(Sum('taxes'))['taxes__sum'] or 0, 2)
|
|
|
|
|
2015-10-29 18:19:00 +00:00
|
|
|
@cached
|
2015-06-02 12:59:49 +00:00
|
|
|
def compute_total(self):
|
2015-07-13 11:31:32 +00:00
|
|
|
if 'lines' in getattr(self, '_prefetched_objects_cache', ()):
|
|
|
|
total = 0
|
|
|
|
for line in self.lines.all():
|
|
|
|
line_total = line.compute_total()
|
|
|
|
total += line_total * (1+line.tax/100)
|
|
|
|
return round(total, 2)
|
|
|
|
else:
|
|
|
|
totals = self.lines.annotate(
|
|
|
|
totals=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100)
|
|
|
|
)
|
|
|
|
return round(totals.aggregate(Sum('totals'))['totals__sum'] or 0, 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
|
|
|
|
|
|
|
|
|
2020-03-18 06:49:04 +00:00
|
|
|
class AbonoInvoice(Bill):
|
|
|
|
class Meta:
|
|
|
|
proxy = True
|
|
|
|
|
|
|
|
|
2014-07-23 16:24:56 +00:00
|
|
|
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):
|
2014-09-02 15:48:07 +00:00
|
|
|
""" Base model for bill item representation """
|
2014-09-11 14:00:20 +00:00
|
|
|
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
|
2014-09-02 15:48:07 +00:00
|
|
|
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)
|
2015-06-22 14:14:16 +00:00
|
|
|
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
|
|
|
decimal_places=2)
|
2016-04-06 19:00:16 +00:00
|
|
|
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16, blank=True)
|
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"))
|
2015-07-07 10:41:34 +00:00
|
|
|
end_on = models.DateField(_("end"), null=True, blank=True)
|
2014-09-26 15:05:20 +00:00
|
|
|
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
|
2016-02-23 11:49:10 +00:00
|
|
|
related_name='lines', on_delete=models.SET_NULL,
|
|
|
|
help_text=_("Informative link back to the order"))
|
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"),
|
2015-05-27 14:05:25 +00:00
|
|
|
related_name='amendment_lines', null=True, blank=True)
|
2014-08-22 11:28:46 +00:00
|
|
|
|
2016-01-29 14:07:55 +00:00
|
|
|
class Meta:
|
|
|
|
get_latest_by = 'id'
|
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2015-10-29 18:19:00 +00:00
|
|
|
return "#%i" % self.pk if self.pk else self.description
|
2014-09-10 16:53:09 +00:00
|
|
|
|
2015-04-14 14:29:22 +00:00
|
|
|
def get_verbose_quantity(self):
|
|
|
|
return self.verbose_quantity or self.quantity
|
2014-09-10 16:53:09 +00:00
|
|
|
|
2016-04-07 11:14:44 +00:00
|
|
|
def clean(self):
|
2016-04-06 19:00:16 +00:00
|
|
|
if not self.verbose_quantity:
|
|
|
|
quantity = str(self.quantity)
|
|
|
|
# Strip trailing zeros
|
|
|
|
if quantity.endswith('0'):
|
|
|
|
self.verbose_quantity = quantity.strip('0').strip('.')
|
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
def get_verbose_period(self):
|
2015-05-27 14:05:25 +00:00
|
|
|
from django.template.defaultfilters import date
|
|
|
|
date_format = "N 'y"
|
2016-04-27 08:35:13 +00:00
|
|
|
if self.start_on.day != 1 or (self.end_on and self.end_on.day != 1):
|
2015-05-27 14:05:25 +00:00
|
|
|
date_format = "N j, 'y"
|
|
|
|
end = date(self.end_on, date_format)
|
2016-04-27 08:35:13 +00:00
|
|
|
elif self.end_on:
|
2015-05-27 14:05:25 +00:00
|
|
|
end = date((self.end_on - datetime.timedelta(days=1)), date_format)
|
2015-05-28 09:43:57 +00:00
|
|
|
ini = date(self.start_on, date_format).capitalize()
|
2015-04-20 14:23:10 +00:00
|
|
|
if not self.end_on:
|
|
|
|
return ini
|
2016-04-27 08:35:13 +00:00
|
|
|
end = end.capitalize()
|
2015-04-20 14:23:10 +00:00
|
|
|
if ini == end:
|
|
|
|
return ini
|
2015-05-27 14:05:25 +00:00
|
|
|
return "{ini} / {end}".format(ini=ini, end=end)
|
2014-09-26 15:05:20 +00:00
|
|
|
|
2015-10-29 18:19:00 +00:00
|
|
|
@cached
|
2015-07-13 11:31:32 +00:00
|
|
|
def compute_total(self):
|
|
|
|
total = self.subtotal or 0
|
|
|
|
if hasattr(self, 'subline_total'):
|
|
|
|
total += self.subline_total or 0
|
|
|
|
elif 'sublines' in getattr(self, '_prefetched_objects_cache', ()):
|
|
|
|
total += sum(subline.total for subline in self.sublines.all())
|
|
|
|
else:
|
|
|
|
total += self.sublines.aggregate(sub_total=Sum('total'))['sub_total'] or 0
|
|
|
|
return round(total, 2)
|
2016-04-06 19:00:16 +00:00
|
|
|
|
|
|
|
def get_absolute_url(self):
|
|
|
|
return change_url(self)
|
2014-07-23 16:24:56 +00:00
|
|
|
|
2014-07-28 17:28:00 +00:00
|
|
|
|
2014-09-03 13:56:02 +00:00
|
|
|
class BillSubline(models.Model):
|
2014-09-02 15:48:07 +00:00
|
|
|
""" 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')
|
2014-09-02 15:48:07 +00:00
|
|
|
description = models.CharField(_("description"), max_length=256)
|
|
|
|
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)
|
2015-09-17 11:21:35 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return "%s %i" % (self.description, self.total)
|