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
|
||||
* 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
|
||||
|
|
3628
docs/images/orchestration.svg
Normal file
3628
docs/images/orchestration.svg
Normal file
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):
|
||||
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 '<a href="%s" %s>%s</a>' % (url, extra, obj)
|
||||
return '<a href="%s" %s>%s</a>' % (url, extra, display)
|
||||
|
||||
|
||||
@admin_field
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
),
|
||||
)
|
||||
|
|
Binary file not shown.
|
@ -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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -18,112 +18,112 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: actions.py:35
|
||||
#: actions.py:36
|
||||
msgid "Download"
|
||||
msgstr "Descarrega"
|
||||
|
||||
#: actions.py:45
|
||||
#: actions.py:46
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
#: actions.py:53
|
||||
#: actions.py:54
|
||||
msgid "Selected bills should be in open state"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:71
|
||||
#: actions.py:72
|
||||
msgid "Selected bills have been closed"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:80
|
||||
#, python-format
|
||||
msgid "<a href=\"%s\">One related transaction</a> has been created"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:81
|
||||
#, 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"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:87
|
||||
#: actions.py:88
|
||||
msgid "Are you sure about closing the following bills?"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:88
|
||||
#: actions.py:89
|
||||
msgid ""
|
||||
"Once a bill is closed it can not be further modified.</p><p>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. <br>\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 <br>\n"
|
||||
" "
|
||||
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
|
||||
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 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']:
|
||||
|
|
|
@ -108,7 +108,7 @@ hr {
|
|||
</div>
|
||||
|
||||
<div id="date" class="column-2">
|
||||
From {{ bill.lines.get.description }}
|
||||
From {{ bill.lines.get.get_verbose_period }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
|
|
@ -175,10 +175,14 @@ a:hover {
|
|||
}
|
||||
|
||||
#lines .column-description {
|
||||
width: 65%;
|
||||
width: 45%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#lines .column-period {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#lines .column-quantity {
|
||||
width: 10%;
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@
|
|||
</div>
|
||||
<div class="contact">
|
||||
<p>{{ seller.address }}<br>
|
||||
{{ seller.zipcode }} - {{ seller.city }}<br>
|
||||
{{ seller.country }}<br>
|
||||
{{ seller.zipcode }} - {% trans seller.city %}<br>
|
||||
{% trans seller.get_country_display %}<br>
|
||||
</p>
|
||||
<p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br>
|
||||
<a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br>
|
||||
|
@ -40,21 +40,21 @@
|
|||
|
||||
{% block summary %}
|
||||
<div id="bill-number">
|
||||
{{ bill.get_type_display.capitalize }}<br>
|
||||
{% trans bill.get_type_display.capitalize %}<br>
|
||||
<span class="value">{{ bill.number }}</span><br>
|
||||
</div>
|
||||
<div id="bill-summary">
|
||||
<hr>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -62,8 +62,8 @@
|
|||
<span class="name">{{ buyer.get_name }}</span><br>
|
||||
{{ buyer.vat }}<br>
|
||||
{{ buyer.address }}<br>
|
||||
{{ buyer.zipcode }} - {{ buyer.city }}<br>
|
||||
{{ buyer.country }}<br>
|
||||
{{ buyer.zipcode }} - {% trans buyer.city %}<br>
|
||||
{% trans buyer.get_country_display %}<br>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -71,6 +71,7 @@
|
|||
<div id="lines">
|
||||
<span class="title column-id">id</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-rate">{% trans "rate/price" %}</span>
|
||||
<span class="title column-subtotal">{% trans "subtotal" %}</span>
|
||||
|
@ -79,6 +80,7 @@
|
|||
{% 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-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-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>
|
||||
|
@ -86,6 +88,7 @@
|
|||
{% for subline in sublines %}
|
||||
<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-period"> </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-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
|
||||
|
@ -97,7 +100,7 @@
|
|||
<div id="totals">
|
||||
<br> <br>
|
||||
{% 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>
|
||||
<br>
|
||||
<span class="tax column-title">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</span>
|
||||
|
@ -126,9 +129,9 @@
|
|||
{% if payment.message %}
|
||||
{{ payment.message | safe }}
|
||||
{% 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>
|
||||
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>
|
||||
{% endblocktrans %}
|
||||
<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 Operation
|
||||
from orchestra.utils.python import OrderedSet
|
||||
|
||||
from . import settings
|
||||
|
||||
|
@ -92,9 +93,9 @@ class Bind9MasterDomainBackend(ServiceController):
|
|||
return servers
|
||||
|
||||
def get_slaves(self, domain):
|
||||
return set(settings.DOMAINS_SLAVES).union(
|
||||
set(self.get_servers(domain, Bind9SlaveDomainBackend))
|
||||
)
|
||||
ips = list(settings.DOMAINS_SLAVES)
|
||||
ips += self.get_servers(domain, Bind9SlaveDomainBackend)
|
||||
return OrderedSet(ips)
|
||||
|
||||
def get_context(self, 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')
|
||||
|
||||
def get_masters(self, domain):
|
||||
return set(settings.DOMAINS_MASTERS).union(
|
||||
set(self.get_servers(domain, Bind9MasterDomainBackend))
|
||||
)
|
||||
ips = list(settings.DOMAINS_MASTERS)
|
||||
ips += self.get_servers(domain, Bind9MasterDomainBackend)
|
||||
return OrderedSet(ips)
|
||||
|
||||
def get_context(self, domain):
|
||||
context = {
|
||||
|
|
|
@ -164,17 +164,25 @@ class Domain(models.Model):
|
|||
value=' '.join(soa)
|
||||
))
|
||||
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:
|
||||
for mx in settings.DOMAINS_DEFAULT_MX:
|
||||
if is_host:
|
||||
if Record.MX not in types:
|
||||
for mx in settings.DOMAINS_DEFAULT_MX:
|
||||
records.append(AttrDict(
|
||||
type=Record.MX,
|
||||
value=mx
|
||||
))
|
||||
default_a = settings.DOMAINS_DEFAULT_A
|
||||
if default_a and Record.A not in types:
|
||||
records.append(AttrDict(
|
||||
type=Record.MX,
|
||||
value=mx
|
||||
type=Record.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
|
||||
))
|
||||
if (Record.A not in types and Record.AAAA not in types) and is_host:
|
||||
records.append(AttrDict(
|
||||
type=Record.A,
|
||||
value=settings.DOMAINS_DEFAULT_A
|
||||
))
|
||||
result = ''
|
||||
for record in records:
|
||||
name = '{name}.{spaces}'.format(
|
||||
|
|
|
@ -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', (
|
||||
'10 mail.{}.'.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.models import Server
|
||||
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):
|
||||
|
@ -34,7 +34,7 @@ class Command(BaseCommand):
|
|||
list_backends = options.get('list_backends')
|
||||
if list_backends:
|
||||
for backend in ServiceBackend.get_backends():
|
||||
print(str(backend).split("'")[1])
|
||||
self.stdout.write(str(backend).split("'")[1])
|
||||
return
|
||||
model = get_model(*options['model'].split('.'))
|
||||
action = options.get('action')
|
||||
|
@ -49,9 +49,9 @@ class Command(BaseCommand):
|
|||
comps = iter(comp.split('='))
|
||||
for arg in comps:
|
||||
kwargs[arg] = next(comps).strip().rstrip(',')
|
||||
operations = []
|
||||
operations = set()
|
||||
operations = OrderedSet()
|
||||
route_cache = {}
|
||||
queryset = model.objects.filter(**kwargs).order_by('id')
|
||||
if servers:
|
||||
server_objects = []
|
||||
# Get and create missing Servers
|
||||
|
@ -62,12 +62,12 @@ class Command(BaseCommand):
|
|||
server = Server.objects.create(name=server, address=server)
|
||||
server_objects.append(server)
|
||||
# Generate operations for the given backend
|
||||
for instance in model.objects.filter(**kwargs):
|
||||
for instance in queryset:
|
||||
for backend in backends:
|
||||
backend = import_class(backend)
|
||||
operations.add(Operation(backend, instance, action, servers=server_objects))
|
||||
else:
|
||||
for instance in model.objects.filter(**kwargs):
|
||||
for instance in queryset:
|
||||
manager.collect(instance, action, operations=operations, route_cache=route_cache)
|
||||
scripts, block = manager.generate(operations)
|
||||
servers = []
|
||||
|
@ -76,11 +76,10 @@ class Command(BaseCommand):
|
|||
server, __ = key
|
||||
backend, operations = value
|
||||
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:
|
||||
script = '\n'.join(commands) + '\n'
|
||||
script = script.encode('ascii', errors='replace')
|
||||
sys.stdout.write(script.decode('ascii'))
|
||||
script = '\n'.join(commands)
|
||||
self.stdout.write(script)
|
||||
if interactive:
|
||||
context = {
|
||||
'servers': ', '.join(servers),
|
||||
|
@ -97,7 +96,7 @@ class Command(BaseCommand):
|
|||
if not dry:
|
||||
logs = manager.execute(scripts, block=block)
|
||||
for log in logs:
|
||||
print(log.stdout.encode('utf8', errors='replace'))
|
||||
sys.stderr.write(log.stderr.encode('utf8', errors='replace'))
|
||||
self.stdout.write(log.stdout)
|
||||
self.stderr.write(log.stderr)
|
||||
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.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 .backends import ServiceBackend
|
||||
|
@ -138,7 +138,7 @@ def execute(scripts, block=False, async=False):
|
|||
|
||||
def collect(instance, action, **kwargs):
|
||||
""" collect operations """
|
||||
operations = kwargs.get('operations', set())
|
||||
operations = kwargs.get('operations', OrderedSet())
|
||||
route_cache = kwargs.get('route_cache', {})
|
||||
for backend_cls in ServiceBackend.get_backends():
|
||||
# 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 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',
|
||||
False
|
||||
)
|
||||
|
||||
|
||||
ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA',
|
||||
timedelta(days=40)
|
||||
)
|
||||
|
|
11
orchestra/contrib/orchestration/tasks.py
Normal file
11
orchestra/contrib/orchestration/tasks.py
Normal file
|
@ -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)
|
||||
proforma = options.get('proforma', False)
|
||||
for line in lines:
|
||||
quantity = line.metric*line.size
|
||||
if quantity == 0:
|
||||
continue
|
||||
service = line.order.service
|
||||
# Create bill if needed
|
||||
if bill is None or service.is_fee:
|
||||
|
@ -33,36 +36,32 @@ class BillsBackend(object):
|
|||
bill = Invoice.objects.create(account=account, is_open=True)
|
||||
bills.append(bill)
|
||||
# Create bill line
|
||||
quantity = line.metric*line.size
|
||||
if quantity != 0:
|
||||
billine = bill.lines.create(
|
||||
rate=service.nominal_price,
|
||||
quantity=line.metric*line.size,
|
||||
verbose_quantity=self.get_verbose_quantity(line),
|
||||
subtotal=line.subtotal,
|
||||
tax=service.tax,
|
||||
description=self.get_line_description(line),
|
||||
order=line.order,
|
||||
order_billed_on=line.order.old_billed_on,
|
||||
order_billed_until=line.order.old_billed_until
|
||||
)
|
||||
self.create_sublines(billine, line.discounts)
|
||||
billine = bill.lines.create(
|
||||
rate=service.nominal_price,
|
||||
quantity=line.metric*line.size,
|
||||
verbose_quantity=self.get_verbose_quantity(line),
|
||||
subtotal=line.subtotal,
|
||||
tax=service.tax,
|
||||
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_billed_on=line.order.old_billed_on,
|
||||
order_billed_until=line.order.old_billed_until
|
||||
)
|
||||
self.create_sublines(billine, line.discounts)
|
||||
return bills
|
||||
|
||||
def format_period(self, ini, end):
|
||||
ini = ini.strftime("%b, %Y")
|
||||
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
||||
if ini == end:
|
||||
return ini
|
||||
return _("{ini} to {end}").format(ini=ini, end=end)
|
||||
# def format_period(self, ini, end):
|
||||
# ini = ini.strftime("%b, %Y")
|
||||
# end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
||||
# if ini == end:
|
||||
# return ini
|
||||
# return _("{ini} to {end}").format(ini=ini, end=end)
|
||||
|
||||
def get_line_description(self, line):
|
||||
service = line.order.service
|
||||
if service.is_fee:
|
||||
return self.format_period(line.ini, line.end)
|
||||
description = line.order.description
|
||||
if service.billing_period != service.NEVER:
|
||||
description += " %s" % self.format_period(line.ini, line.end)
|
||||
return description
|
||||
|
||||
def get_verbose_quantity(self, line):
|
||||
|
|
|
@ -31,18 +31,15 @@ class BilledOrderListFilter(SimpleListFilter):
|
|||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('to_date', _("To date")),
|
||||
('full', _("Full period")),
|
||||
('not', _("Not billed")),
|
||||
('yes', _("Billed")),
|
||||
('no', _("Not billed")),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'to_date':
|
||||
if self.value() == 'yes':
|
||||
return queryset.filter(billed_until__isnull=False,
|
||||
billed_until__gte=timezone.now())
|
||||
elif self.value() == 'full':
|
||||
raise NotImplementedError
|
||||
elif self.value() == 'not':
|
||||
elif self.value() == 'no':
|
||||
return queryset.filter(
|
||||
Q(billed_until__isnull=True) |
|
||||
Q(billed_until__lt=timezone.now())
|
||||
|
|
|
@ -44,6 +44,9 @@ class OrderQuerySet(models.QuerySet):
|
|||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||
else:
|
||||
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
|
||||
|
||||
def givers(self, ini, end):
|
||||
|
@ -173,6 +176,8 @@ class Order(models.Model):
|
|||
|
||||
def update(self):
|
||||
instance = self.content_object
|
||||
if instance is None:
|
||||
return
|
||||
handler = self.service.handler
|
||||
metric = ''
|
||||
if handler.metric:
|
||||
|
|
|
@ -25,7 +25,7 @@ class GitLabSaaSBackend(ServiceController):
|
|||
def validate_response(self, response, *status_codes):
|
||||
if response.status_code not in status_codes:
|
||||
raise RuntimeError("[%i] %s" % (response.status_code, response.content))
|
||||
return json.loads(response.content)
|
||||
return json.loads(response.content.decode('utf8'))
|
||||
|
||||
def authenticate(self):
|
||||
login_url = self.get_base_url() + '/session'
|
||||
|
@ -61,7 +61,7 @@ class GitLabSaaSBackend(ServiceController):
|
|||
user_url = self.get_user_url(saas)
|
||||
response = requests.get(user_url, headers=self.headers)
|
||||
user = self.validate_response(response, 200)
|
||||
user = json.loads(response.content)
|
||||
user = json.loads(response.content.decode('utf8'))
|
||||
user['password'] = saas.password
|
||||
response = requests.put(user_url, data=user, headers=self.headers)
|
||||
user = self.validate_response(response, 200)
|
||||
|
@ -92,7 +92,8 @@ class GitLabSaaSBackend(ServiceController):
|
|||
username = saas.name
|
||||
email = saas.data['email']
|
||||
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:
|
||||
if user['username'] == username:
|
||||
print('ValidationError: user-exists')
|
||||
|
|
|
@ -14,7 +14,7 @@ class PhpListSaaSBackend(ServiceController):
|
|||
|
||||
def _save(self, saas, server):
|
||||
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'):
|
||||
raise RuntimeError("Database is not yet configured")
|
||||
install = re.search(r'([^"]+firstinstall[^"]+)', admin_content)
|
||||
|
@ -30,7 +30,7 @@ class PhpListSaaSBackend(ServiceController):
|
|||
'adminpassword': saas.password,
|
||||
}
|
||||
response = requests.post(install_link, data=post)
|
||||
print(response.content)
|
||||
print(response.content.decode('utf8'))
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError("Bad status code %i" % response.status_code)
|
||||
else:
|
||||
|
|
|
@ -31,7 +31,7 @@ class WordpressMuBackend(ServiceController):
|
|||
|
||||
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)
|
||||
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)
|
||||
|
||||
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+'
|
||||
'class="edit">%s</a>' % 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 = {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -91,7 +91,7 @@ class Service(models.Model):
|
|||
help_text=_(
|
||||
"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>"
|
||||
"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,
|
||||
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),
|
||||
'match': (self.handler.validate_match, self),
|
||||
'metric': (self.handler.validate_metric, self),
|
||||
'order_description': (self.handler.validate_order_description, self),
|
||||
})
|
||||
|
||||
def get_pricing_period(self):
|
||||
|
@ -238,7 +239,10 @@ class Service(models.Model):
|
|||
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
||||
related_model = self.content_type.model_class()
|
||||
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)
|
||||
return updates
|
||||
|
||||
|
|
|
@ -118,8 +118,18 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
|||
)
|
||||
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):
|
||||
options = webapp.get_options(merge=self.MERGE)
|
||||
options = self.get_options(webapp)
|
||||
context.update({
|
||||
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
|
||||
'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
|
||||
|
||||
def get_fcgid_cmd_options(self, webapp, context):
|
||||
options = webapp.get_options(merge=self.MERGE)
|
||||
options = self.get_options(webapp)
|
||||
maps = {
|
||||
'MaxProcesses': options.get('processes', None),
|
||||
'IOTimeout': options.get('timeout', None),
|
||||
|
|
|
@ -10,7 +10,12 @@ from . import WebAppServiceMixin
|
|||
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")
|
||||
default_route_match = "webapp.type.endswith('python')"
|
||||
|
||||
|
@ -26,26 +31,14 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
|
|||
self.delete_webapp_dir(context)
|
||||
|
||||
def save_uwsgi(self, webapp, context):
|
||||
self.append(textwrap.dedent("""\
|
||||
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
|
||||
)
|
||||
self.append("echo '%(uwsgi_config)s' > %(vassal_path)s" % context)
|
||||
|
||||
def delete_uwsgi(self, webapp, context):
|
||||
self.append("rm -f %(uwsgi_path)s" % context)
|
||||
self.append("rm -f %(uwsgi_enabled)s" % context)
|
||||
self.append("rm -f %(vassal_path)s" % 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("""\
|
||||
# %(banner)s
|
||||
[uwsgi]
|
||||
plugins = python{python_version_number}
|
||||
chdir = {app_path}
|
||||
|
@ -70,10 +63,8 @@ class PythonBackend(WebAppServiceMixin, ServiceController):
|
|||
context.update({
|
||||
'uwsgi_ini': self.get_uwsgi_ini(context),
|
||||
'uwsgi_dir': settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||
'uwsgi_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||
'apps-available/%s.ini'% context['app_name']),
|
||||
'uwsgi_enabled': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||
'apps-enabled/%s.ini'% context['app_name']),
|
||||
'vassal_path': os.path.join(settings.WEBAPPS_UWSGI_BASE_DIR,
|
||||
'vassals/%s' % context['app_name']),
|
||||
})
|
||||
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()
|
||||
|
||||
@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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue