diff --git a/TODO.md b/TODO.md
index 19ad5cae..5edb02a1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -276,8 +276,14 @@ https://code.djangoproject.com/ticket/24576
* force ignore slack billing period overridig when billing
* fpm reload starts new pools?
* rename resource.monitors to resource.backends ?
-* abstract model classes enabling overriding?
+* abstract model classes that enabling overriding, and ORCHESTRA_DATABASE_MODEL settings + orchestra.get_database_model() instead of explicitly importing from orchestra.contrib.databases.models import Database.. (Admin and REST API are fucked then?)
# Ignore superusers & co on billing: list filter doesn't work nor ignore detection
# bill.totals make it 100% computed?
* joomla: wget https://github.com/joomla/joomla-cms/releases/download/3.4.1/Joomla_3.4.1-Stable-Full_Package.tar.gz -O - | tar xvfz -
+
+
+
+# Link related orders on bill line
+# Customize those service.descriptions that are
+# replace multichoicefield and jsonfield by ArrayField, HStoreField
diff --git a/docs/images/orchestration.svg b/docs/images/orchestration.svg
new file mode 100644
index 00000000..65ea3de2
--- /dev/null
+++ b/docs/images/orchestration.svg
@@ -0,0 +1,3628 @@
+
+
+
+
diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py
index fede4fe3..37e7eff9 100644
--- a/orchestra/admin/utils.py
+++ b/orchestra/admin/utils.py
@@ -107,10 +107,15 @@ def admin_link(*args, **kwargs):
if not getattr(obj, 'pk', None):
return '---'
url = change_url(obj)
+ display = kwargs.get('display')
+ if display:
+ display = getattr(obj, display, 'merda')
+ else:
+ display = obj
extra = ''
if kwargs['popup']:
extra = 'onclick="return showAddAnotherPopup(this);"'
- return '%s' % (url, extra, obj)
+ return '%s' % (url, extra, display)
@admin_field
diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py
index fee78fd1..f965df1d 100644
--- a/orchestra/conf/base_settings.py
+++ b/orchestra/conf/base_settings.py
@@ -35,6 +35,7 @@ MEDIA_URL = '/media/'
ALLOWED_HOSTS = '*'
+
# Set this to True to wrap each HTTP request in a transaction on this database.
# ATOMIC REQUESTS do not wrap middlewares (orchestra.contrib.orchestration.middlewares.OperationsMiddleware)
ATOMIC_REQUESTS = False
@@ -101,6 +102,7 @@ INSTALLED_APPS = (
'rest_framework',
'rest_framework.authtoken',
'passlib.ext.django',
+ 'django_countries',
# Django.contrib
'django.contrib.auth',
diff --git a/orchestra/conf/project_template/project_name/settings.py b/orchestra/conf/project_template/project_name/settings.py
index 20dee7c2..3accc5c5 100644
--- a/orchestra/conf/project_template/project_name/settings.py
+++ b/orchestra/conf/project_template/project_name/settings.py
@@ -40,6 +40,7 @@ DATABASES = {
'PASSWORD': 'orchestra', # Not used with sqlite3.
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ 'CONN_MAX_AGE': 300,
}
}
diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py
index c8b0d887..ad4e51ef 100644
--- a/orchestra/contrib/bills/admin.py
+++ b/orchestra/contrib/bills/admin.py
@@ -11,7 +11,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
-from orchestra.admin.utils import admin_date, insertattr
+from orchestra.admin.utils import admin_date, insertattr, admin_link
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
@@ -29,8 +29,13 @@ PAYMENT_STATE_COLORS = {
class BillLineInline(admin.TabularInline):
model = BillLine
- fields = ('description', 'rate', 'quantity', 'tax', 'subtotal', 'display_total')
- readonly_fields = ('display_total',)
+ fields = (
+ 'description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
+ 'subtotal', 'display_total',
+ )
+ readonly_fields = ('display_total', 'order_link')
+
+ order_link = admin_link('order', display='pk')
def display_total(self, line):
total = line.get_total()
@@ -46,9 +51,9 @@ class BillLineInline(admin.TabularInline):
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'description':
- kwargs['widget'] = forms.TextInput(attrs={'size':'110'})
- else:
- kwargs['widget'] = forms.TextInput(attrs={'size':'13'})
+ kwargs['widget'] = forms.TextInput(attrs={'size':'50'})
+ elif db_field.name not in ('start_on', 'end_on'):
+ kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
def get_queryset(self, request):
@@ -61,7 +66,8 @@ class ClosedBillLineInline(BillLineInline):
# https://code.djangoproject.com/ticket/9025
fields = (
- 'display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total'
+ 'display_description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
+ 'display_subtotal', 'display_total'
)
readonly_fields = fields
@@ -157,10 +163,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines")
def display_total(self, bill):
- return "%s &%s;" % (round(bill.totals, 2), settings.BILLS_CURRENCY.lower())
+ return "%s &%s;" % (round(bill.computed_total or 0, 2), settings.BILLS_CURRENCY.lower())
display_total.allow_tags = True
display_total.short_description = _("total")
- display_total.admin_order_field = 'totals'
+ display_total.admin_order_field = 'computed_total'
def type_link(self, bill):
bill_type = bill.type.lower()
@@ -235,7 +241,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(
models.Count('lines'),
- totals=Sum(
+ computed_total=Sum(
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
),
)
diff --git a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo
index a3fd9f38..e2aeb0f7 100644
Binary files a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo and b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo differ
diff --git a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po
index 57872c77..3c731913 100644
--- a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po
+++ b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-03-29 20:21+0000\n"
+"POT-Creation-Date: 2015-04-20 11:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME
Please select a "
"payment source for the selected bills"
msgstr ""
-#: actions.py:101
+#: actions.py:102
msgid "Close"
msgstr ""
-#: actions.py:112
+#: actions.py:113
msgid "Resend"
msgstr ""
-#: actions.py:129 models.py:309
+#: actions.py:130 models.py:312
msgid "Not enough information stored for undoing"
msgstr ""
-#: actions.py:132 models.py:311
+#: actions.py:133 models.py:314
msgid "Dates don't match"
msgstr ""
-#: actions.py:147
+#: actions.py:148
msgid "Can not move lines which are not in open state."
msgstr ""
-#: actions.py:152
+#: actions.py:153
msgid "Can not move lines from different accounts"
msgstr ""
-#: actions.py:160
+#: actions.py:161
msgid "Target account different than lines account."
msgstr ""
-#: actions.py:167
+#: actions.py:168
msgid "Lines moved"
msgstr ""
-#: admin.py:41 forms.py:12
+#: admin.py:43 admin.py:86 forms.py:11
msgid "Total"
msgstr ""
-#: admin.py:69
+#: admin.py:73
msgid "Description"
msgstr "Descripció"
-#: admin.py:77
+#: admin.py:81
msgid "Subtotal"
msgstr ""
-#: admin.py:104
+#: admin.py:113
msgid "Manage bill lines of multiple bills."
msgstr ""
-#: admin.py:109
+#: admin.py:118
#, python-format
msgid "Manage %s bill lines."
msgstr ""
-#: admin.py:129
+#: admin.py:138
msgid "Raw"
msgstr ""
-#: admin.py:147
+#: admin.py:157
msgid "lines"
msgstr ""
-#: admin.py:152 templates/bills/microspective.html:107
+#: admin.py:162 templates/bills/microspective.html:107
msgid "total"
msgstr ""
-#: admin.py:160 models.py:85 models.py:340
+#: admin.py:170 models.py:87 models.py:342
msgid "type"
msgstr "tipus"
-#: admin.py:177
+#: admin.py:187
msgid "Payment"
msgstr "Pagament"
@@ -131,15 +131,15 @@ msgstr "Pagament"
msgid "All"
msgstr "Tot"
-#: filters.py:18 models.py:75
+#: filters.py:18 models.py:77
msgid "Invoice"
msgstr "Factura"
-#: filters.py:19 models.py:76
+#: filters.py:19 models.py:78
msgid "Amendment invoice"
msgstr "Factura rectificativa"
-#: filters.py:20 models.py:77
+#: filters.py:20 models.py:79
msgid "Fee"
msgstr "Quota de soci"
@@ -167,15 +167,15 @@ msgstr "No"
msgid "Number"
msgstr ""
-#: forms.py:11
+#: forms.py:10
msgid "Account"
msgstr ""
-#: forms.py:13
+#: forms.py:12
msgid "Type"
msgstr ""
-#: forms.py:15
+#: forms.py:13
msgid "Source"
msgstr ""
@@ -193,158 +193,178 @@ msgstr ""
msgid "Main"
msgstr ""
-#: models.py:20 models.py:83
+#: models.py:22 models.py:85
msgid "account"
msgstr ""
-#: models.py:22
+#: models.py:24
msgid "name"
msgstr ""
-#: models.py:23
+#: models.py:25
msgid "Account full name will be used when left blank."
msgstr ""
-#: models.py:24
+#: models.py:26
msgid "address"
msgstr ""
-#: models.py:25
+#: models.py:27
msgid "city"
msgstr ""
-#: models.py:27
+#: models.py:29
msgid "zip code"
msgstr ""
-#: models.py:28
+#: models.py:30
msgid "Enter a valid zipcode."
msgstr ""
-#: models.py:29
+#: models.py:31
msgid "country"
msgstr ""
-#: models.py:32
+#: models.py:34
msgid "VAT number"
msgstr "NIF"
-#: models.py:64
+#: models.py:66
msgid "Paid"
msgstr ""
-#: models.py:65
+#: models.py:67
msgid "Pending"
msgstr ""
-#: models.py:66
+#: models.py:68
msgid "Bad debt"
msgstr ""
-#: models.py:78
+#: models.py:80
msgid "Amendment Fee"
msgstr ""
-#: models.py:79
+#: models.py:81
msgid "Pro forma"
msgstr ""
-#: models.py:82
+#: models.py:84
msgid "number"
msgstr ""
-#: models.py:86
+#: models.py:88
msgid "created on"
msgstr ""
-#: models.py:87
+#: models.py:89
msgid "closed on"
msgstr ""
-#: models.py:88
+#: models.py:90
msgid "open"
msgstr ""
-#: models.py:89
+#: models.py:91
msgid "sent"
msgstr ""
-#: models.py:90
+#: models.py:92
msgid "due on"
msgstr ""
-#: models.py:91
+#: models.py:93
msgid "updated on"
msgstr ""
-#: models.py:93
+#: models.py:96
msgid "comments"
-msgstr ""
+msgstr "comentaris"
-#: models.py:94
+#: models.py:97
msgid "HTML"
msgstr ""
-#: models.py:271
+#: models.py:273
msgid "bill"
msgstr ""
-#: models.py:272 models.py:337 templates/bills/microspective.html:73
+#: models.py:274 models.py:339 templates/bills/microspective.html:73
msgid "description"
msgstr "descripció"
-#: models.py:273
+#: models.py:275
msgid "rate"
msgstr "tarifa"
-#: models.py:274
+#: models.py:276
msgid "quantity"
msgstr "quantitat"
-#: models.py:275 templates/bills/microspective.html:76
+#: models.py:277
+#, fuzzy
+#| msgid "quantity"
+msgid "Verbose quantity"
+msgstr "quantitat"
+
+#: models.py:278 templates/bills/microspective.html:76
+#: templates/bills/microspective.html:100
msgid "subtotal"
msgstr ""
-#: models.py:276
+#: models.py:279
msgid "tax"
msgstr "impostos"
-#: models.py:282
+#: models.py:284
msgid "Informative link back to the order"
msgstr ""
-#: models.py:283
+#: models.py:285
msgid "order billed"
msgstr ""
-#: models.py:284
+#: models.py:286
msgid "order billed until"
msgstr ""
-#: models.py:285
+#: models.py:287
msgid "created"
msgstr ""
-#: models.py:287
+#: models.py:289
msgid "amended line"
msgstr ""
-#: models.py:330
+#: models.py:332
msgid "Volume"
msgstr ""
-#: models.py:331
+#: models.py:333
msgid "Compensation"
msgstr ""
-#: models.py:332
+#: models.py:334
msgid "Other"
msgstr ""
-#: models.py:336
+#: models.py:338
msgid "bill line"
msgstr ""
+#: templates/bills/microspective.html:49
+msgid "DUE DATE"
+msgstr "VENCIMENT"
+
+#: templates/bills/microspective.html:53
+msgid "TOTAL"
+msgstr "TOTAL"
+
+#: templates/bills/microspective.html:57
+#, python-format
+msgid "%(bill_type|upper)s DATE "
+msgstr "DATA %(bill_type|upper)s"
+
#: templates/bills/microspective.html:74
msgid "hrs/qty"
msgstr "hrs/quant"
@@ -360,7 +380,7 @@ msgstr "IVA"
#: templates/bills/microspective.html:103
msgid "taxes"
-msgstr ""
+msgstr "impostos"
#: templates/bills/microspective.html:119
msgid "COMMENTS"
@@ -371,15 +391,17 @@ msgid "PAYMENT"
msgstr "PAGAMENT"
#: templates/bills/microspective.html:129
-#, python-format
msgid ""
"\n"
" You can pay our %(type)s by bank transfer.
\n"
-" Please make sure to state your name and the "
-"%(bill.get_type_display.lower)s number.\n"
+" Please make sure to state your name and the %(type)s "
+"number.\n"
" Our bank account number is
\n"
" "
msgstr ""
+"\n"
+"Pots pagar aquesta %(type)s per transferencia banacaria.
Inclou el teu "
+"nom i el numero de %(type)s. El nostre compte bancari és"
#: templates/bills/microspective.html:138
msgid "QUESTIONS"
diff --git a/orchestra/contrib/bills/migrations/0003_auto_20150420_1223.py b/orchestra/contrib/bills/migrations/0003_auto_20150420_1223.py
new file mode 100644
index 00000000..802b87aa
--- /dev/null
+++ b/orchestra/contrib/bills/migrations/0003_auto_20150420_1223.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.utils.timezone import utc
+import datetime
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bills', '0002_auto_20150413_1937'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='billline',
+ name='end_on',
+ field=models.DateTimeField(null=True),
+ ),
+ migrations.AddField(
+ model_name='billline',
+ name='start_on',
+ field=models.DateTimeField(default=datetime.datetime(2015, 4, 20, 12, 23, 38, 471684, tzinfo=utc)),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='billcontact',
+ name='country',
+ field=models.CharField(max_length=20, verbose_name='country', default='ES', choices=[('IQ', 'Iraq'), ('MN', 'Mongolia'), ('CX', 'Christmas Island'), ('RO', 'Romania'), ('KR', 'Korea (the Republic of)'), ('TH', 'Thailand'), ('FO', 'Faroe Islands'), ('CZ', 'Czech Republic'), ('ER', 'Eritrea'), ('MA', 'Morocco'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('GA', 'Gabon'), ('RS', 'Serbia'), ('HM', 'Heard Island and McDonald Islands'), ('GW', 'Guinea-Bissau'), ('KE', 'Kenya'), ('BE', 'Belgium'), ('MV', 'Maldives'), ('SR', 'Suriname'), ('AZ', 'Azerbaijan'), ('KG', 'Kyrgyzstan'), ('UA', 'Ukraine'), ('CF', 'Central African Republic'), ('PM', 'Saint Pierre and Miquelon'), ('GU', 'Guam'), ('ZM', 'Zambia'), ('AI', 'Anguilla'), ('VU', 'Vanuatu'), ('MZ', 'Mozambique'), ('TF', 'French Southern Territories'), ('PG', 'Papua New Guinea'), ('TT', 'Trinidad and Tobago'), ('NF', 'Norfolk Island'), ('KM', 'Comoros'), ('JM', 'Jamaica'), ('NU', 'Niue'), ('MH', 'Marshall Islands'), ('AL', 'Albania'), ('KY', 'Cayman Islands'), ('FR', 'France'), ('BA', 'Bosnia and Herzegovina'), ('GD', 'Grenada'), ('KP', "Korea (the Democratic People's Republic of)"), ('SZ', 'Swaziland'), ('TN', 'Tunisia'), ('CR', 'Costa Rica'), ('IO', 'British Indian Ocean Territory'), ('BY', 'Belarus'), ('ST', 'Sao Tome and Principe'), ('VC', 'Saint Vincent and the Grenadines'), ('CH', 'Switzerland'), ('AG', 'Antigua and Barbuda'), ('TO', 'Tonga'), ('CG', 'Congo'), ('MC', 'Monaco'), ('PS', 'Palestine, State of'), ('YE', 'Yemen'), ('PW', 'Palau'), ('VG', 'Virgin Islands (British)'), ('MQ', 'Martinique'), ('NZ', 'New Zealand'), ('TZ', 'Tanzania, United Republic of'), ('KZ', 'Kazakhstan'), ('NC', 'New Caledonia'), ('IT', 'Italy'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('GI', 'Gibraltar'), ('EE', 'Estonia'), ('PN', 'Pitcairn'), ('TV', 'Tuvalu'), ('TJ', 'Tajikistan'), ('FJ', 'Fiji'), ('OM', 'Oman'), ('MY', 'Malaysia'), ('GL', 'Greenland'), ('PE', 'Peru'), ('SX', 'Sint Maarten (Dutch part)'), ('CY', 'Cyprus'), ('GG', 'Guernsey'), ('GS', 'South Georgia and the South Sandwich Islands'), ('SK', 'Slovakia'), ('BO', 'Bolivia (Plurinational State of)'), ('CI', "Côte d'Ivoire"), ('MG', 'Madagascar'), ('UZ', 'Uzbekistan'), ('IR', 'Iran (Islamic Republic of)'), ('CV', 'Cabo Verde'), ('MX', 'Mexico'), ('GM', 'Gambia'), ('TC', 'Turks and Caicos Islands'), ('TK', 'Tokelau'), ('BZ', 'Belize'), ('SE', 'Sweden'), ('WF', 'Wallis and Futuna'), ('HT', 'Haiti'), ('MR', 'Mauritania'), ('GN', 'Guinea'), ('MU', 'Mauritius'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('LS', 'Lesotho'), ('LU', 'Luxembourg'), ('JE', 'Jersey'), ('MF', 'Saint Martin (French part)'), ('PF', 'French Polynesia'), ('IS', 'Iceland'), ('LA', "Lao People's Democratic Republic"), ('IN', 'India'), ('AX', 'Åland Islands'), ('VN', 'Viet Nam'), ('MM', 'Myanmar'), ('RW', 'Rwanda'), ('WS', 'Samoa'), ('MW', 'Malawi'), ('EH', 'Western Sahara'), ('GH', 'Ghana'), ('DO', 'Dominican Republic'), ('HN', 'Honduras'), ('AS', 'American Samoa'), ('TD', 'Chad'), ('NG', 'Nigeria'), ('DJ', 'Djibouti'), ('ZA', 'South Africa'), ('BI', 'Burundi'), ('TM', 'Turkmenistan'), ('EC', 'Ecuador'), ('GE', 'Georgia'), ('NP', 'Nepal'), ('AT', 'Austria'), ('PA', 'Panama'), ('BR', 'Brazil'), ('MD', 'Moldova (the Republic of)'), ('GY', 'Guyana'), ('KH', 'Cambodia'), ('CL', 'Chile'), ('NO', 'Norway'), ('SJ', 'Svalbard and Jan Mayen'), ('BJ', 'Benin'), ('CO', 'Colombia'), ('CC', 'Cocos (Keeling) Islands'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('UM', 'United States Minor Outlying Islands'), ('BF', 'Burkina Faso'), ('UG', 'Uganda'), ('GR', 'Greece'), ('TW', 'Taiwan (Province of China)'), ('SO', 'Somalia'), ('DE', 'Germany'), ('PL', 'Poland'), ('TL', 'Timor-Leste'), ('BT', 'Bhutan'), ('CA', 'Canada'), ('HR', 'Croatia'), ('BB', 'Barbados'), ('LR', 'Liberia'), ('PR', 'Puerto Rico'), ('GF', 'French Guiana'), ('IM', 'Isle of Man'), ('VI', 'Virgin Islands (U.S.)'), ('HU', 'Hungary'), ('ES', 'Spain'), ('AR', 'Argentina'), ('CU', 'Cuba'), ('AU', 'Australia'), ('NI', 'Nicaragua'), ('SS', 'South Sudan'), ('IE', 'Ireland'), ('BH', 'Bahrain'), ('GQ', 'Equatorial Guinea'), ('SC', 'Seychelles'), ('PH', 'Philippines'), ('SM', 'San Marino'), ('ID', 'Indonesia'), ('HK', 'Hong Kong'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('PY', 'Paraguay'), ('ZW', 'Zimbabwe'), ('GT', 'Guatemala'), ('CD', 'Congo (the Democratic Republic of the)'), ('ME', 'Montenegro'), ('RE', 'Réunion'), ('LK', 'Sri Lanka'), ('FK', 'Falkland Islands [Malvinas]'), ('BL', 'Saint Barthélemy'), ('NR', 'Nauru'), ('LV', 'Latvia'), ('KW', 'Kuwait'), ('IL', 'Israel'), ('BV', 'Bouvet Island'), ('SY', 'Syrian Arab Republic'), ('BS', 'Bahamas'), ('CW', 'Curaçao'), ('CM', 'Cameroon'), ('SV', 'El Salvador'), ('SL', 'Sierra Leone'), ('DM', 'Dominica'), ('US', 'United States of America'), ('LB', 'Lebanon'), ('AD', 'Andorra'), ('CN', 'China'), ('SN', 'Senegal'), ('LI', 'Liechtenstein'), ('JP', 'Japan'), ('KI', 'Kiribati'), ('BM', 'Bermuda'), ('EG', 'Egypt'), ('UY', 'Uruguay'), ('BD', 'Bangladesh'), ('PK', 'Pakistan'), ('MT', 'Malta'), ('CK', 'Cook Islands'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('SI', 'Slovenia'), ('ET', 'Ethiopia'), ('BG', 'Bulgaria'), ('GP', 'Guadeloupe'), ('BW', 'Botswana'), ('VA', 'Holy See'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('LY', 'Libya'), ('TR', 'Turkey'), ('TG', 'Togo'), ('LT', 'Lithuania'), ('QA', 'Qatar'), ('AM', 'Armenia'), ('DZ', 'Algeria'), ('SD', 'Sudan'), ('ML', 'Mali'), ('MP', 'Northern Mariana Islands'), ('LC', 'Saint Lucia'), ('NA', 'Namibia'), ('MO', 'Macao'), ('KN', 'Saint Kitts and Nevis'), ('JO', 'Jordan'), ('RU', 'Russian Federation'), ('AW', 'Aruba'), ('AF', 'Afghanistan'), ('SG', 'Singapore'), ('DK', 'Denmark'), ('MS', 'Montserrat'), ('YT', 'Mayotte'), ('NL', 'Netherlands'), ('FM', 'Micronesia (Federated States of)'), ('BN', 'Brunei Darussalam'), ('AE', 'United Arab Emirates'), ('PT', 'Portugal'), ('NE', 'Niger'), ('FI', 'Finland')]),
+ ),
+ ]
diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py
index 6a25a270..83cd7fa6 100644
--- a/orchestra/contrib/bills/models.py
+++ b/orchestra/contrib/bills/models.py
@@ -1,3 +1,4 @@
+import datetime
from dateutil.relativedelta import relativedelta
from django.core.validators import ValidationError, RegexValidator
@@ -277,9 +278,8 @@ class BillLine(models.Model):
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=4, decimal_places=2)
- # Undo
-# initial = models.DateTimeField(null=True)
-# end = models.DateTimeField(null=True)
+ start_on = models.DateField(_("start"))
+ end_on = models.DateField(_("end"), 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)
@@ -305,6 +305,15 @@ class BillLine(models.Model):
def get_verbose_quantity(self):
return self.verbose_quantity or self.quantity
+ 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)
+
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']:
diff --git a/orchestra/contrib/bills/templates/bills/microspective-fee.html b/orchestra/contrib/bills/templates/bills/microspective-fee.html
index 7c4d4da0..7ba9d6b4 100644
--- a/orchestra/contrib/bills/templates/bills/microspective-fee.html
+++ b/orchestra/contrib/bills/templates/bills/microspective-fee.html
@@ -108,7 +108,7 @@ hr {
{{ seller.address }}
- {{ seller.zipcode }} - {{ seller.city }}
- {{ seller.country }}
+ {{ seller.zipcode }} - {% trans seller.city %}
+ {% trans seller.get_country_display %}
{{ seller_info.phone }}
{{ seller_info.email }}
@@ -40,21 +40,21 @@
{% block summary %}
(.*)
', response.content) + errors = re.findall(r'\n\t(.*)
', response.content.decode('utf8')) raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code) def get_id(self, session, webapp): @@ -41,7 +41,7 @@ class WordpressMuBackend(ServiceController): '%s' % webapp.name ) - content = session.get(search).content + content = session.get(search).content.decode('utf8') # Get id ids = regex.search(content) if not ids: @@ -64,7 +64,7 @@ class WordpressMuBackend(ServiceController): except RuntimeError: url = self.get_base_url() url += '/wp-admin/network/site-new.php' - content = session.get(url).content + content = session.get(url).content.decode('utf8') wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"') wpnonce = wpnonce.search(content).groups()[0] @@ -94,7 +94,7 @@ class WordpressMuBackend(ServiceController): delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog' delete += '&id=%d&_wpnonce=%s' % (id, wpnonce) - content = session.get(delete).content + content = session.get(delete).content.decode('utf8') wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"') wpnonce = wpnonce.search(content).groups()[0] data = { diff --git a/orchestra/contrib/saas/services/gitlab.py b/orchestra/contrib/saas/services/gitlab.py index 38343389..33334e51 100644 --- a/orchestra/contrib/saas/services/gitlab.py +++ b/orchestra/contrib/saas/services/gitlab.py @@ -11,12 +11,12 @@ from .. import settings class GitLabForm(SoftwareServiceForm): email = forms.EmailField(label=_("Email"), - help_text=_("Initial email address, changes on the GitLab server are not reflected here.")) + help_text=_("Initial email address, changes on the GitLab server are not reflected here.")) class GitLaChangebForm(GitLabForm): user_id = forms.IntegerField(label=("User ID"), widget=widgets.ShowTextWidget, - help_text=_("ID of this user on the GitLab server, the only attribute that not changes.")) + help_text=_("ID of this user on the GitLab server, the only attribute that not changes.")) class GitLabSerializer(serializers.Serializer): diff --git a/orchestra/contrib/saas/services/options.py b/orchestra/contrib/saas/services/options.py index 3b3523d3..e3ab9283 100644 --- a/orchestra/contrib/saas/services/options.py +++ b/orchestra/contrib/saas/services/options.py @@ -106,6 +106,8 @@ class SoftwareService(plugins.Plugin): except IndexError: pass else: + if log.state != log.SUCCESS: + raise ValidationError(_("Validate creation execution has failed.")) errors = {} if 'user-exists' in log.stdout: errors['name'] = _("User with this username already exists.") diff --git a/orchestra/contrib/services/handlers.py b/orchestra/contrib/services/handlers.py index 6ada4d4e..293977fb 100644 --- a/orchestra/contrib/services/handlers.py +++ b/orchestra/contrib/services/handlers.py @@ -23,8 +23,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): Relax and enjoy the journey. """ - _VOLUME = 'VOLUME' - _COMPENSATION = 'COMPENSATION' + _VOLUME = 'volume' + _COMPENSATION = 'compensation' model = None @@ -42,29 +42,27 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): def validate_content_type(self, service): pass - def validate_match(self, service): - if not service.match: - service.match = 'True' + def validate_expression(self, service, method): try: obj = service.content_type.model_class().objects.all()[0] except IndexError: return try: - bool(self.matches(obj)) + bool(getattr(self, method)(obj)) except Exception as exception: name = type(exception).__name__ raise ValidationError(': '.join((name, str(exception)))) + def validate_match(self, service): + if not service.match: + service.match = 'True' + self.validate_expression(service, 'matches') + def validate_metric(self, service): - try: - obj = service.content_type.model_class().objects.all()[0] - except IndexError: - return - try: - bool(self.get_metric(obj)) - except Exception as exception: - name = type(exception).__name__ - raise ValidationError(': '.join((name, str(exception)))) + self.validate_expression(service, 'get_metric') + + def validate_order_description(self, service): + self.validate_expression(service, 'get_order_description') def get_content_type(self): if not self.model: @@ -72,15 +70,26 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): app_label, model = self.model.split('.') return ContentType.objects.get_by_natural_key(app_label, model.lower()) + def get_expression_context(self, instance): + return { + 'instance': instance, + 'obj': instance, + 'ugettext': ugettext, + 'handler': self, + 'service': self.service, + instance._meta.model_name: instance, + 'math': math, + 'logsteps': lambda n, size=1: \ + round(n/(decimal.Decimal(size*10**int(math.log10(max(n, 1))))))*size*10**int(math.log10(max(n, 1))), + 'log10': math.log10, + 'Decimal': decimal.Decimal, + } + def matches(self, instance): if not self.match: # Blank expressions always evaluate True return True - safe_locals = { - 'instance': instance, - 'obj': instance, - instance._meta.model_name: instance, - } + safe_locals = self.get_expression_context(instance) return eval(self.match, safe_locals) def get_ignore_delta(self): @@ -113,27 +122,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): def get_metric(self, instance): if self.metric: - safe_locals = { - instance._meta.model_name: instance, - 'instance': instance, - 'math': math, - 'logsteps': lambda n, size=1: \ - round(n/(decimal.Decimal(size*10**int(math.log10(max(n, 1))))))*size*10**int(math.log10(max(n, 1))), - 'log10': math.log10, - 'Decimal': decimal.Decimal, - } + safe_locals = self.get_expression_context(instance) try: return eval(self.metric, safe_locals) except Exception as error: raise type(error)("%s on '%s'" %(error, self.service)) def get_order_description(self, instance): - safe_locals = { - 'instance': instance, - 'obj': instance, - 'ugettext': ugettext, - instance._meta.model_name: instance, - } + safe_locals = self.get_expression_context(instance) account = getattr(instance, 'account', instance) with translation.override(account.language): if not self.order_description: diff --git a/orchestra/contrib/services/models.py b/orchestra/contrib/services/models.py index aaa9a0c5..9b832a01 100644 --- a/orchestra/contrib/services/models.py +++ b/orchestra/contrib/services/models.py @@ -91,7 +91,7 @@ class Service(models.Model): help_text=_( "Python expression " "used for generating the description for the bill lines of this services.(.*)
', response.content) - raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code) - - def get_id(self, session, webapp): - search = self.get_base_url() - search += '/wp-admin/network/sites.php?s=%s&action=blogs' % webapp.name - regex = re.compile( - '%s' % webapp.name - ) - content = session.get(search).content - # Get id - ids = regex.search(content) - if not ids: - raise RuntimeError("Blog '%s' not found" % webapp.name) - ids = ids.groups() - if len(ids) > 1: - raise ValueError("Multiple matches") - # Get wpnonce - wpnonce = re.search(r'(.*)', content).groups()[0] - wpnonce = re.search(r'_wpnonce=([^"]*)"', wpnonce).groups()[0] - return int(ids[0]), wpnonce - - def create_blog(self, webapp, server): - session = requests.Session() - self.login(session) - - # Check if blog already exists - try: - self.get_id(session, webapp) - except RuntimeError: - url = self.get_base_url() - url += '/wp-admin/network/site-new.php' - content = session.get(url).content - - wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"') - wpnonce = wpnonce.search(content).groups()[0] - - url += '?action=add-site' - data = { - 'blog[domain]': webapp.name, - 'blog[title]': webapp.name, - 'blog[email]': webapp.account.email, - '_wpnonce_add-blog': wpnonce, - } - - # Validate response - response = session.post(url, data=data) - self.validate_response(response) - - def delete_blog(self, webapp, server): - session = requests.Session() - self.login(session) - - try: - id, wpnonce = self.get_id(session, webapp) - except RuntimeError: - pass - else: - delete = self.get_base_url() - delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog' - delete += '&id=%d&_wpnonce=%s' % (id, wpnonce) - - content = session.get(delete).content - wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"') - wpnonce = wpnonce.search(content).groups()[0] - data = { - 'action': 'deleteblog', - 'id': id, - '_wpnonce': wpnonce, - '_wp_http_referer': '/wp-admin/network/sites.php', - } - delete = self.get_base_url() - delete += '/wp-admin/network/sites.php?action=deleteblog' - response = session.post(delete, data=data) - self.validate_response(response) - - def save(self, webapp): - if webapp.type != 'wordpress-mu': - return - self.append(self.create_blog, webapp) - - def delete(self, webapp): - if webapp.type != 'wordpress-mu': - return - self.append(self.delete_blog, webapp) diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index 1249c5aa..6d78e504 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -57,17 +57,19 @@ class WebApp(models.Model): self.data = apptype.clean_data() @cached - def get_options(self, merge=False): - if merge: - options = OrderedDict() - qs = WebAppOption.objects.filter(webapp__account=self.account, webapp__type=self.type) - for name, value in qs.values_list('name', 'value').order_by('name'): - if name in options: - options[name] = max(options[name], value) - else: - options[name] = value - return options - return OrderedDict(self.options.values_list('name', 'value').order_by('name')) + def get_options(self, **kwargs): + options = OrderedDict() + if not kwargs: + kwargs = { + 'webapp_id': self.pk, + } + qs = WebAppOption.objects.filter(**kwargs) + for name, value in qs.values_list('name', 'value').order_by('name'): + if name in options: + options[name] = max(options[name], value) + else: + options[name] = value + return options def get_directive(self): return self.type_instance.get_directive() diff --git a/orchestra/management/commands/staticcheck.py b/orchestra/management/commands/staticcheck.py index 208e3c5b..e471a215 100644 --- a/orchestra/management/commands/staticcheck.py +++ b/orchestra/management/commands/staticcheck.py @@ -9,4 +9,4 @@ class Command(BaseCommand): def handle(self, *filenames, **options): flake = run('flake8 {%s,%s} | grep -v "W293\|E501"' % (get_orchestra_dir(), get_site_dir())) - print(flake.stdout) + self.stdout.write(flake.stdout) diff --git a/orchestra/utils/html.py b/orchestra/utils/html.py index 356397c0..57aca198 100644 --- a/orchestra/utils/html.py +++ b/orchestra/utils/html.py @@ -7,5 +7,5 @@ def html_to_pdf(html): 'PATH=$PATH:/usr/local/bin/\n' 'xvfb-run -a -s "-screen 0 640x4800x16" ' 'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -', - stdin=html.encode('utf-8'), force_unicode=False + stdin=html.encode('utf-8') ).stdout diff --git a/orchestra/utils/sys.py b/orchestra/utils/sys.py index 430862b1..4277cc46 100644 --- a/orchestra/utils/sys.py +++ b/orchestra/utils/sys.py @@ -45,15 +45,15 @@ def read_async(fd): return '' -def runiterator(command, display=False, error_codes=[0], silent=False, stdin='', force_unicode=True): +def runiterator(command, display=False, error_codes=[0], silent=False, stdin=''): """ Subprocess wrapper for running commands concurrently """ if display: sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command) p = subprocess.Popen(command, shell=True, executable='/bin/bash', - stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) - p.stdin.write(bytes(stdin, 'utf-8')) + p.stdin.write(stdin) p.stdin.close() yield @@ -62,16 +62,18 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='', # Async reading of stdout and sterr while True: - stdout = '' - stderr = '' + stdout = b'' + stderr = b'' # Get complete unicode chunks select.select([p.stdout, p.stderr], [], []) stdoutPiece = read_async(p.stdout) stderrPiece = read_async(p.stderr) - stdout += (stdoutPiece or b'').decode('utf8', errors='replace') - stderr += (stderrPiece or b'').decode('utf8', errors='replace') + stdout += (stdoutPiece or b'') + #.decode('ascii'), errors='replace') + stderr += (stderrPiece or b'') + #.decode('ascii'), errors='replace') if display and stdout: sys.stdout.write(stdout) @@ -89,14 +91,14 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='', raise StopIteration -def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False, force_unicode=True): - iterator = runiterator(command, display, error_codes, silent, stdin, force_unicode) +def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False): + iterator = runiterator(command, display, error_codes, silent, stdin) next(iterator) if async: return iterator - stdout = '' - stderr = '' + stdout = b'' + stderr = b'' for state in iterator: stdout += state.stdout stderr += state.stderr