diff --git a/TODO.md b/TODO.md
index 0fd3659e..91cae3f7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -186,7 +186,6 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl
* use server.name | server.address on python backends, like gitlab instead of settings?
* TODO raise404, here and everywhere
-# display subline links on billlines, to show that they exists.
* update service orders on a celery task? because it take alot
# billline quantity eval('10x100') instead of miningless description '(10*100)' line.verbose_quantity
@@ -246,7 +245,6 @@ celery max-tasks-per-child
* autoscale celery workers http://docs.celeryproject.org/en/latest/userguide/workers.html#autoscaling
-* webapp has_website list filter
glic3rinu's django-fluent-dashboard
* gevent is not ported to python3 :'(
@@ -294,3 +292,6 @@ https://code.djangoproject.com/ticket/24576
* fpm reload starts new pools?
* rename resource.monitors to resource.backends ?
* abstract model classes enabling overriding?
+
+# Ignore superusers & co on billing
+# bill.totals make it 100% computed?
diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py
index b3cc03e5..cf4ad24e 100644
--- a/orchestra/admin/utils.py
+++ b/orchestra/admin/utils.py
@@ -36,7 +36,7 @@ def get_modeladmin(model, import_module=True):
def insertattr(model, name, value):
""" Inserts attribute to a modeladmin """
modeladmin = None
- if models.Model in model.__mro__:
+ if isinstance(model, models.Model)
modeladmin = get_modeladmin(model)
modeladmin_class = type(modeladmin)
elif not inspect.isclass(model):
diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py
index 74448d89..c8b0d887 100644
--- a/orchestra/contrib/bills/admin.py
+++ b/orchestra/contrib/bills/admin.py
@@ -4,6 +4,8 @@ from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.core.urlresolvers import reverse
from django.db import models
+from django.db.models import F, Sum
+from django.db.models.functions import Coalesce
from django.templatetags.static import static
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@@ -58,7 +60,9 @@ class ClosedBillLineInline(BillLineInline):
# TODO reimplement as nested inlines when upstream
# https://code.djangoproject.com/ticket/9025
- fields = ('display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total')
+ fields = (
+ 'display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total'
+ )
readonly_fields = fields
def display_description(self, line):
@@ -77,6 +81,11 @@ class ClosedBillLineInline(BillLineInline):
display_subtotal.short_description = _("Subtotal")
display_subtotal.allow_tags = True
+ def display_total(self, line):
+ return line.get_total()
+ display_total.short_description = _("Total")
+ display_total.allow_tags = True
+
def has_add_permission(self, request):
return False
@@ -134,6 +143,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
change_view_actions = [
actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills
]
+ search_fields = ('number', 'account__username', 'comments')
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
change_readonly_fields = ('account_link', 'type', 'is_open')
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
@@ -147,10 +157,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines")
def display_total(self, bill):
- return "%s &%s;" % (bill.total, settings.BILLS_CURRENCY.lower())
+ return "%s &%s;" % (round(bill.totals, 2), settings.BILLS_CURRENCY.lower())
display_total.allow_tags = True
display_total.short_description = _("total")
- display_total.admin_order_field = 'total'
+ display_total.admin_order_field = 'totals'
def type_link(self, bill):
bill_type = bill.type.lower()
@@ -210,8 +220,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_inline_instances(self, request, obj=None):
inlines = super(BillAdmin, self).get_inline_instances(request, obj)
if obj and not obj.is_open:
- return [inline for inline in inlines if not isinstance(inline, BillLineInline)]
- return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)]
+ return [inline for inline in inlines if type(inline) is not BillLineInline]
+ return [inline for inline in inlines if type(inline) is not ClosedBillLineInline]
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
@@ -223,8 +233,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_queryset(self, request):
qs = super(BillAdmin, self).get_queryset(request)
- qs = qs.annotate(models.Count('lines'))
- qs = qs.prefetch_related('lines', 'lines__sublines', 'transactions')
+ qs = qs.annotate(
+ models.Count('lines'),
+ totals=Sum(
+ (F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
+ ),
+ )
+ qs = qs.prefetch_related('transactions')
return qs
def change_view(self, request, object_id, **kwargs):
diff --git a/orchestra/contrib/bills/migrations/0001_initial.py b/orchestra/contrib/bills/migrations/0001_initial.py
new file mode 100644
index 00000000..690c799a
--- /dev/null
+++ b/orchestra/contrib/bills/migrations/0001_initial.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+import django.db.models.deletion
+import django.core.validators
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('orders', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Bill',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
+ ('number', models.CharField(verbose_name='number', blank=True, unique=True, max_length=16)),
+ ('type', models.CharField(verbose_name='type', choices=[('INVOICE', 'Invoice'), ('AMENDMENTINVOICE', 'Amendment invoice'), ('FEE', 'Fee'), ('AMENDMENTFEE', 'Amendment Fee'), ('PROFORMA', 'Pro forma')], max_length=16)),
+ ('created_on', models.DateField(verbose_name='created on', auto_now_add=True)),
+ ('closed_on', models.DateField(verbose_name='closed on', null=True, blank=True)),
+ ('is_open', models.BooleanField(verbose_name='open', default=True)),
+ ('is_sent', models.BooleanField(verbose_name='sent', default=False)),
+ ('due_on', models.DateField(verbose_name='due on', null=True, blank=True)),
+ ('updated_on', models.DateField(verbose_name='updated on', auto_now=True)),
+ ('total', models.DecimalField(default=0, decimal_places=2, max_digits=12)),
+ ('comments', models.TextField(verbose_name='comments', blank=True)),
+ ('html', models.TextField(verbose_name='HTML', blank=True)),
+ ('account', models.ForeignKey(verbose_name='account', related_name='bill', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'get_latest_by': 'id',
+ },
+ ),
+ migrations.CreateModel(
+ name='BillContact',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
+ ('name', models.CharField(verbose_name='name', blank=True, help_text='Account full name will be used when left blank.', max_length=256)),
+ ('address', models.TextField(verbose_name='address')),
+ ('city', models.CharField(verbose_name='city', default='Barcelona', max_length=128)),
+ ('zipcode', models.CharField(verbose_name='zip code', validators=[django.core.validators.RegexValidator('^[0-9A-Z]{3,10}$', 'Enter a valid zipcode.')], max_length=10)),
+ ('country', models.CharField(verbose_name='country', default='ES', choices=[('TR', 'Turkey'), ('BV', 'Bouvet Island'), ('EE', 'Estonia'), ('CO', 'Colombia'), ('MW', 'Malawi'), ('JM', 'Jamaica'), ('GF', 'French Guiana'), ('NR', 'Nauru'), ('DK', 'Denmark'), ('SY', 'Syrian Arab Republic'), ('PH', 'Philippines'), ('TF', 'French Southern Territories'), ('GH', 'Ghana'), ('AM', 'Armenia'), ('PY', 'Paraguay'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('EG', 'Egypt'), ('CU', 'Cuba'), ('VI', 'Virgin Islands (U.S.)'), ('KN', 'Saint Kitts and Nevis'), ('RU', 'Russian Federation'), ('RO', 'Romania'), ('MD', 'Moldova (the Republic of)'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('JP', 'Japan'), ('OM', 'Oman'), ('AE', 'United Arab Emirates'), ('BM', 'Bermuda'), ('VG', 'Virgin Islands (British)'), ('CD', 'Congo (the Democratic Republic of the)'), ('GY', 'Guyana'), ('IQ', 'Iraq'), ('DJ', 'Djibouti'), ('MU', 'Mauritius'), ('UG', 'Uganda'), ('ID', 'Indonesia'), ('KP', "Korea (the Democratic People's Republic of)"), ('CA', 'Canada'), ('MS', 'Montserrat'), ('SA', 'Saudi Arabia'), ('SZ', 'Swaziland'), ('NZ', 'New Zealand'), ('TO', 'Tonga'), ('IM', 'Isle of Man'), ('AZ', 'Azerbaijan'), ('PG', 'Papua New Guinea'), ('LB', 'Lebanon'), ('PR', 'Puerto Rico'), ('HM', 'Heard Island and McDonald Islands'), ('GR', 'Greece'), ('CR', 'Costa Rica'), ('PA', 'Panama'), ('BG', 'Bulgaria'), ('SS', 'South Sudan'), ('PE', 'Peru'), ('BY', 'Belarus'), ('FK', 'Falkland Islands [Malvinas]'), ('PF', 'French Polynesia'), ('MP', 'Northern Mariana Islands'), ('HN', 'Honduras'), ('SI', 'Slovenia'), ('GU', 'Guam'), ('PL', 'Poland'), ('CW', 'Curaçao'), ('BF', 'Burkina Faso'), ('PT', 'Portugal'), ('ZM', 'Zambia'), ('TZ', 'Tanzania, United Republic of'), ('WF', 'Wallis and Futuna'), ('DM', 'Dominica'), ('GT', 'Guatemala'), ('PS', 'Palestine, State of'), ('TN', 'Tunisia'), ('BE', 'Belgium'), ('SX', 'Sint Maarten (Dutch part)'), ('FJ', 'Fiji'), ('FO', 'Faroe Islands'), ('BH', 'Bahrain'), ('BL', 'Saint Barthélemy'), ('DE', 'Germany'), ('NU', 'Niue'), ('SV', 'El Salvador'), ('BS', 'Bahamas'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('SL', 'Sierra Leone'), ('SN', 'Senegal'), ('EH', 'Western Sahara'), ('TD', 'Chad'), ('NA', 'Namibia'), ('FI', 'Finland'), ('GW', 'Guinea-Bissau'), ('MT', 'Malta'), ('KY', 'Cayman Islands'), ('UM', 'United States Minor Outlying Islands'), ('LC', 'Saint Lucia'), ('GD', 'Grenada'), ('GM', 'Gambia'), ('HU', 'Hungary'), ('DZ', 'Algeria'), ('JO', 'Jordan'), ('ZW', 'Zimbabwe'), ('CY', 'Cyprus'), ('GL', 'Greenland'), ('UY', 'Uruguay'), ('MA', 'Morocco'), ('GP', 'Guadeloupe'), ('MY', 'Malaysia'), ('FR', 'France'), ('RE', 'Réunion'), ('MV', 'Maldives'), ('MN', 'Mongolia'), ('MO', 'Macao'), ('AU', 'Australia'), ('CX', 'Christmas Island'), ('VN', 'Viet Nam'), ('AS', 'American Samoa'), ('TK', 'Tokelau'), ('GS', 'South Georgia and the South Sandwich Islands'), ('KG', 'Kyrgyzstan'), ('AO', 'Angola'), ('TV', 'Tuvalu'), ('NI', 'Nicaragua'), ('QA', 'Qatar'), ('LT', 'Lithuania'), ('VA', 'Holy See'), ('PK', 'Pakistan'), ('GQ', 'Equatorial Guinea'), ('RS', 'Serbia'), ('KR', 'Korea (the Republic of)'), ('ER', 'Eritrea'), ('KW', 'Kuwait'), ('IR', 'Iran (Islamic Republic of)'), ('SK', 'Slovakia'), ('SE', 'Sweden'), ('TL', 'Timor-Leste'), ('AG', 'Antigua and Barbuda'), ('SD', 'Sudan'), ('BR', 'Brazil'), ('TM', 'Turkmenistan'), ('AI', 'Anguilla'), ('SR', 'Suriname'), ('MX', 'Mexico'), ('GE', 'Georgia'), ('KE', 'Kenya'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('VC', 'Saint Vincent and the Grenadines'), ('MF', 'Saint Martin (French part)'), ('CC', 'Cocos (Keeling) Islands'), ('GI', 'Gibraltar'), ('ME', 'Montenegro'), ('MC', 'Monaco'), ('ZA', 'South Africa'), ('IS', 'Iceland'), ('KM', 'Comoros'), ('KI', 'Kiribati'), ('HT', 'Haiti'), ('BO', 'Bolivia (Plurinational State of)'), ('CH', 'Switzerland'), ('MR', 'Mauritania'), ('GA', 'Gabon'), ('KZ', 'Kazakhstan'), ('BN', 'Brunei Darussalam'), ('YT', 'Mayotte'), ('IL', 'Israel'), ('YE', 'Yemen'), ('SO', 'Somalia'), ('TJ', 'Tajikistan'), ('CZ', 'Czech Republic'), ('SC', 'Seychelles'), ('RW', 'Rwanda'), ('SG', 'Singapore'), ('SB', 'Solomon Islands'), ('AX', 'Åland Islands'), ('PN', 'Pitcairn'), ('NF', 'Norfolk Island'), ('AR', 'Argentina'), ('BD', 'Bangladesh'), ('GN', 'Guinea'), ('AF', 'Afghanistan'), ('VU', 'Vanuatu'), ('NL', 'Netherlands'), ('LA', "Lao People's Democratic Republic"), ('BW', 'Botswana'), ('BA', 'Bosnia and Herzegovina'), ('ST', 'Sao Tome and Principe'), ('GG', 'Guernsey'), ('BJ', 'Benin'), ('IT', 'Italy'), ('EC', 'Ecuador'), ('LY', 'Libya'), ('FM', 'Micronesia (Federated States of)'), ('AW', 'Aruba'), ('MG', 'Madagascar'), ('UZ', 'Uzbekistan'), ('AD', 'Andorra'), ('HK', 'Hong Kong'), ('PW', 'Palau'), ('PM', 'Saint Pierre and Miquelon'), ('AT', 'Austria'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('ET', 'Ethiopia'), ('US', 'United States of America'), ('CV', 'Cabo Verde'), ('SJ', 'Svalbard and Jan Mayen'), ('IO', 'British Indian Ocean Territory'), ('BB', 'Barbados'), ('CK', 'Cook Islands'), ('NC', 'New Caledonia'), ('BI', 'Burundi'), ('TT', 'Trinidad and Tobago'), ('CG', 'Congo'), ('CM', 'Cameroon'), ('KH', 'Cambodia'), ('TG', 'Togo'), ('CL', 'Chile'), ('CF', 'Central African Republic'), ('IN', 'India'), ('NP', 'Nepal'), ('TC', 'Turks and Caicos Islands'), ('MM', 'Myanmar'), ('MQ', 'Martinique'), ('LI', 'Liechtenstein'), ('JE', 'Jersey'), ('SM', 'San Marino'), ('MZ', 'Mozambique'), ('UA', 'Ukraine'), ('LV', 'Latvia'), ('MH', 'Marshall Islands'), ('AL', 'Albania'), ('TW', 'Taiwan (Province of China)'), ('DO', 'Dominican Republic'), ('ES', 'Spain'), ('IE', 'Ireland'), ('WS', 'Samoa'), ('HR', 'Croatia'), ('AQ', 'Antarctica'), ('ML', 'Mali'), ('NE', 'Niger'), ('BZ', 'Belize'), ('TH', 'Thailand'), ('CN', 'China'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('NG', 'Nigeria'), ('LU', 'Luxembourg'), ('BT', 'Bhutan'), ('NO', 'Norway'), ('CI', "Côte d'Ivoire"), ('LS', 'Lesotho')], max_length=20)),
+ ('vat', models.CharField(verbose_name='VAT number', max_length=64)),
+ ('account', models.OneToOneField(verbose_name='account', related_name='billcontact', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='BillLine',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
+ ('description', models.CharField(verbose_name='description', max_length=256)),
+ ('rate', models.DecimalField(verbose_name='rate', decimal_places=2, max_digits=12, null=True, blank=True)),
+ ('quantity', models.DecimalField(verbose_name='quantity', decimal_places=2, max_digits=12)),
+ ('verbose_quantity', models.CharField(verbose_name='Verbose quantity', max_length=16)),
+ ('subtotal', models.DecimalField(verbose_name='subtotal', decimal_places=2, max_digits=12)),
+ ('tax', models.DecimalField(verbose_name='tax', decimal_places=2, max_digits=2)),
+ ('order_billed_on', models.DateField(verbose_name='order billed', null=True, blank=True)),
+ ('order_billed_until', models.DateField(verbose_name='order billed until', null=True, blank=True)),
+ ('created_on', models.DateField(verbose_name='created', auto_now_add=True)),
+ ('amended_line', models.ForeignKey(verbose_name='amended line', blank=True, null=True, related_name='amendment_lines', to='bills.BillLine')),
+ ('bill', models.ForeignKey(verbose_name='bill', related_name='lines', to='bills.Bill')),
+ ('order', models.ForeignKey(help_text='Informative link back to the order', blank=True, on_delete=django.db.models.deletion.SET_NULL, to='orders.Order', null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='BillSubline',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
+ ('description', models.CharField(verbose_name='description', max_length=256)),
+ ('total', models.DecimalField(decimal_places=2, max_digits=12)),
+ ('type', models.CharField(verbose_name='type', default='OTHER', choices=[('VOLUME', 'Volume'), ('COMPENSATION', 'Compensation'), ('OTHER', 'Other')], max_length=16)),
+ ('line', models.ForeignKey(verbose_name='bill line', related_name='sublines', to='bills.BillLine')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='AmendmentFee',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('bills.bill',),
+ ),
+ migrations.CreateModel(
+ name='AmendmentInvoice',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('bills.bill',),
+ ),
+ migrations.CreateModel(
+ name='Fee',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('bills.bill',),
+ ),
+ migrations.CreateModel(
+ name='Invoice',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('bills.bill',),
+ ),
+ migrations.CreateModel(
+ name='ProForma',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('bills.bill',),
+ ),
+ ]
diff --git a/orchestra/contrib/bills/migrations/0002_auto_20150413_1937.py b/orchestra/contrib/bills/migrations/0002_auto_20150413_1937.py
new file mode 100644
index 00000000..30b23cbb
--- /dev/null
+++ b/orchestra/contrib/bills/migrations/0002_auto_20150413_1937.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bills', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='billcontact',
+ name='country',
+ field=models.CharField(choices=[('KZ', 'Kazakhstan'), ('IM', 'Isle of Man'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('PW', 'Palau'), ('WF', 'Wallis and Futuna'), ('HK', 'Hong Kong'), ('BO', 'Bolivia (Plurinational State of)'), ('RE', 'Réunion'), ('PS', 'Palestine, State of'), ('IE', 'Ireland'), ('CH', 'Switzerland'), ('AR', 'Argentina'), ('LA', "Lao People's Democratic Republic"), ('BA', 'Bosnia and Herzegovina'), ('IR', 'Iran (Islamic Republic of)'), ('BD', 'Bangladesh'), ('ER', 'Eritrea'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('SJ', 'Svalbard and Jan Mayen'), ('ZW', 'Zimbabwe'), ('IN', 'India'), ('TW', 'Taiwan (Province of China)'), ('DO', 'Dominican Republic'), ('PE', 'Peru'), ('HT', 'Haiti'), ('MO', 'Macao'), ('ST', 'Sao Tome and Principe'), ('VG', 'Virgin Islands (British)'), ('ME', 'Montenegro'), ('IT', 'Italy'), ('IQ', 'Iraq'), ('MT', 'Malta'), ('AG', 'Antigua and Barbuda'), ('UZ', 'Uzbekistan'), ('KN', 'Saint Kitts and Nevis'), ('TD', 'Chad'), ('AI', 'Anguilla'), ('MM', 'Myanmar'), ('AM', 'Armenia'), ('UY', 'Uruguay'), ('BB', 'Barbados'), ('BN', 'Brunei Darussalam'), ('CN', 'China'), ('AL', 'Albania'), ('AQ', 'Antarctica'), ('GT', 'Guatemala'), ('NR', 'Nauru'), ('UM', 'United States Minor Outlying Islands'), ('MP', 'Northern Mariana Islands'), ('SR', 'Suriname'), ('GY', 'Guyana'), ('LV', 'Latvia'), ('LS', 'Lesotho'), ('ES', 'Spain'), ('TC', 'Turks and Caicos Islands'), ('VA', 'Holy See'), ('NZ', 'New Zealand'), ('SK', 'Slovakia'), ('BE', 'Belgium'), ('TG', 'Togo'), ('SN', 'Senegal'), ('CG', 'Congo'), ('MN', 'Mongolia'), ('GA', 'Gabon'), ('GW', 'Guinea-Bissau'), ('HU', 'Hungary'), ('TR', 'Turkey'), ('GE', 'Georgia'), ('EH', 'Western Sahara'), ('PN', 'Pitcairn'), ('FJ', 'Fiji'), ('TV', 'Tuvalu'), ('AZ', 'Azerbaijan'), ('MZ', 'Mozambique'), ('GL', 'Greenland'), ('US', 'United States of America'), ('BF', 'Burkina Faso'), ('BT', 'Bhutan'), ('VN', 'Viet Nam'), ('PM', 'Saint Pierre and Miquelon'), ('PY', 'Paraguay'), ('FR', 'France'), ('DZ', 'Algeria'), ('LT', 'Lithuania'), ('NU', 'Niue'), ('MY', 'Malaysia'), ('DM', 'Dominica'), ('NC', 'New Caledonia'), ('NA', 'Namibia'), ('WS', 'Samoa'), ('MW', 'Malawi'), ('BW', 'Botswana'), ('SM', 'San Marino'), ('HM', 'Heard Island and McDonald Islands'), ('IS', 'Iceland'), ('CF', 'Central African Republic'), ('SB', 'Solomon Islands'), ('LK', 'Sri Lanka'), ('ID', 'Indonesia'), ('GR', 'Greece'), ('CO', 'Colombia'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('KR', 'Korea (the Republic of)'), ('SZ', 'Swaziland'), ('KE', 'Kenya'), ('AF', 'Afghanistan'), ('AE', 'United Arab Emirates'), ('DK', 'Denmark'), ('TZ', 'Tanzania, United Republic of'), ('AD', 'Andorra'), ('KH', 'Cambodia'), ('CY', 'Cyprus'), ('GS', 'South Georgia and the South Sandwich Islands'), ('EG', 'Egypt'), ('UG', 'Uganda'), ('TK', 'Tokelau'), ('MS', 'Montserrat'), ('YT', 'Mayotte'), ('MU', 'Mauritius'), ('BS', 'Bahamas'), ('CD', 'Congo (the Democratic Republic of the)'), ('CZ', 'Czech Republic'), ('CR', 'Costa Rica'), ('NL', 'Netherlands'), ('GQ', 'Equatorial Guinea'), ('SS', 'South Sudan'), ('RW', 'Rwanda'), ('VU', 'Vanuatu'), ('DE', 'Germany'), ('PL', 'Poland'), ('CX', 'Christmas Island'), ('AO', 'Angola'), ('BZ', 'Belize'), ('CK', 'Cook Islands'), ('TO', 'Tonga'), ('MA', 'Morocco'), ('CU', 'Cuba'), ('JM', 'Jamaica'), ('NI', 'Nicaragua'), ('AT', 'Austria'), ('FI', 'Finland'), ('FO', 'Faroe Islands'), ('VI', 'Virgin Islands (U.S.)'), ('BR', 'Brazil'), ('SY', 'Syrian Arab Republic'), ('ET', 'Ethiopia'), ('BJ', 'Benin'), ('PH', 'Philippines'), ('AS', 'American Samoa'), ('TL', 'Timor-Leste'), ('AU', 'Australia'), ('SX', 'Sint Maarten (Dutch part)'), ('PG', 'Papua New Guinea'), ('NG', 'Nigeria'), ('SL', 'Sierra Leone'), ('LB', 'Lebanon'), ('OM', 'Oman'), ('SG', 'Singapore'), ('CM', 'Cameroon'), ('PT', 'Portugal'), ('KM', 'Comoros'), ('IO', 'British Indian Ocean Territory'), ('BM', 'Bermuda'), ('YE', 'Yemen'), ('RU', 'Russian Federation'), ('GM', 'Gambia'), ('SI', 'Slovenia'), ('GI', 'Gibraltar'), ('LY', 'Libya'), ('GU', 'Guam'), ('LU', 'Luxembourg'), ('RO', 'Romania'), ('MD', 'Moldova (the Republic of)'), ('BL', 'Saint Barthélemy'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('EE', 'Estonia'), ('LC', 'Saint Lucia'), ('TJ', 'Tajikistan'), ('IL', 'Israel'), ('PA', 'Panama'), ('PR', 'Puerto Rico'), ('CL', 'Chile'), ('KP', "Korea (the Democratic People's Republic of)"), ('GF', 'French Guiana'), ('CI', "Côte d'Ivoire"), ('MR', 'Mauritania'), ('NF', 'Norfolk Island'), ('BG', 'Bulgaria'), ('SD', 'Sudan'), ('NO', 'Norway'), ('GG', 'Guernsey'), ('CV', 'Cabo Verde'), ('GP', 'Guadeloupe'), ('BV', 'Bouvet Island'), ('TT', 'Trinidad and Tobago'), ('SO', 'Somalia'), ('DJ', 'Djibouti'), ('MC', 'Monaco'), ('JP', 'Japan'), ('NP', 'Nepal'), ('JE', 'Jersey'), ('TN', 'Tunisia'), ('RS', 'Serbia'), ('NE', 'Niger'), ('ML', 'Mali'), ('KW', 'Kuwait'), ('PF', 'French Polynesia'), ('ZA', 'South Africa'), ('GH', 'Ghana'), ('BH', 'Bahrain'), ('CC', 'Cocos (Keeling) Islands'), ('MQ', 'Martinique'), ('AW', 'Aruba'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('JO', 'Jordan'), ('LI', 'Liechtenstein'), ('TM', 'Turkmenistan'), ('MX', 'Mexico'), ('LR', 'Liberia'), ('GD', 'Grenada'), ('HN', 'Honduras'), ('SA', 'Saudi Arabia'), ('PK', 'Pakistan'), ('CA', 'Canada'), ('CW', 'Curaçao'), ('SV', 'El Salvador'), ('SC', 'Seychelles'), ('QA', 'Qatar'), ('EC', 'Ecuador'), ('VC', 'Saint Vincent and the Grenadines'), ('HR', 'Croatia'), ('MV', 'Maldives'), ('KI', 'Kiribati'), ('FM', 'Micronesia (Federated States of)'), ('BY', 'Belarus'), ('FK', 'Falkland Islands [Malvinas]'), ('TF', 'French Southern Territories'), ('AX', 'Åland Islands'), ('MG', 'Madagascar'), ('SE', 'Sweden'), ('BI', 'Burundi'), ('KY', 'Cayman Islands'), ('MF', 'Saint Martin (French part)'), ('ZM', 'Zambia'), ('GN', 'Guinea'), ('UA', 'Ukraine'), ('TH', 'Thailand'), ('KG', 'Kyrgyzstan'), ('MH', 'Marshall Islands')], verbose_name='country', max_length=20, default='ES'),
+ ),
+ migrations.AlterField(
+ model_name='billline',
+ name='tax',
+ field=models.DecimalField(decimal_places=2, verbose_name='tax', max_digits=4),
+ ),
+ ]
diff --git a/orchestra/contrib/bills/migrations/__init__.py b/orchestra/contrib/bills/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py
index e46778ec..6a25a270 100644
--- a/orchestra/contrib/bills/models.py
+++ b/orchestra/contrib/bills/models.py
@@ -2,6 +2,8 @@ from dateutil.relativedelta import relativedelta
from django.core.validators import ValidationError, RegexValidator
from django.db import models
+from django.db.models import F, Sum
+from django.db.models.functions import Coalesce
from django.template import loader, Context
from django.utils import timezone, translation
from django.utils.encoding import force_text
@@ -89,6 +91,7 @@ class Bill(models.Model):
is_sent = models.BooleanField(_("sent"), default=False)
due_on = models.DateField(_("due on"), null=True, blank=True)
updated_on = models.DateField(_("updated on"), auto_now=True)
+ # TODO allways compute total or what?
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
comments = models.TextField(_("comments"), blank=True)
html = models.TextField(_("HTML"), blank=True)
@@ -227,18 +230,17 @@ class Bill(models.Model):
def get_subtotals(self):
subtotals = {}
- for line in self.lines.all():
- subtotal, taxes = subtotals.get(line.tax, (0, 0))
- subtotal += line.get_total()
- subtotals[line.tax] = (subtotal, (line.tax/100)*subtotal)
+ 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))
return subtotals
def get_total(self):
- total = 0
- for tax, subtotal in self.get_subtotals().items():
- subtotal, taxes = subtotal
- total += subtotal + taxes
- return total
+ 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)
class Invoice(Bill):
@@ -272,12 +274,12 @@ class BillLine(models.Model):
description = models.CharField(_("description"), max_length=256)
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
+ verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
- tax = models.DecimalField(_("tax"), max_digits=2, decimal_places=2)
+ tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
# Undo
# initial = models.DateTimeField(null=True)
# end = models.DateTimeField(null=True)
-
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
@@ -297,10 +299,11 @@ class BillLine(models.Model):
def get_total(self):
""" Computes subline discounts """
- total = self.subtotal
- for subline in self.sublines.all():
- total += subline.total
- return total
+ 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
def undo(self):
# TODO warn user that undoing bills with compensations lead to compensation lost
@@ -313,12 +316,11 @@ class BillLine(models.Model):
self.order.billed_on = self.order_billed_on
self.delete()
- def save(self, *args, **kwargs):
- # TODO cost and consistency of this shit
- super(BillLine, self).save(*args, **kwargs)
- if self.bill.is_open:
- self.bill.total = self.bill.get_total()
- self.bill.save(update_fields=['total'])
+# 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'])
class BillSubline(models.Model):
@@ -339,12 +341,12 @@ class BillSubline(models.Model):
total = models.DecimalField(max_digits=12, decimal_places=2)
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
- 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'])
+# 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'])
accounts.register(Bill)
diff --git a/orchestra/contrib/bills/templates/bills/microspective.html b/orchestra/contrib/bills/templates/bills/microspective.html
index cd555390..be997133 100644
--- a/orchestra/contrib/bills/templates/bills/microspective.html
+++ b/orchestra/contrib/bills/templates/bills/microspective.html
@@ -79,7 +79,7 @@
{% with sublines=line.sublines.all %}
{{ line.id }}
{{ line.description }}
- {{ line.quantity|default:" " }}
+ {{ line.get_verbose_quantity|default:" "|safe }}
{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}
{{ line.subtotal }} &{{ currency.lower }};
@@ -96,7 +96,7 @@