Random fixes
This commit is contained in:
parent
5d95e64965
commit
9ac9f6b933
8
TODO.md
8
TODO.md
|
@ -276,8 +276,14 @@ https://code.djangoproject.com/ticket/24576
|
||||||
* force ignore slack billing period overridig when billing
|
* force ignore slack billing period overridig when billing
|
||||||
* fpm reload starts new pools?
|
* fpm reload starts new pools?
|
||||||
* rename resource.monitors to resource.backends ?
|
* 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
|
# Ignore superusers & co on billing: list filter doesn't work nor ignore detection
|
||||||
# bill.totals make it 100% computed?
|
# 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 -
|
* 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
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 103 KiB |
|
@ -107,10 +107,15 @@ def admin_link(*args, **kwargs):
|
||||||
if not getattr(obj, 'pk', None):
|
if not getattr(obj, 'pk', None):
|
||||||
return '---'
|
return '---'
|
||||||
url = change_url(obj)
|
url = change_url(obj)
|
||||||
|
display = kwargs.get('display')
|
||||||
|
if display:
|
||||||
|
display = getattr(obj, display, 'merda')
|
||||||
|
else:
|
||||||
|
display = obj
|
||||||
extra = ''
|
extra = ''
|
||||||
if kwargs['popup']:
|
if kwargs['popup']:
|
||||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||||
return '<a href="%s" %s>%s</a>' % (url, extra, obj)
|
return '<a href="%s" %s>%s</a>' % (url, extra, display)
|
||||||
|
|
||||||
|
|
||||||
@admin_field
|
@admin_field
|
||||||
|
|
|
@ -35,6 +35,7 @@ MEDIA_URL = '/media/'
|
||||||
|
|
||||||
ALLOWED_HOSTS = '*'
|
ALLOWED_HOSTS = '*'
|
||||||
|
|
||||||
|
|
||||||
# Set this to True to wrap each HTTP request in a transaction on this database.
|
# 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 do not wrap middlewares (orchestra.contrib.orchestration.middlewares.OperationsMiddleware)
|
||||||
ATOMIC_REQUESTS = False
|
ATOMIC_REQUESTS = False
|
||||||
|
@ -101,6 +102,7 @@ INSTALLED_APPS = (
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
'passlib.ext.django',
|
'passlib.ext.django',
|
||||||
|
'django_countries',
|
||||||
|
|
||||||
# Django.contrib
|
# Django.contrib
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
|
|
@ -40,6 +40,7 @@ DATABASES = {
|
||||||
'PASSWORD': 'orchestra', # Not used with sqlite3.
|
'PASSWORD': 'orchestra', # Not used with sqlite3.
|
||||||
'HOST': 'localhost', # Set to empty string for localhost. 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.
|
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||||
|
'CONN_MAX_AGE': 300,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
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.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
||||||
|
|
||||||
|
@ -29,8 +29,13 @@ PAYMENT_STATE_COLORS = {
|
||||||
|
|
||||||
class BillLineInline(admin.TabularInline):
|
class BillLineInline(admin.TabularInline):
|
||||||
model = BillLine
|
model = BillLine
|
||||||
fields = ('description', 'rate', 'quantity', 'tax', 'subtotal', 'display_total')
|
fields = (
|
||||||
readonly_fields = ('display_total',)
|
'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):
|
def display_total(self, line):
|
||||||
total = line.get_total()
|
total = line.get_total()
|
||||||
|
@ -46,9 +51,9 @@ class BillLineInline(admin.TabularInline):
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'description':
|
if db_field.name == 'description':
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'110'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'50'})
|
||||||
else:
|
elif db_field.name not in ('start_on', 'end_on'):
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'13'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
|
||||||
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
@ -61,7 +66,8 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
# https://code.djangoproject.com/ticket/9025
|
# https://code.djangoproject.com/ticket/9025
|
||||||
|
|
||||||
fields = (
|
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
|
readonly_fields = fields
|
||||||
|
|
||||||
|
@ -157,10 +163,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
num_lines.short_description = _("lines")
|
num_lines.short_description = _("lines")
|
||||||
|
|
||||||
def display_total(self, bill):
|
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.allow_tags = True
|
||||||
display_total.short_description = _("total")
|
display_total.short_description = _("total")
|
||||||
display_total.admin_order_field = 'totals'
|
display_total.admin_order_field = 'computed_total'
|
||||||
|
|
||||||
def type_link(self, bill):
|
def type_link(self, bill):
|
||||||
bill_type = bill.type.lower()
|
bill_type = bill.type.lower()
|
||||||
|
@ -235,7 +241,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
qs = super(BillAdmin, self).get_queryset(request)
|
qs = super(BillAdmin, self).get_queryset(request)
|
||||||
qs = qs.annotate(
|
qs = qs.annotate(
|
||||||
models.Count('lines'),
|
models.Count('lines'),
|
||||||
totals=Sum(
|
computed_total=Sum(
|
||||||
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
|
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -18,112 +18,112 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: actions.py:35
|
#: actions.py:36
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr "Descarrega"
|
msgstr "Descarrega"
|
||||||
|
|
||||||
#: actions.py:45
|
#: actions.py:46
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr "Vista"
|
msgstr "Vista"
|
||||||
|
|
||||||
#: actions.py:53
|
#: actions.py:54
|
||||||
msgid "Selected bills should be in open state"
|
msgid "Selected bills should be in open state"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:71
|
#: actions.py:72
|
||||||
msgid "Selected bills have been closed"
|
msgid "Selected bills have been closed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:80
|
|
||||||
#, python-format
|
|
||||||
msgid "<a href=\"%s\">One related transaction</a> has been created"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: actions.py:81
|
#: actions.py:81
|
||||||
#, python-format
|
#, python-format
|
||||||
|
msgid "<a href=\"%s\">One related transaction</a> has been created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:82
|
||||||
|
#, python-format
|
||||||
msgid "<a href=\"%s\">%i related transactions</a> have been created"
|
msgid "<a href=\"%s\">%i related transactions</a> have been created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:87
|
#: actions.py:88
|
||||||
msgid "Are you sure about closing the following bills?"
|
msgid "Are you sure about closing the following bills?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:88
|
#: actions.py:89
|
||||||
msgid ""
|
msgid ""
|
||||||
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
"Once a bill is closed it can not be further modified.</p><p>Please select a "
|
||||||
"payment source for the selected bills"
|
"payment source for the selected bills"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:101
|
#: actions.py:102
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:112
|
#: actions.py:113
|
||||||
msgid "Resend"
|
msgid "Resend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:129 models.py:309
|
#: actions.py:130 models.py:312
|
||||||
msgid "Not enough information stored for undoing"
|
msgid "Not enough information stored for undoing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:132 models.py:311
|
#: actions.py:133 models.py:314
|
||||||
msgid "Dates don't match"
|
msgid "Dates don't match"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:147
|
#: actions.py:148
|
||||||
msgid "Can not move lines which are not in open state."
|
msgid "Can not move lines which are not in open state."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:152
|
#: actions.py:153
|
||||||
msgid "Can not move lines from different accounts"
|
msgid "Can not move lines from different accounts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:160
|
#: actions.py:161
|
||||||
msgid "Target account different than lines account."
|
msgid "Target account different than lines account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:167
|
#: actions.py:168
|
||||||
msgid "Lines moved"
|
msgid "Lines moved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:41 forms.py:12
|
#: admin.py:43 admin.py:86 forms.py:11
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:69
|
#: admin.py:73
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Descripció"
|
msgstr "Descripció"
|
||||||
|
|
||||||
#: admin.py:77
|
#: admin.py:81
|
||||||
msgid "Subtotal"
|
msgid "Subtotal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:104
|
#: admin.py:113
|
||||||
msgid "Manage bill lines of multiple bills."
|
msgid "Manage bill lines of multiple bills."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:109
|
#: admin.py:118
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Manage %s bill lines."
|
msgid "Manage %s bill lines."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:129
|
#: admin.py:138
|
||||||
msgid "Raw"
|
msgid "Raw"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:147
|
#: admin.py:157
|
||||||
msgid "lines"
|
msgid "lines"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:152 templates/bills/microspective.html:107
|
#: admin.py:162 templates/bills/microspective.html:107
|
||||||
msgid "total"
|
msgid "total"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: admin.py:160 models.py:85 models.py:340
|
#: admin.py:170 models.py:87 models.py:342
|
||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr "tipus"
|
msgstr "tipus"
|
||||||
|
|
||||||
#: admin.py:177
|
#: admin.py:187
|
||||||
msgid "Payment"
|
msgid "Payment"
|
||||||
msgstr "Pagament"
|
msgstr "Pagament"
|
||||||
|
|
||||||
|
@ -131,15 +131,15 @@ msgstr "Pagament"
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Tot"
|
msgstr "Tot"
|
||||||
|
|
||||||
#: filters.py:18 models.py:75
|
#: filters.py:18 models.py:77
|
||||||
msgid "Invoice"
|
msgid "Invoice"
|
||||||
msgstr "Factura"
|
msgstr "Factura"
|
||||||
|
|
||||||
#: filters.py:19 models.py:76
|
#: filters.py:19 models.py:78
|
||||||
msgid "Amendment invoice"
|
msgid "Amendment invoice"
|
||||||
msgstr "Factura rectificativa"
|
msgstr "Factura rectificativa"
|
||||||
|
|
||||||
#: filters.py:20 models.py:77
|
#: filters.py:20 models.py:79
|
||||||
msgid "Fee"
|
msgid "Fee"
|
||||||
msgstr "Quota de soci"
|
msgstr "Quota de soci"
|
||||||
|
|
||||||
|
@ -167,15 +167,15 @@ msgstr "No"
|
||||||
msgid "Number"
|
msgid "Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:11
|
#: forms.py:10
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:13
|
#: forms.py:12
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:15
|
#: forms.py:13
|
||||||
msgid "Source"
|
msgid "Source"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -193,158 +193,178 @@ msgstr ""
|
||||||
msgid "Main"
|
msgid "Main"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:20 models.py:83
|
#: models.py:22 models.py:85
|
||||||
msgid "account"
|
msgid "account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:22
|
#: models.py:24
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:23
|
#: models.py:25
|
||||||
msgid "Account full name will be used when left blank."
|
msgid "Account full name will be used when left blank."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:24
|
#: models.py:26
|
||||||
msgid "address"
|
msgid "address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:25
|
#: models.py:27
|
||||||
msgid "city"
|
msgid "city"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:27
|
#: models.py:29
|
||||||
msgid "zip code"
|
msgid "zip code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:28
|
#: models.py:30
|
||||||
msgid "Enter a valid zipcode."
|
msgid "Enter a valid zipcode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:29
|
#: models.py:31
|
||||||
msgid "country"
|
msgid "country"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:32
|
#: models.py:34
|
||||||
msgid "VAT number"
|
msgid "VAT number"
|
||||||
msgstr "NIF"
|
msgstr "NIF"
|
||||||
|
|
||||||
#: models.py:64
|
#: models.py:66
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:65
|
#: models.py:67
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:66
|
#: models.py:68
|
||||||
msgid "Bad debt"
|
msgid "Bad debt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:78
|
#: models.py:80
|
||||||
msgid "Amendment Fee"
|
msgid "Amendment Fee"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:79
|
#: models.py:81
|
||||||
msgid "Pro forma"
|
msgid "Pro forma"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:82
|
#: models.py:84
|
||||||
msgid "number"
|
msgid "number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:86
|
#: models.py:88
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:87
|
#: models.py:89
|
||||||
msgid "closed on"
|
msgid "closed on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:88
|
#: models.py:90
|
||||||
msgid "open"
|
msgid "open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:89
|
#: models.py:91
|
||||||
msgid "sent"
|
msgid "sent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:90
|
#: models.py:92
|
||||||
msgid "due on"
|
msgid "due on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:91
|
#: models.py:93
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:93
|
#: models.py:96
|
||||||
msgid "comments"
|
msgid "comments"
|
||||||
msgstr ""
|
msgstr "comentaris"
|
||||||
|
|
||||||
#: models.py:94
|
#: models.py:97
|
||||||
msgid "HTML"
|
msgid "HTML"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:271
|
#: models.py:273
|
||||||
msgid "bill"
|
msgid "bill"
|
||||||
msgstr ""
|
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"
|
msgid "description"
|
||||||
msgstr "descripció"
|
msgstr "descripció"
|
||||||
|
|
||||||
#: models.py:273
|
#: models.py:275
|
||||||
msgid "rate"
|
msgid "rate"
|
||||||
msgstr "tarifa"
|
msgstr "tarifa"
|
||||||
|
|
||||||
#: models.py:274
|
#: models.py:276
|
||||||
msgid "quantity"
|
msgid "quantity"
|
||||||
msgstr "quantitat"
|
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"
|
msgid "subtotal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:276
|
#: models.py:279
|
||||||
msgid "tax"
|
msgid "tax"
|
||||||
msgstr "impostos"
|
msgstr "impostos"
|
||||||
|
|
||||||
#: models.py:282
|
#: models.py:284
|
||||||
msgid "Informative link back to the order"
|
msgid "Informative link back to the order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:283
|
#: models.py:285
|
||||||
msgid "order billed"
|
msgid "order billed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:284
|
#: models.py:286
|
||||||
msgid "order billed until"
|
msgid "order billed until"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:285
|
#: models.py:287
|
||||||
msgid "created"
|
msgid "created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:287
|
#: models.py:289
|
||||||
msgid "amended line"
|
msgid "amended line"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:330
|
#: models.py:332
|
||||||
msgid "Volume"
|
msgid "Volume"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:331
|
#: models.py:333
|
||||||
msgid "Compensation"
|
msgid "Compensation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:332
|
#: models.py:334
|
||||||
msgid "Other"
|
msgid "Other"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:336
|
#: models.py:338
|
||||||
msgid "bill line"
|
msgid "bill line"
|
||||||
msgstr ""
|
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
|
#: templates/bills/microspective.html:74
|
||||||
msgid "hrs/qty"
|
msgid "hrs/qty"
|
||||||
msgstr "hrs/quant"
|
msgstr "hrs/quant"
|
||||||
|
@ -360,7 +380,7 @@ msgstr "IVA"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:103
|
#: templates/bills/microspective.html:103
|
||||||
msgid "taxes"
|
msgid "taxes"
|
||||||
msgstr ""
|
msgstr "impostos"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:119
|
#: templates/bills/microspective.html:119
|
||||||
msgid "COMMENTS"
|
msgid "COMMENTS"
|
||||||
|
@ -371,15 +391,17 @@ msgid "PAYMENT"
|
||||||
msgstr "PAGAMENT"
|
msgstr "PAGAMENT"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:129
|
#: templates/bills/microspective.html:129
|
||||||
#, python-format
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" You can pay our %(type)s by bank transfer. <br>\n"
|
" You can pay our %(type)s by bank transfer. <br>\n"
|
||||||
" Please make sure to state your name and the "
|
" Please make sure to state your name and the %(type)s "
|
||||||
"%(bill.get_type_display.lower)s number.\n"
|
"number.\n"
|
||||||
" Our bank account number is <br>\n"
|
" Our bank account number is <br>\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"Pots pagar aquesta %(type)s per transferencia banacaria.<br>Inclou el teu "
|
||||||
|
"nom i el numero de %(type)s. El nostre compte bancari és"
|
||||||
|
|
||||||
#: templates/bills/microspective.html:138
|
#: templates/bills/microspective.html:138
|
||||||
msgid "QUESTIONS"
|
msgid "QUESTIONS"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.core.validators import ValidationError, RegexValidator
|
from django.core.validators import ValidationError, RegexValidator
|
||||||
|
@ -277,9 +278,8 @@ class BillLine(models.Model):
|
||||||
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
|
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
|
||||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||||
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
|
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
|
||||||
# Undo
|
start_on = models.DateField(_("start"))
|
||||||
# initial = models.DateTimeField(null=True)
|
end_on = models.DateField(_("end"), null=True)
|
||||||
# end = models.DateTimeField(null=True)
|
|
||||||
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=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)
|
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
|
||||||
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
||||||
|
@ -305,6 +305,15 @@ class BillLine(models.Model):
|
||||||
def get_verbose_quantity(self):
|
def get_verbose_quantity(self):
|
||||||
return self.verbose_quantity or self.quantity
|
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):
|
def undo(self):
|
||||||
# TODO warn user that undoing bills with compensations lead to compensation lost
|
# TODO warn user that undoing bills with compensations lead to compensation lost
|
||||||
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
||||||
|
|
|
@ -108,7 +108,7 @@ hr {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="date" class="column-2">
|
<div id="date" class="column-2">
|
||||||
From {{ bill.lines.get.description }}
|
From {{ bill.lines.get.get_verbose_period }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -175,10 +175,14 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
#lines .column-description {
|
#lines .column-description {
|
||||||
width: 65%;
|
width: 45%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#lines .column-period {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
#lines .column-quantity {
|
#lines .column-quantity {
|
||||||
width: 10%;
|
width: 10%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="contact">
|
<div class="contact">
|
||||||
<p>{{ seller.address }}<br>
|
<p>{{ seller.address }}<br>
|
||||||
{{ seller.zipcode }} - {{ seller.city }}<br>
|
{{ seller.zipcode }} - {% trans seller.city %}<br>
|
||||||
{{ seller.country }}<br>
|
{% trans seller.get_country_display %}<br>
|
||||||
</p>
|
</p>
|
||||||
<p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br>
|
<p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br>
|
||||||
<a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br>
|
<a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br>
|
||||||
|
@ -40,21 +40,21 @@
|
||||||
|
|
||||||
{% block summary %}
|
{% block summary %}
|
||||||
<div id="bill-number">
|
<div id="bill-number">
|
||||||
{{ bill.get_type_display.capitalize }}<br>
|
{% trans bill.get_type_display.capitalize %}<br>
|
||||||
<span class="value">{{ bill.number }}</span><br>
|
<span class="value">{{ bill.number }}</span><br>
|
||||||
</div>
|
</div>
|
||||||
<div id="bill-summary">
|
<div id="bill-summary">
|
||||||
<hr>
|
<hr>
|
||||||
<div id="due-date">
|
<div id="due-date">
|
||||||
<span class="title">DUE DATE</span><br>
|
<span class="title">{% trans "DUE DATE" %}</span><br>
|
||||||
<psan class="value">{{ bill.due_on | default:default_due_date | date }}</span>
|
<psan class="value">{{ bill.due_on | default:default_due_date | date }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="total">
|
<div id="total">
|
||||||
<span class="title">TOTAL</span><br>
|
<span class="title">{% trans "TOTAL" %}</span><br>
|
||||||
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span>
|
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="bill-date">
|
<div id="bill-date">
|
||||||
<span class="title">{{ bill.get_type_display.upper }} DATE</span><br>
|
<span class="title">{% blocktrans with bill_type=bill.get_type_display %}{{ bill_type|upper }} DATE {% endblocktrans %}</span><br>
|
||||||
<psan class="value">{{ bill.closed_on | default:now | date }}</span>
|
<psan class="value">{{ bill.closed_on | default:now | date }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,8 +62,8 @@
|
||||||
<span class="name">{{ buyer.get_name }}</span><br>
|
<span class="name">{{ buyer.get_name }}</span><br>
|
||||||
{{ buyer.vat }}<br>
|
{{ buyer.vat }}<br>
|
||||||
{{ buyer.address }}<br>
|
{{ buyer.address }}<br>
|
||||||
{{ buyer.zipcode }} - {{ buyer.city }}<br>
|
{{ buyer.zipcode }} - {% trans buyer.city %}<br>
|
||||||
{{ buyer.country }}<br>
|
{% trans buyer.get_country_display %}<br>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@
|
||||||
<div id="lines">
|
<div id="lines">
|
||||||
<span class="title column-id">id</span>
|
<span class="title column-id">id</span>
|
||||||
<span class="title column-description">{% trans "description" %}</span>
|
<span class="title column-description">{% trans "description" %}</span>
|
||||||
|
<span class="title column-period">{% trans "period" %}</span>
|
||||||
<span class="title column-quantity">{% trans "hrs/qty" %}</span>
|
<span class="title column-quantity">{% trans "hrs/qty" %}</span>
|
||||||
<span class="title column-rate">{% trans "rate/price" %}</span>
|
<span class="title column-rate">{% trans "rate/price" %}</span>
|
||||||
<span class="title column-subtotal">{% trans "subtotal" %}</span>
|
<span class="title column-subtotal">{% trans "subtotal" %}</span>
|
||||||
|
@ -79,6 +80,7 @@
|
||||||
{% with sublines=line.sublines.all %}
|
{% with sublines=line.sublines.all %}
|
||||||
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
||||||
|
<span class="{% if not sublines %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||||
|
@ -86,6 +88,7 @@
|
||||||
{% for subline in sublines %}
|
{% for subline in sublines %}
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span>
|
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span>
|
||||||
|
<span class="{% if forloop.last %}last {% endif %}subline column-period"> </span>
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-quantity"> </span>
|
<span class="{% if forloop.last %}last {% endif %}subline column-quantity"> </span>
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-rate"> </span>
|
<span class="{% if forloop.last %}last {% endif %}subline column-rate"> </span>
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
|
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
|
||||||
|
@ -97,7 +100,7 @@
|
||||||
<div id="totals">
|
<div id="totals">
|
||||||
<br> <br>
|
<br> <br>
|
||||||
{% for tax, subtotal in bill.get_subtotals.items %}
|
{% for tax, subtotal in bill.get_subtotals.items %}
|
||||||
<span class="subtotal column-title">subtotal {{ tax }}% {% trans "VAT" %}</span>
|
<span class="subtotal column-title">{% trans "subtotal" %} {{ tax }}% {% trans "VAT" %}</span>
|
||||||
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
|
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
|
||||||
<br>
|
<br>
|
||||||
<span class="tax column-title">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</span>
|
<span class="tax column-title">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</span>
|
||||||
|
@ -126,9 +129,9 @@
|
||||||
{% if payment.message %}
|
{% if payment.message %}
|
||||||
{{ payment.message | safe }}
|
{{ payment.message | safe }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% blocktrans with type=bill.get_type_display.lower %}
|
{% blocktrans with type=bill.get_type_display %}
|
||||||
You can pay our {{ type }} by bank transfer. <br>
|
You can pay our {{ type }} by bank transfer. <br>
|
||||||
Please make sure to state your name and the {{ bill.get_type_display.lower}} number.
|
Please make sure to state your name and the {{ type }} number.
|
||||||
Our bank account number is <br>
|
Our bank account number is <br>
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
<strong>{{ seller_info.bank_account }}</strong>
|
<strong>{{ seller_info.bank_account }}</strong>
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.contrib.orchestration import ServiceController, replace
|
from orchestra.contrib.orchestration import ServiceController, replace
|
||||||
from orchestra.contrib.orchestration import Operation
|
from orchestra.contrib.orchestration import Operation
|
||||||
|
from orchestra.utils.python import OrderedSet
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
|
||||||
|
@ -92,9 +93,9 @@ class Bind9MasterDomainBackend(ServiceController):
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
def get_slaves(self, domain):
|
def get_slaves(self, domain):
|
||||||
return set(settings.DOMAINS_SLAVES).union(
|
ips = list(settings.DOMAINS_SLAVES)
|
||||||
set(self.get_servers(domain, Bind9SlaveDomainBackend))
|
ips += self.get_servers(domain, Bind9SlaveDomainBackend)
|
||||||
)
|
return OrderedSet(ips)
|
||||||
|
|
||||||
def get_context(self, domain):
|
def get_context(self, domain):
|
||||||
slaves = self.get_slaves(domain)
|
slaves = self.get_slaves(domain)
|
||||||
|
@ -139,9 +140,9 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
|
||||||
self.append('if [[ $UPDATED == 1 ]]; then { sleep 1 && service bind9 reload; } & fi')
|
self.append('if [[ $UPDATED == 1 ]]; then { sleep 1 && service bind9 reload; } & fi')
|
||||||
|
|
||||||
def get_masters(self, domain):
|
def get_masters(self, domain):
|
||||||
return set(settings.DOMAINS_MASTERS).union(
|
ips = list(settings.DOMAINS_MASTERS)
|
||||||
set(self.get_servers(domain, Bind9MasterDomainBackend))
|
ips += self.get_servers(domain, Bind9MasterDomainBackend)
|
||||||
)
|
return OrderedSet(ips)
|
||||||
|
|
||||||
def get_context(self, domain):
|
def get_context(self, domain):
|
||||||
context = {
|
context = {
|
||||||
|
|
|
@ -164,16 +164,24 @@ class Domain(models.Model):
|
||||||
value=' '.join(soa)
|
value=' '.join(soa)
|
||||||
))
|
))
|
||||||
is_host = self.is_top or not types or Record.A in types or Record.AAAA in types
|
is_host = self.is_top or not types or Record.A in types or Record.AAAA in types
|
||||||
if Record.MX not in types and is_host:
|
if is_host:
|
||||||
|
if Record.MX not in types:
|
||||||
for mx in settings.DOMAINS_DEFAULT_MX:
|
for mx in settings.DOMAINS_DEFAULT_MX:
|
||||||
records.append(AttrDict(
|
records.append(AttrDict(
|
||||||
type=Record.MX,
|
type=Record.MX,
|
||||||
value=mx
|
value=mx
|
||||||
))
|
))
|
||||||
if (Record.A not in types and Record.AAAA not in types) and is_host:
|
default_a = settings.DOMAINS_DEFAULT_A
|
||||||
|
if default_a and Record.A not in types:
|
||||||
records.append(AttrDict(
|
records.append(AttrDict(
|
||||||
type=Record.A,
|
type=Record.A,
|
||||||
value=settings.DOMAINS_DEFAULT_A
|
value=default_a
|
||||||
|
))
|
||||||
|
default_aaaa = settings.DOMAINS_DEFAULT_AAAA
|
||||||
|
if default_aaaa and Record.AAAA not in types:
|
||||||
|
records.append(AttrDict(
|
||||||
|
type=Record.AAAA,
|
||||||
|
value=default_aaaa
|
||||||
))
|
))
|
||||||
result = ''
|
result = ''
|
||||||
for record in records:
|
for record in records:
|
||||||
|
|
|
@ -69,6 +69,11 @@ DOMAINS_DEFAULT_A = getattr(settings, 'DOMAINS_DEFAULT_A',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DOMAINS_DEFAULT_AAAA = getattr(settings, 'DOMAINS_DEFAULT_AAAA',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
DOMAINS_DEFAULT_MX = getattr(settings, 'DOMAINS_DEFAULT_MX', (
|
DOMAINS_DEFAULT_MX = getattr(settings, 'DOMAINS_DEFAULT_MX', (
|
||||||
'10 mail.{}.'.format(ORCHESTRA_BASE_DOMAIN),
|
'10 mail.{}.'.format(ORCHESTRA_BASE_DOMAIN),
|
||||||
'10 mail2.{}.'.format(ORCHESTRA_BASE_DOMAIN),
|
'10 mail2.{}.'.format(ORCHESTRA_BASE_DOMAIN),
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.db.models.loading import get_model
|
||||||
from orchestra.contrib.orchestration import manager, Operation
|
from orchestra.contrib.orchestration import manager, Operation
|
||||||
from orchestra.contrib.orchestration.models import Server
|
from orchestra.contrib.orchestration.models import Server
|
||||||
from orchestra.contrib.orchestration.backends import ServiceBackend
|
from orchestra.contrib.orchestration.backends import ServiceBackend
|
||||||
from orchestra.utils.python import import_class
|
from orchestra.utils.python import import_class, OrderedSet
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -34,7 +34,7 @@ class Command(BaseCommand):
|
||||||
list_backends = options.get('list_backends')
|
list_backends = options.get('list_backends')
|
||||||
if list_backends:
|
if list_backends:
|
||||||
for backend in ServiceBackend.get_backends():
|
for backend in ServiceBackend.get_backends():
|
||||||
print(str(backend).split("'")[1])
|
self.stdout.write(str(backend).split("'")[1])
|
||||||
return
|
return
|
||||||
model = get_model(*options['model'].split('.'))
|
model = get_model(*options['model'].split('.'))
|
||||||
action = options.get('action')
|
action = options.get('action')
|
||||||
|
@ -49,9 +49,9 @@ class Command(BaseCommand):
|
||||||
comps = iter(comp.split('='))
|
comps = iter(comp.split('='))
|
||||||
for arg in comps:
|
for arg in comps:
|
||||||
kwargs[arg] = next(comps).strip().rstrip(',')
|
kwargs[arg] = next(comps).strip().rstrip(',')
|
||||||
operations = []
|
operations = OrderedSet()
|
||||||
operations = set()
|
|
||||||
route_cache = {}
|
route_cache = {}
|
||||||
|
queryset = model.objects.filter(**kwargs).order_by('id')
|
||||||
if servers:
|
if servers:
|
||||||
server_objects = []
|
server_objects = []
|
||||||
# Get and create missing Servers
|
# Get and create missing Servers
|
||||||
|
@ -62,12 +62,12 @@ class Command(BaseCommand):
|
||||||
server = Server.objects.create(name=server, address=server)
|
server = Server.objects.create(name=server, address=server)
|
||||||
server_objects.append(server)
|
server_objects.append(server)
|
||||||
# Generate operations for the given backend
|
# Generate operations for the given backend
|
||||||
for instance in model.objects.filter(**kwargs):
|
for instance in queryset:
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
backend = import_class(backend)
|
backend = import_class(backend)
|
||||||
operations.add(Operation(backend, instance, action, servers=server_objects))
|
operations.add(Operation(backend, instance, action, servers=server_objects))
|
||||||
else:
|
else:
|
||||||
for instance in model.objects.filter(**kwargs):
|
for instance in queryset:
|
||||||
manager.collect(instance, action, operations=operations, route_cache=route_cache)
|
manager.collect(instance, action, operations=operations, route_cache=route_cache)
|
||||||
scripts, block = manager.generate(operations)
|
scripts, block = manager.generate(operations)
|
||||||
servers = []
|
servers = []
|
||||||
|
@ -76,11 +76,10 @@ class Command(BaseCommand):
|
||||||
server, __ = key
|
server, __ = key
|
||||||
backend, operations = value
|
backend, operations = value
|
||||||
servers.append(server.name)
|
servers.append(server.name)
|
||||||
sys.stdout.write('# Execute on %s\n' % server.name)
|
self.stdout.write('# Execute on %s' % server.name)
|
||||||
for method, commands in backend.scripts:
|
for method, commands in backend.scripts:
|
||||||
script = '\n'.join(commands) + '\n'
|
script = '\n'.join(commands)
|
||||||
script = script.encode('ascii', errors='replace')
|
self.stdout.write(script)
|
||||||
sys.stdout.write(script.decode('ascii'))
|
|
||||||
if interactive:
|
if interactive:
|
||||||
context = {
|
context = {
|
||||||
'servers': ', '.join(servers),
|
'servers': ', '.join(servers),
|
||||||
|
@ -97,7 +96,7 @@ class Command(BaseCommand):
|
||||||
if not dry:
|
if not dry:
|
||||||
logs = manager.execute(scripts, block=block)
|
logs = manager.execute(scripts, block=block)
|
||||||
for log in logs:
|
for log in logs:
|
||||||
print(log.stdout.encode('utf8', errors='replace'))
|
self.stdout.write(log.stdout)
|
||||||
sys.stderr.write(log.stderr.encode('utf8', errors='replace'))
|
self.stderr.write(log.stderr)
|
||||||
for log in logs:
|
for log in logs:
|
||||||
print(log.backend, log.state)
|
self.stdout.write(' '.join((log.backend, log.state)))
|
||||||
|
|
|
@ -6,7 +6,7 @@ from collections import OrderedDict
|
||||||
from django import db
|
from django import db
|
||||||
from django.core.mail import mail_admins
|
from django.core.mail import mail_admins
|
||||||
|
|
||||||
from orchestra.utils.python import import_class
|
from orchestra.utils.python import import_class, OrderedSet
|
||||||
|
|
||||||
from . import settings, Operation
|
from . import settings, Operation
|
||||||
from .backends import ServiceBackend
|
from .backends import ServiceBackend
|
||||||
|
@ -138,7 +138,7 @@ def execute(scripts, block=False, async=False):
|
||||||
|
|
||||||
def collect(instance, action, **kwargs):
|
def collect(instance, action, **kwargs):
|
||||||
""" collect operations """
|
""" collect operations """
|
||||||
operations = kwargs.get('operations', set())
|
operations = kwargs.get('operations', OrderedSet())
|
||||||
route_cache = kwargs.get('route_cache', {})
|
route_cache = kwargs.get('route_cache', {})
|
||||||
for backend_cls in ServiceBackend.get_backends():
|
for backend_cls in ServiceBackend.get_backends():
|
||||||
# Check if there exists a related instance to be executed for this backend and action
|
# Check if there exists a related instance to be executed for this backend and action
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import timedelta
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -28,3 +29,8 @@ ORCHESTRATION_TEMP_SCRIPT_PATH = getattr(settings, 'ORCHESTRATION_TEMP_SCRIPT_PA
|
||||||
ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION',
|
ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION',
|
||||||
False
|
False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA',
|
||||||
|
timedelta(days=40)
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
from celery.task.schedules import crontab
|
||||||
|
from celery.decorators import periodic_task
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import BackendLog
|
||||||
|
|
||||||
|
|
||||||
|
@periodic_task(run_every=crontab(hour=7, minute=30, day_of_week=1))
|
||||||
|
def backend_logs_cleanup(run_every=run_every):
|
||||||
|
epoch = timezone.now()-settings.ORCHESTRATION_BACKEND_CLEANUP_DELTA
|
||||||
|
BackendLog.objects.filter(created_at__lt=epoch).delete()
|
|
@ -12,6 +12,9 @@ class BillsBackend(object):
|
||||||
create_new = options.get('new_open', False)
|
create_new = options.get('new_open', False)
|
||||||
proforma = options.get('proforma', False)
|
proforma = options.get('proforma', False)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
quantity = line.metric*line.size
|
||||||
|
if quantity == 0:
|
||||||
|
continue
|
||||||
service = line.order.service
|
service = line.order.service
|
||||||
# Create bill if needed
|
# Create bill if needed
|
||||||
if bill is None or service.is_fee:
|
if bill is None or service.is_fee:
|
||||||
|
@ -33,8 +36,6 @@ class BillsBackend(object):
|
||||||
bill = Invoice.objects.create(account=account, is_open=True)
|
bill = Invoice.objects.create(account=account, is_open=True)
|
||||||
bills.append(bill)
|
bills.append(bill)
|
||||||
# Create bill line
|
# Create bill line
|
||||||
quantity = line.metric*line.size
|
|
||||||
if quantity != 0:
|
|
||||||
billine = bill.lines.create(
|
billine = bill.lines.create(
|
||||||
rate=service.nominal_price,
|
rate=service.nominal_price,
|
||||||
quantity=line.metric*line.size,
|
quantity=line.metric*line.size,
|
||||||
|
@ -42,6 +43,8 @@ class BillsBackend(object):
|
||||||
subtotal=line.subtotal,
|
subtotal=line.subtotal,
|
||||||
tax=service.tax,
|
tax=service.tax,
|
||||||
description=self.get_line_description(line),
|
description=self.get_line_description(line),
|
||||||
|
start_on=line.ini,
|
||||||
|
end_on=line.end if service.billing_period != service.NEVER else None,
|
||||||
order=line.order,
|
order=line.order,
|
||||||
order_billed_on=line.order.old_billed_on,
|
order_billed_on=line.order.old_billed_on,
|
||||||
order_billed_until=line.order.old_billed_until
|
order_billed_until=line.order.old_billed_until
|
||||||
|
@ -49,20 +52,16 @@ class BillsBackend(object):
|
||||||
self.create_sublines(billine, line.discounts)
|
self.create_sublines(billine, line.discounts)
|
||||||
return bills
|
return bills
|
||||||
|
|
||||||
def format_period(self, ini, end):
|
# def format_period(self, ini, end):
|
||||||
ini = ini.strftime("%b, %Y")
|
# ini = ini.strftime("%b, %Y")
|
||||||
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
# end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
||||||
if ini == end:
|
# if ini == end:
|
||||||
return ini
|
# return ini
|
||||||
return _("{ini} to {end}").format(ini=ini, end=end)
|
# return _("{ini} to {end}").format(ini=ini, end=end)
|
||||||
|
|
||||||
def get_line_description(self, line):
|
def get_line_description(self, line):
|
||||||
service = line.order.service
|
service = line.order.service
|
||||||
if service.is_fee:
|
|
||||||
return self.format_period(line.ini, line.end)
|
|
||||||
description = line.order.description
|
description = line.order.description
|
||||||
if service.billing_period != service.NEVER:
|
|
||||||
description += " %s" % self.format_period(line.ini, line.end)
|
|
||||||
return description
|
return description
|
||||||
|
|
||||||
def get_verbose_quantity(self, line):
|
def get_verbose_quantity(self, line):
|
||||||
|
|
|
@ -31,18 +31,15 @@ class BilledOrderListFilter(SimpleListFilter):
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return (
|
||||||
('to_date', _("To date")),
|
('yes', _("Billed")),
|
||||||
('full', _("Full period")),
|
('no', _("Not billed")),
|
||||||
('not', _("Not billed")),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == 'to_date':
|
if self.value() == 'yes':
|
||||||
return queryset.filter(billed_until__isnull=False,
|
return queryset.filter(billed_until__isnull=False,
|
||||||
billed_until__gte=timezone.now())
|
billed_until__gte=timezone.now())
|
||||||
elif self.value() == 'full':
|
elif self.value() == 'no':
|
||||||
raise NotImplementedError
|
|
||||||
elif self.value() == 'not':
|
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(billed_until__isnull=True) |
|
Q(billed_until__isnull=True) |
|
||||||
Q(billed_until__lt=timezone.now())
|
Q(billed_until__lt=timezone.now())
|
||||||
|
|
|
@ -44,6 +44,9 @@ class OrderQuerySet(models.QuerySet):
|
||||||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||||
else:
|
else:
|
||||||
bills += [(account, bill_lines)]
|
bills += [(account, bill_lines)]
|
||||||
|
# TODO remove if commit and always return unique elemenets (set()) when the other todo is fixed
|
||||||
|
if commit:
|
||||||
|
return list(set(bills))
|
||||||
return bills
|
return bills
|
||||||
|
|
||||||
def givers(self, ini, end):
|
def givers(self, ini, end):
|
||||||
|
@ -173,6 +176,8 @@ class Order(models.Model):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
instance = self.content_object
|
instance = self.content_object
|
||||||
|
if instance is None:
|
||||||
|
return
|
||||||
handler = self.service.handler
|
handler = self.service.handler
|
||||||
metric = ''
|
metric = ''
|
||||||
if handler.metric:
|
if handler.metric:
|
||||||
|
|
|
@ -25,7 +25,7 @@ class GitLabSaaSBackend(ServiceController):
|
||||||
def validate_response(self, response, *status_codes):
|
def validate_response(self, response, *status_codes):
|
||||||
if response.status_code not in status_codes:
|
if response.status_code not in status_codes:
|
||||||
raise RuntimeError("[%i] %s" % (response.status_code, response.content))
|
raise RuntimeError("[%i] %s" % (response.status_code, response.content))
|
||||||
return json.loads(response.content)
|
return json.loads(response.content.decode('utf8'))
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
login_url = self.get_base_url() + '/session'
|
login_url = self.get_base_url() + '/session'
|
||||||
|
@ -61,7 +61,7 @@ class GitLabSaaSBackend(ServiceController):
|
||||||
user_url = self.get_user_url(saas)
|
user_url = self.get_user_url(saas)
|
||||||
response = requests.get(user_url, headers=self.headers)
|
response = requests.get(user_url, headers=self.headers)
|
||||||
user = self.validate_response(response, 200)
|
user = self.validate_response(response, 200)
|
||||||
user = json.loads(response.content)
|
user = json.loads(response.content.decode('utf8'))
|
||||||
user['password'] = saas.password
|
user['password'] = saas.password
|
||||||
response = requests.put(user_url, data=user, headers=self.headers)
|
response = requests.put(user_url, data=user, headers=self.headers)
|
||||||
user = self.validate_response(response, 200)
|
user = self.validate_response(response, 200)
|
||||||
|
@ -92,7 +92,8 @@ class GitLabSaaSBackend(ServiceController):
|
||||||
username = saas.name
|
username = saas.name
|
||||||
email = saas.data['email']
|
email = saas.data['email']
|
||||||
users_url = self.get_base_url() + '/users/'
|
users_url = self.get_base_url() + '/users/'
|
||||||
users = json.loads(requests.get(users_url, headers=self.headers).content)
|
response = requests.get(users_url, headers=self.headers)
|
||||||
|
users = json.loads(response.content.decode('utf8'))
|
||||||
for user in users:
|
for user in users:
|
||||||
if user['username'] == username:
|
if user['username'] == username:
|
||||||
print('ValidationError: user-exists')
|
print('ValidationError: user-exists')
|
||||||
|
|
|
@ -14,7 +14,7 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
|
|
||||||
def _save(self, saas, server):
|
def _save(self, saas, server):
|
||||||
admin_link = 'http://%s/admin/' % saas.get_site_domain()
|
admin_link = 'http://%s/admin/' % saas.get_site_domain()
|
||||||
admin_content = requests.get(admin_link).content
|
admin_content = requests.get(admin_link).content.decode('utf8')
|
||||||
if admin_content.startswith('Cannot connect to Database'):
|
if admin_content.startswith('Cannot connect to Database'):
|
||||||
raise RuntimeError("Database is not yet configured")
|
raise RuntimeError("Database is not yet configured")
|
||||||
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
|
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
|
||||||
|
@ -30,7 +30,7 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
'adminpassword': saas.password,
|
'adminpassword': saas.password,
|
||||||
}
|
}
|
||||||
response = requests.post(install_link, data=post)
|
response = requests.post(install_link, data=post)
|
||||||
print(response.content)
|
print(response.content.decode('utf8'))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise RuntimeError("Bad status code %i" % response.status_code)
|
raise RuntimeError("Bad status code %i" % response.status_code)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -31,7 +31,7 @@ class WordpressMuBackend(ServiceController):
|
||||||
|
|
||||||
def validate_response(self, response):
|
def validate_response(self, response):
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
errors = re.findall(r'<body id="error-page">\n\t<p>(.*)</p></body>', response.content)
|
errors = re.findall(r'<body id="error-page">\n\t<p>(.*)</p></body>', response.content.decode('utf8'))
|
||||||
raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code)
|
raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code)
|
||||||
|
|
||||||
def get_id(self, session, webapp):
|
def get_id(self, session, webapp):
|
||||||
|
@ -41,7 +41,7 @@ class WordpressMuBackend(ServiceController):
|
||||||
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
|
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
|
||||||
'class="edit">%s</a>' % webapp.name
|
'class="edit">%s</a>' % webapp.name
|
||||||
)
|
)
|
||||||
content = session.get(search).content
|
content = session.get(search).content.decode('utf8')
|
||||||
# Get id
|
# Get id
|
||||||
ids = regex.search(content)
|
ids = regex.search(content)
|
||||||
if not ids:
|
if not ids:
|
||||||
|
@ -64,7 +64,7 @@ class WordpressMuBackend(ServiceController):
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
url = self.get_base_url()
|
url = self.get_base_url()
|
||||||
url += '/wp-admin/network/site-new.php'
|
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 = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"')
|
||||||
wpnonce = wpnonce.search(content).groups()[0]
|
wpnonce = wpnonce.search(content).groups()[0]
|
||||||
|
@ -94,7 +94,7 @@ class WordpressMuBackend(ServiceController):
|
||||||
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
|
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
|
||||||
delete += '&id=%d&_wpnonce=%s' % (id, wpnonce)
|
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 = re.compile('name="_wpnonce"\s+value="([^"]*)"')
|
||||||
wpnonce = wpnonce.search(content).groups()[0]
|
wpnonce = wpnonce.search(content).groups()[0]
|
||||||
data = {
|
data = {
|
||||||
|
|
|
@ -106,6 +106,8 @@ class SoftwareService(plugins.Plugin):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
if log.state != log.SUCCESS:
|
||||||
|
raise ValidationError(_("Validate creation execution has failed."))
|
||||||
errors = {}
|
errors = {}
|
||||||
if 'user-exists' in log.stdout:
|
if 'user-exists' in log.stdout:
|
||||||
errors['name'] = _("User with this username already exists.")
|
errors['name'] = _("User with this username already exists.")
|
||||||
|
|
|
@ -23,8 +23,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
|
|
||||||
Relax and enjoy the journey.
|
Relax and enjoy the journey.
|
||||||
"""
|
"""
|
||||||
_VOLUME = 'VOLUME'
|
_VOLUME = 'volume'
|
||||||
_COMPENSATION = 'COMPENSATION'
|
_COMPENSATION = 'compensation'
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
|
@ -42,29 +42,27 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
def validate_content_type(self, service):
|
def validate_content_type(self, service):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def validate_match(self, service):
|
def validate_expression(self, service, method):
|
||||||
if not service.match:
|
|
||||||
service.match = 'True'
|
|
||||||
try:
|
try:
|
||||||
obj = service.content_type.model_class().objects.all()[0]
|
obj = service.content_type.model_class().objects.all()[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
bool(self.matches(obj))
|
bool(getattr(self, method)(obj))
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
name = type(exception).__name__
|
name = type(exception).__name__
|
||||||
raise ValidationError(': '.join((name, str(exception))))
|
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):
|
def validate_metric(self, service):
|
||||||
try:
|
self.validate_expression(service, 'get_metric')
|
||||||
obj = service.content_type.model_class().objects.all()[0]
|
|
||||||
except IndexError:
|
def validate_order_description(self, service):
|
||||||
return
|
self.validate_expression(service, 'get_order_description')
|
||||||
try:
|
|
||||||
bool(self.get_metric(obj))
|
|
||||||
except Exception as exception:
|
|
||||||
name = type(exception).__name__
|
|
||||||
raise ValidationError(': '.join((name, str(exception))))
|
|
||||||
|
|
||||||
def get_content_type(self):
|
def get_content_type(self):
|
||||||
if not self.model:
|
if not self.model:
|
||||||
|
@ -72,15 +70,26 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
app_label, model = self.model.split('.')
|
app_label, model = self.model.split('.')
|
||||||
return ContentType.objects.get_by_natural_key(app_label, model.lower())
|
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):
|
def matches(self, instance):
|
||||||
if not self.match:
|
if not self.match:
|
||||||
# Blank expressions always evaluate True
|
# Blank expressions always evaluate True
|
||||||
return True
|
return True
|
||||||
safe_locals = {
|
safe_locals = self.get_expression_context(instance)
|
||||||
'instance': instance,
|
|
||||||
'obj': instance,
|
|
||||||
instance._meta.model_name: instance,
|
|
||||||
}
|
|
||||||
return eval(self.match, safe_locals)
|
return eval(self.match, safe_locals)
|
||||||
|
|
||||||
def get_ignore_delta(self):
|
def get_ignore_delta(self):
|
||||||
|
@ -113,27 +122,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
|
|
||||||
def get_metric(self, instance):
|
def get_metric(self, instance):
|
||||||
if self.metric:
|
if self.metric:
|
||||||
safe_locals = {
|
safe_locals = self.get_expression_context(instance)
|
||||||
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,
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
return eval(self.metric, safe_locals)
|
return eval(self.metric, safe_locals)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise type(error)("%s on '%s'" %(error, self.service))
|
raise type(error)("%s on '%s'" %(error, self.service))
|
||||||
|
|
||||||
def get_order_description(self, instance):
|
def get_order_description(self, instance):
|
||||||
safe_locals = {
|
safe_locals = self.get_expression_context(instance)
|
||||||
'instance': instance,
|
|
||||||
'obj': instance,
|
|
||||||
'ugettext': ugettext,
|
|
||||||
instance._meta.model_name: instance,
|
|
||||||
}
|
|
||||||
account = getattr(instance, 'account', instance)
|
account = getattr(instance, 'account', instance)
|
||||||
with translation.override(account.language):
|
with translation.override(account.language):
|
||||||
if not self.order_description:
|
if not self.order_description:
|
||||||
|
|
|
@ -91,7 +91,7 @@ class Service(models.Model):
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Python <a href='https://docs.python.org/2/library/functions.html#eval'>expression</a> "
|
"Python <a href='https://docs.python.org/2/library/functions.html#eval'>expression</a> "
|
||||||
"used for generating the description for the bill lines of this services.<br>"
|
"used for generating the description for the bill lines of this services.<br>"
|
||||||
"Defaults to <tt>'%s: %s' % (handler.description, instance)</tt>"
|
"Defaults to <tt>'%s: %s' % (ugettext(handler.description), instance)</tt>"
|
||||||
))
|
))
|
||||||
ignore_period = models.CharField(_("ignore period"), max_length=16, blank=True,
|
ignore_period = models.CharField(_("ignore period"), max_length=16, blank=True,
|
||||||
help_text=_("Period in which orders will be ignored if cancelled. "
|
help_text=_("Period in which orders will be ignored if cancelled. "
|
||||||
|
@ -180,6 +180,7 @@ class Service(models.Model):
|
||||||
'content_type': (self.handler.validate_content_type, self),
|
'content_type': (self.handler.validate_content_type, self),
|
||||||
'match': (self.handler.validate_match, self),
|
'match': (self.handler.validate_match, self),
|
||||||
'metric': (self.handler.validate_metric, self),
|
'metric': (self.handler.validate_metric, self),
|
||||||
|
'order_description': (self.handler.validate_order_description, self),
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_pricing_period(self):
|
def get_pricing_period(self):
|
||||||
|
@ -238,7 +239,10 @@ class Service(models.Model):
|
||||||
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
||||||
related_model = self.content_type.model_class()
|
related_model = self.content_type.model_class()
|
||||||
updates = []
|
updates = []
|
||||||
for instance in related_model.objects.select_related('account').all():
|
queryset = related_model.objects.all()
|
||||||
|
if related_model._meta.model_name != 'account':
|
||||||
|
queryset = queryset.select_related('account').all()
|
||||||
|
for instance in queryset:
|
||||||
updates += order_model.update_orders(instance, service=self, commit=commit)
|
updates += order_model.update_orders(instance, service=self, commit=commit)
|
||||||
return updates
|
return updates
|
||||||
|
|
||||||
|
|
|
@ -118,8 +118,18 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
)
|
)
|
||||||
super(PHPBackend, self).commit()
|
super(PHPBackend, self).commit()
|
||||||
|
|
||||||
|
def get_options(self, webapp):
|
||||||
|
kwargs = {}
|
||||||
|
if self.MERGE:
|
||||||
|
kwargs = {
|
||||||
|
'webapp__account': webapp.account,
|
||||||
|
'webapp__type': webapp.type,
|
||||||
|
'webapp__data__contains': '"php_version":"%s"' % webapp.data['php_version'],
|
||||||
|
}
|
||||||
|
return webapp.get_options(**kwargs)
|
||||||
|
|
||||||
def get_fpm_config(self, webapp, context):
|
def get_fpm_config(self, webapp, context):
|
||||||
options = webapp.get_options(merge=self.MERGE)
|
options = self.get_options(webapp)
|
||||||
context.update({
|
context.update({
|
||||||
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
|
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
|
||||||
'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
|
'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
|
||||||
|
@ -168,7 +178,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
exec %(php_binary_path)s %(php_init_vars)s""") % context
|
exec %(php_binary_path)s %(php_init_vars)s""") % context
|
||||||
|
|
||||||
def get_fcgid_cmd_options(self, webapp, context):
|
def get_fcgid_cmd_options(self, webapp, context):
|
||||||
options = webapp.get_options(merge=self.MERGE)
|
options = self.get_options(webapp)
|
||||||
maps = {
|
maps = {
|
||||||
'MaxProcesses': options.get('processes', None),
|
'MaxProcesses': options.get('processes', None),
|
||||||
'IOTimeout': options.get('timeout', None),
|
'IOTimeout': options.get('timeout', None),
|
||||||
|
|
|
@ -10,7 +10,12 @@ from . import WebAppServiceMixin
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
|
||||||
class PythonBackend(WebAppServiceMixin, ServiceController):
|
class uWSGIPythonBackend(WebAppServiceMixin, ServiceController):
|
||||||
|
"""
|
||||||
|
Emperor mode
|
||||||
|
|
||||||
|
http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
|
||||||
|
"""
|
||||||
verbose_name = _("Python uWSGI")
|
verbose_name = _("Python uWSGI")
|
||||||
default_route_match = "webapp.type.endswith('python')"
|
default_route_match = "webapp.type.endswith('python')"
|
||||||
|
|
||||||
|
@ -26,26 +31,14 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
|
||||||
self.delete_webapp_dir(context)
|
self.delete_webapp_dir(context)
|
||||||
|
|
||||||
def save_uwsgi(self, webapp, context):
|
def save_uwsgi(self, webapp, context):
|
||||||
self.append(textwrap.dedent("""\
|
self.append("echo '%(uwsgi_config)s' > %(vassal_path)s" % context)
|
||||||
uwsgi_config='%(uwsgi_config)s'
|
|
||||||
{
|
|
||||||
echo -e "${uwsgi_config}" | diff -N -I'^\s*;;' %(uwsgi_path)s -
|
|
||||||
} || {
|
|
||||||
echo -e "${uwsgi_config}" > %(uwsgi_path)s
|
|
||||||
UPDATED_UWSGI=1
|
|
||||||
}
|
|
||||||
ln -s %(uwsgi_path)s %(uwsgi_enabled)s
|
|
||||||
""") % context
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete_uwsgi(self, webapp, context):
|
def delete_uwsgi(self, webapp, context):
|
||||||
self.append("rm -f %(uwsgi_path)s" % context)
|
self.append("rm -f %(vassal_path)s" % context)
|
||||||
self.append("rm -f %(uwsgi_enabled)s" % context)
|
|
||||||
|
|
||||||
def get_uwsgi_ini(self, context):
|
def get_uwsgi_ini(self, context):
|
||||||
# TODO switch to this http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
|
|
||||||
# TODO http://uwsgi-docs.readthedocs.org/en/latest/Changelog-1.9.1.html#on-demand-vassals
|
|
||||||
return textwrap.dedent("""\
|
return textwrap.dedent("""\
|
||||||
|
# %(banner)s
|
||||||
[uwsgi]
|
[uwsgi]
|
||||||
plugins = python{python_version_number}
|
plugins = python{python_version_number}
|
||||||
chdir = {app_path}
|
chdir = {app_path}
|
||||||
|
@ -70,10 +63,8 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
|
||||||
context.update({
|
context.update({
|
||||||
'uwsgi_ini': self.get_uwsgi_ini(context),
|
'uwsgi_ini': self.get_uwsgi_ini(context),
|
||||||
'uwsgi_dir': settings.WEBAPPS_UWSGI_BASE_DIR,
|
'uwsgi_dir': settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||||
'uwsgi_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
|
'vassal_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||||
'apps-available/%s.ini'% context['app_name']),
|
'vassals/%s' % context['app_name']),
|
||||||
'uwsgi_enabled': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
|
|
||||||
'apps-enabled/%s.ini'% context['app_name']),
|
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
import re
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orchestra.contrib.orchestration import ServiceController
|
|
||||||
|
|
||||||
from .. import settings
|
|
||||||
|
|
||||||
|
|
||||||
class WordpressMuBackend(ServiceController):
|
|
||||||
verbose_name = _("Wordpress multisite")
|
|
||||||
model = 'saas.SaaS'
|
|
||||||
default_route_match = "saas.service == 'wordpress-mu'"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def script(self):
|
|
||||||
return self.cmds
|
|
||||||
|
|
||||||
def login(self, session):
|
|
||||||
base_url = self.get_base_url()
|
|
||||||
login_url = base_url + '/wp-login.php'
|
|
||||||
login_data = {
|
|
||||||
'log': 'admin',
|
|
||||||
'pwd': settings.WEBSITES_WORDPRESSMU_ADMIN_PASSWORD,
|
|
||||||
'redirect_to': '/wp-admin/'
|
|
||||||
}
|
|
||||||
response = session.post(login_url, data=login_data)
|
|
||||||
if response.url != base_url + '/wp-admin/':
|
|
||||||
raise IOError("Failure login to remote application")
|
|
||||||
|
|
||||||
def get_base_url(self):
|
|
||||||
base_url = settings.WEBSITES_WORDPRESSMU_BASE_URL
|
|
||||||
return base_url.rstrip('/')
|
|
||||||
|
|
||||||
def validate_response(self, response):
|
|
||||||
if response.status_code != 200:
|
|
||||||
errors = re.findall(r'<body id="error-page">\n\t<p>(.*)</p></body>', 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(
|
|
||||||
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
|
|
||||||
'class="edit">%s</a>' % 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'<span class="delete">(.*)</span>', 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)
|
|
|
@ -57,17 +57,19 @@ class WebApp(models.Model):
|
||||||
self.data = apptype.clean_data()
|
self.data = apptype.clean_data()
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def get_options(self, merge=False):
|
def get_options(self, **kwargs):
|
||||||
if merge:
|
|
||||||
options = OrderedDict()
|
options = OrderedDict()
|
||||||
qs = WebAppOption.objects.filter(webapp__account=self.account, webapp__type=self.type)
|
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'):
|
for name, value in qs.values_list('name', 'value').order_by('name'):
|
||||||
if name in options:
|
if name in options:
|
||||||
options[name] = max(options[name], value)
|
options[name] = max(options[name], value)
|
||||||
else:
|
else:
|
||||||
options[name] = value
|
options[name] = value
|
||||||
return options
|
return options
|
||||||
return OrderedDict(self.options.values_list('name', 'value').order_by('name'))
|
|
||||||
|
|
||||||
def get_directive(self):
|
def get_directive(self):
|
||||||
return self.type_instance.get_directive()
|
return self.type_instance.get_directive()
|
||||||
|
|
|
@ -9,4 +9,4 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *filenames, **options):
|
def handle(self, *filenames, **options):
|
||||||
flake = run('flake8 {%s,%s} | grep -v "W293\|E501"' % (get_orchestra_dir(), get_site_dir()))
|
flake = run('flake8 {%s,%s} | grep -v "W293\|E501"' % (get_orchestra_dir(), get_site_dir()))
|
||||||
print(flake.stdout)
|
self.stdout.write(flake.stdout)
|
||||||
|
|
|
@ -7,5 +7,5 @@ def html_to_pdf(html):
|
||||||
'PATH=$PATH:/usr/local/bin/\n'
|
'PATH=$PATH:/usr/local/bin/\n'
|
||||||
'xvfb-run -a -s "-screen 0 640x4800x16" '
|
'xvfb-run -a -s "-screen 0 640x4800x16" '
|
||||||
'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
|
'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
|
).stdout
|
||||||
|
|
|
@ -45,7 +45,7 @@ def read_async(fd):
|
||||||
return ''
|
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 """
|
""" Subprocess wrapper for running commands concurrently """
|
||||||
if display:
|
if display:
|
||||||
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
|
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
|
||||||
|
@ -53,7 +53,7 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
|
||||||
p = subprocess.Popen(command, shell=True, executable='/bin/bash',
|
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()
|
p.stdin.close()
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
@ -62,16 +62,18 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
|
||||||
|
|
||||||
# Async reading of stdout and sterr
|
# Async reading of stdout and sterr
|
||||||
while True:
|
while True:
|
||||||
stdout = ''
|
stdout = b''
|
||||||
stderr = ''
|
stderr = b''
|
||||||
# Get complete unicode chunks
|
# Get complete unicode chunks
|
||||||
select.select([p.stdout, p.stderr], [], [])
|
select.select([p.stdout, p.stderr], [], [])
|
||||||
|
|
||||||
stdoutPiece = read_async(p.stdout)
|
stdoutPiece = read_async(p.stdout)
|
||||||
stderrPiece = read_async(p.stderr)
|
stderrPiece = read_async(p.stderr)
|
||||||
|
|
||||||
stdout += (stdoutPiece or b'').decode('utf8', errors='replace')
|
stdout += (stdoutPiece or b'')
|
||||||
stderr += (stderrPiece or b'').decode('utf8', errors='replace')
|
#.decode('ascii'), errors='replace')
|
||||||
|
stderr += (stderrPiece or b'')
|
||||||
|
#.decode('ascii'), errors='replace')
|
||||||
|
|
||||||
if display and stdout:
|
if display and stdout:
|
||||||
sys.stdout.write(stdout)
|
sys.stdout.write(stdout)
|
||||||
|
@ -89,14 +91,14 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='',
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
|
|
||||||
|
|
||||||
def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False, force_unicode=True):
|
def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False):
|
||||||
iterator = runiterator(command, display, error_codes, silent, stdin, force_unicode)
|
iterator = runiterator(command, display, error_codes, silent, stdin)
|
||||||
next(iterator)
|
next(iterator)
|
||||||
if async:
|
if async:
|
||||||
return iterator
|
return iterator
|
||||||
|
|
||||||
stdout = ''
|
stdout = b''
|
||||||
stderr = ''
|
stderr = b''
|
||||||
for state in iterator:
|
for state in iterator:
|
||||||
stdout += state.stdout
|
stdout += state.stdout
|
||||||
stderr += state.stderr
|
stderr += state.stderr
|
||||||
|
|
Loading…
Reference in New Issue