Added database translations support
This commit is contained in:
parent
124124da6c
commit
bcfc453a95
55
TODO.md
55
TODO.md
|
@ -83,7 +83,8 @@
|
||||||
|
|
||||||
* print open invoices as proforma?
|
* print open invoices as proforma?
|
||||||
|
|
||||||
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python manage.py test orchestra.apps.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest
|
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python manage.py test orchestra.apps.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest --nologcapture¶
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* ForeignKey.swappable
|
* ForeignKey.swappable
|
||||||
|
@ -222,13 +223,55 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl
|
||||||
* autoexpand mailbox.filter according to filtering options
|
* autoexpand mailbox.filter according to filtering options
|
||||||
|
|
||||||
* allow empty metric pack for default rates? changes on rating algo
|
* allow empty metric pack for default rates? changes on rating algo
|
||||||
* rates plan verbose name!"!
|
* IMPORTANT make sure no order is created for mailboxes that include disk? or just don't produce lines with cost == 0 or quantity 0 ?
|
||||||
* IMPORTANT make sure no order is created for mailboxes that include disk? or just don't produce lines with cost == 0
|
|
||||||
* IMPORTANT maildis updae and metric storage ?? threshold ? or what?
|
|
||||||
|
|
||||||
* Improve performance of admin change lists with debug toolbar and prefech_related
|
* Improve performance of admin change lists with debug toolbar and prefech_related
|
||||||
* and miscellaneous.service.name == 'domini-registre'
|
* and miscellaneous.service.name == 'domini-registre'
|
||||||
* DOMINI REGISTRE MIGRATION SCRIPTS
|
* DOMINI REGISTRE MIGRATION SCRIPTS
|
||||||
|
|
||||||
* detect subdomains accounts correctly with subdomains: i.e. www.marcay.pangea.org
|
* lines too long on invoice, double lines or cut, and make margin wider
|
||||||
* lines too long on invoice, double lines or cut
|
* PHP_TIMEOUT env variable in sync with fcgid idle timeout
|
||||||
|
|
||||||
|
* payment methods icons
|
||||||
|
* use server.name | server.address on python backends, like gitlab instead of settings?
|
||||||
|
* saas change password feature (the only way of re.running a backend)
|
||||||
|
|
||||||
|
* TODO raise404, here and everywhere
|
||||||
|
* display subline links on billlines
|
||||||
|
* update service orders on a celery task?
|
||||||
|
|
||||||
|
* billline quantity eval('10x100') instead of miningless description '(10*100)'
|
||||||
|
|
||||||
|
* order metric increases inside billed until period
|
||||||
|
* do more test, make sure billed until doesn't get uodated whhen services are billed with les metric, and don't upgrade billed_until when undoing under this circumstances
|
||||||
|
|
||||||
|
* move normurlpath to orchestra.utils from websites.utils
|
||||||
|
|
||||||
|
* one time service metric change should update last value, only record for recurring invoicing.
|
||||||
|
|
||||||
|
* write down insights
|
||||||
|
|
||||||
|
* pluggable rate algorithms, with help_text, and change some services to match price
|
||||||
|
|
||||||
|
* translation app, with generates the trans files from models
|
||||||
|
* use english on services defs and so on, an translate them on render time
|
||||||
|
* (miscellaneous.service.ident or '').startswith()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Translation
|
||||||
|
-----------
|
||||||
|
|
||||||
|
python manage.py makemessages -l ca --domain database
|
||||||
|
|
||||||
|
mkdir locale
|
||||||
|
django-admin.py makemessages -l ca
|
||||||
|
django-admin.py compilemessages -l ca
|
||||||
|
|
||||||
|
https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#joining-strings-string-concat
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext
|
||||||
|
from django.utils import translation
|
||||||
|
translation.activate('ca')
|
||||||
|
ugettext("Fuck you")
|
||||||
|
|
||||||
|
|
|
@ -111,3 +111,68 @@ def send_bills(modeladmin, request, queryset):
|
||||||
modeladmin.log_change(request, bill, 'Sent')
|
modeladmin.log_change(request, bill, 'Sent')
|
||||||
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
|
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
|
||||||
send_bills.url_name = 'send'
|
send_bills.url_name = 'send'
|
||||||
|
|
||||||
|
|
||||||
|
def undo_billing(modeladmin, request, queryset):
|
||||||
|
group = {}
|
||||||
|
for line in queryset.select_related('order'):
|
||||||
|
if line.order_id:
|
||||||
|
try:
|
||||||
|
group[line.order].append(line)
|
||||||
|
except KeyError:
|
||||||
|
group[line.order] = [line]
|
||||||
|
# TODO force incomplete info
|
||||||
|
for order, lines in group.iteritems():
|
||||||
|
# Find path from ini to end
|
||||||
|
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
||||||
|
if not getattr(self, attr):
|
||||||
|
raise ValidationError(_("Not enough information stored for undoing"))
|
||||||
|
sorted(lines, key=lambda l: l.created_on)
|
||||||
|
if 'a' != order.billed_on:
|
||||||
|
raise ValidationError(_("Dates don't match"))
|
||||||
|
prev = order.billed_on
|
||||||
|
for ix in xrange(0, len(lines)):
|
||||||
|
if lines[ix].order_b: # TODO we need to look at the periods here
|
||||||
|
pass
|
||||||
|
order.billed_until = self.order_billed_until
|
||||||
|
order.billed_on = self.order_billed_on
|
||||||
|
|
||||||
|
# TODO son't check for account equality
|
||||||
|
def move_lines(modeladmin, request, queryset):
|
||||||
|
# Validate
|
||||||
|
account = None
|
||||||
|
for line in queryset.select_related('bill'):
|
||||||
|
bill = line.bill
|
||||||
|
if bill.state != bill.OPEN:
|
||||||
|
messages.error(request, _("Can not move lines which are not in open state."))
|
||||||
|
return
|
||||||
|
elif not account:
|
||||||
|
account = bill.account
|
||||||
|
elif bill.account != account:
|
||||||
|
messages.error(request, _("Can not move lines from different accounts"))
|
||||||
|
return
|
||||||
|
target = request.GET.get('target')
|
||||||
|
if not target:
|
||||||
|
# select target
|
||||||
|
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
||||||
|
target = Bill.objects.get(pk=int(pk))
|
||||||
|
if target.account != account:
|
||||||
|
messages.error(request, _("Target account different than lines account."))
|
||||||
|
return
|
||||||
|
if request.POST.get('post') == 'generic_confirmation':
|
||||||
|
for line in queryset:
|
||||||
|
line.bill = target
|
||||||
|
line.save(update_fields=['bill'])
|
||||||
|
# TODO bill history update
|
||||||
|
messages.success(request, _("Lines moved"))
|
||||||
|
# Final confirmation
|
||||||
|
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_lines(modeladmin, request, queryset):
|
||||||
|
# same as move, but changing action behaviour
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def delete_lines(modeladmin, request, queryset):
|
||||||
|
pass
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf.urls import patterns, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
@ -12,8 +13,7 @@ from orchestra.admin.utils import admin_date, insertattr
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin, AccountAdmin
|
from orchestra.apps.accounts.admin import AccountAdminMixin, AccountAdmin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from . import settings
|
from . import settings, actions
|
||||||
from .actions import download_bills, view_bill, close_bills, send_bills, validate_contact
|
|
||||||
from .filters import BillTypeListFilter, HasBillContactListFilter
|
from .filters import BillTypeListFilter, HasBillContactListFilter
|
||||||
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
|
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
|
||||||
|
|
||||||
|
@ -84,6 +84,36 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BillLineManagerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('description', 'rate', 'quantity', 'tax', 'subtotal')
|
||||||
|
actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qset = super(BillLineManagerAdmin, self).get_queryset(request)
|
||||||
|
return qset.filter(bill_id__in=self.bill_ids)
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
GET = request.GET.copy()
|
||||||
|
bill_ids = GET.pop('bill_ids', ['0'])[0]
|
||||||
|
request.GET = GET
|
||||||
|
bill_ids = [int(id) for id in bill_ids.split(',')]
|
||||||
|
self.bill_ids = bill_ids
|
||||||
|
if not bill_ids:
|
||||||
|
return
|
||||||
|
elif len(bill_ids) > 1:
|
||||||
|
title = _("Manage bill lines of multiple bills.")
|
||||||
|
else:
|
||||||
|
bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],))
|
||||||
|
bill = Bill.objects.get(pk=bill_ids[0])
|
||||||
|
bill_link = '<a href="%s">%s</a>' % (bill_url, bill.ident)
|
||||||
|
title = mark_safe(_("Manage %s bill lines.") % bill_link)
|
||||||
|
context = {
|
||||||
|
'title': title,
|
||||||
|
}
|
||||||
|
context.update(extra_context or {})
|
||||||
|
return super(BillLineManagerAdmin, self).changelist_view(request, context)
|
||||||
|
|
||||||
|
|
||||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'number', 'type_link', 'account_link', 'created_on_display',
|
'number', 'type_link', 'account_link', 'created_on_display',
|
||||||
|
@ -101,8 +131,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
'fields': ('html',),
|
'fields': ('html',),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
actions = [download_bills, close_bills, send_bills]
|
change_view_actions = [
|
||||||
change_view_actions = [view_bill, download_bills, send_bills, close_bills]
|
actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills
|
||||||
|
]
|
||||||
|
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
|
||||||
change_readonly_fields = ('account_link', 'type', 'is_open')
|
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||||
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
||||||
inlines = [BillLineInline, ClosedBillLineInline]
|
inlines = [BillLineInline, ClosedBillLineInline]
|
||||||
|
@ -144,6 +176,17 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
display_payment_state.allow_tags = True
|
display_payment_state.allow_tags = True
|
||||||
display_payment_state.short_description = _("Payment")
|
display_payment_state.short_description = _("Payment")
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
""" Hook bill lines management URLs on bill admin """
|
||||||
|
urls = super(BillAdmin, self).get_urls()
|
||||||
|
admin_site = self.admin_site
|
||||||
|
extra_urls = patterns("",
|
||||||
|
url("^manage-lines/$",
|
||||||
|
admin_site.admin_view(BillLineManagerAdmin(BillLine, admin_site).changelist_view),
|
||||||
|
name='bills_bill_manage_lines'),
|
||||||
|
)
|
||||||
|
return extra_urls + urls
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = super(BillAdmin, self).get_readonly_fields(request, obj)
|
fields = super(BillAdmin, self).get_readonly_fields(request, obj)
|
||||||
if obj and not obj.is_open:
|
if obj and not obj.is_open:
|
||||||
|
@ -187,7 +230,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
def change_view(self, request, object_id, **kwargs):
|
def change_view(self, request, object_id, **kwargs):
|
||||||
# TODO raise404, here and everywhere
|
# TODO raise404, here and everywhere
|
||||||
bill = self.get_object(request, unquote(object_id))
|
bill = self.get_object(request, unquote(object_id))
|
||||||
validate_contact(request, bill, error=False)
|
actions.validate_contact(request, bill, error=False)
|
||||||
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
|
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
346
orchestra/apps/bills/locale/ca/LC_MESSAGES/django.po
Normal file
346
orchestra/apps/bills/locale/ca/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2015-03-29 10:17+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"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: actions.py:35
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Blod"
|
||||||
|
|
||||||
|
#: actions.py:45
|
||||||
|
msgid "View"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:53
|
||||||
|
msgid "Selected bills should be in open state"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:71
|
||||||
|
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\">%i related transactions</a> have been created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:87
|
||||||
|
msgid "Are you sure about closing the following bills?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:88
|
||||||
|
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
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:112
|
||||||
|
msgid "Resend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:129 models.py:308
|
||||||
|
msgid "Not enough information stored for undoing"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:132 models.py:310
|
||||||
|
msgid "Dates don't match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:147
|
||||||
|
msgid "Can not move lines which are not in open state."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:152
|
||||||
|
msgid "Can not move lines from different accounts"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:160
|
||||||
|
msgid "Target account different than lines account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: actions.py:167
|
||||||
|
msgid "Lines moved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:41 forms.py:12
|
||||||
|
msgid "Total"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:69
|
||||||
|
msgid "Description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:77
|
||||||
|
msgid "Subtotal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:104
|
||||||
|
msgid "Manage bill lines of multiple bills."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:109
|
||||||
|
#, python-format
|
||||||
|
msgid "Manage %s bill lines."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:129
|
||||||
|
msgid "Raw"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:147
|
||||||
|
msgid "lines"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:152
|
||||||
|
msgid "total"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:160 models.py:85 models.py:339
|
||||||
|
msgid "type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin.py:177
|
||||||
|
msgid "Payment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:17
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:18 models.py:75
|
||||||
|
msgid "Invoice"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:19 models.py:76
|
||||||
|
msgid "Amendment invoice"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:20 models.py:77
|
||||||
|
msgid "Fee"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:21
|
||||||
|
msgid "Amendment fee"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:22
|
||||||
|
msgid "Pro-forma"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:42
|
||||||
|
msgid "has bill contact"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:47
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: filters.py:48
|
||||||
|
msgid "No"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:9
|
||||||
|
msgid "Number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:11
|
||||||
|
msgid "Account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:13
|
||||||
|
msgid "Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms.py:15
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: helpers.py:10
|
||||||
|
msgid ""
|
||||||
|
"{relation} account \"{account}\" does not have a declared invoice contact. "
|
||||||
|
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: helpers.py:17
|
||||||
|
msgid "Related"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: helpers.py:24
|
||||||
|
msgid "Main"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:20 models.py:83
|
||||||
|
msgid "account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:22
|
||||||
|
msgid "name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:23
|
||||||
|
msgid "Account full name will be used when left blank."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:24
|
||||||
|
msgid "address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:25
|
||||||
|
msgid "city"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:27
|
||||||
|
msgid "zip code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:28
|
||||||
|
msgid "Enter a valid zipcode."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:29
|
||||||
|
msgid "country"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:32
|
||||||
|
msgid "VAT number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:64
|
||||||
|
msgid "Paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:65
|
||||||
|
msgid "Pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:66
|
||||||
|
msgid "Bad debt"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:78
|
||||||
|
msgid "Amendment Fee"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:79
|
||||||
|
msgid "Pro forma"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:82
|
||||||
|
msgid "number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:86
|
||||||
|
msgid "created on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:87
|
||||||
|
msgid "closed on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:88
|
||||||
|
msgid "open"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:89
|
||||||
|
msgid "sent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:90
|
||||||
|
msgid "due on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:91
|
||||||
|
msgid "updated on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:93
|
||||||
|
msgid "comments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:94
|
||||||
|
msgid "HTML"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:270
|
||||||
|
msgid "bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:271 models.py:336
|
||||||
|
msgid "description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:272
|
||||||
|
msgid "rate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:273
|
||||||
|
msgid "quantity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:274
|
||||||
|
msgid "subtotal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:275
|
||||||
|
msgid "tax"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:281
|
||||||
|
msgid "Informative link back to the order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:282
|
||||||
|
msgid "order billed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:283
|
||||||
|
msgid "order billed until"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:284
|
||||||
|
msgid "created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:286
|
||||||
|
msgid "amended line"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:329
|
||||||
|
msgid "Volume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:330
|
||||||
|
msgid "Compensation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:331
|
||||||
|
msgid "Other"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:335
|
||||||
|
msgid "bill line"
|
||||||
|
msgstr ""
|
|
@ -274,8 +274,11 @@ class BillLine(models.Model):
|
||||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||||
tax = models.PositiveIntegerField(_("tax"))
|
tax = models.PositiveIntegerField(_("tax"))
|
||||||
# Undo
|
# Undo
|
||||||
|
# initial = models.DateTimeField(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"))
|
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)
|
||||||
order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
|
order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
|
||||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||||
|
|
|
@ -30,34 +30,33 @@ class BatchDomainCreationAdminForm(forms.ModelForm):
|
||||||
return target
|
return target
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
""" inherit related top domain account, when exists """
|
""" inherit related parent domain account, when exists """
|
||||||
cleaned_data = super(BatchDomainCreationAdminForm, self).clean()
|
cleaned_data = super(BatchDomainCreationAdminForm, self).clean()
|
||||||
if not cleaned_data['account']:
|
if not cleaned_data['account']:
|
||||||
account = None
|
account = None
|
||||||
for name in [cleaned_data['name']] + self.extra_names:
|
for name in [cleaned_data['name']] + self.extra_names:
|
||||||
domain = Domain(name=name)
|
domain = Domain(name=name)
|
||||||
top = domain.get_top()
|
parent = domain.get_parent()
|
||||||
if not top:
|
if not parent:
|
||||||
# Fake an account to make django validation happy
|
# Fake an account to make django validation happy
|
||||||
account_model = self.fields['account']._queryset.model
|
account_model = self.fields['account']._queryset.model
|
||||||
cleaned_data['account'] = account_model()
|
cleaned_data['account'] = account_model()
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'account': _("An account should be provided for top domain names."),
|
'account': _("An account should be provided for top domain names."),
|
||||||
})
|
})
|
||||||
elif account and top.account != account:
|
elif account and parent.account != account:
|
||||||
# Fake an account to make django validation happy
|
# Fake an account to make django validation happy
|
||||||
account_model = self.fields['account']._queryset.model
|
account_model = self.fields['account']._queryset.model
|
||||||
cleaned_data['account'] = account_model()
|
cleaned_data['account'] = account_model()
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'account': _("Provided domain names belong to different accounts."),
|
'account': _("Provided domain names belong to different accounts."),
|
||||||
})
|
})
|
||||||
account = top.account
|
account = parent.account
|
||||||
cleaned_data['account'] = account
|
cleaned_data['account'] = account
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class RecordInlineFormSet(forms.models.BaseInlineFormSet):
|
class RecordInlineFormSet(forms.models.BaseInlineFormSet):
|
||||||
# TODO
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
""" Checks if everything is consistent """
|
""" Checks if everything is consistent """
|
||||||
if any(self.errors):
|
if any(self.errors):
|
||||||
|
|
|
@ -17,7 +17,7 @@ def domain_for_validation(instance, records):
|
||||||
|
|
||||||
if not domain.pk:
|
if not domain.pk:
|
||||||
# top domain lookup for new domains
|
# top domain lookup for new domains
|
||||||
domain.top = domain.get_top()
|
domain.top = domain.get_parent(top=True)
|
||||||
if domain.top:
|
if domain.top:
|
||||||
# is a subdomain
|
# is a subdomain
|
||||||
subdomains = [sub for sub in domain.top.subdomains.all() if sub.pk != domain.pk]
|
subdomains = [sub for sub in domain.top.subdomains.all() if sub.pk != domain.pk]
|
||||||
|
|
|
@ -27,15 +27,18 @@ class Domain(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_top_domain(cls, name):
|
def get_parent_domain(cls, name, top=False):
|
||||||
|
""" get the next domain on the chain """
|
||||||
split = name.split('.')
|
split = name.split('.')
|
||||||
top = None
|
parent = None
|
||||||
for i in range(1, len(split)-1):
|
for i in range(1, len(split)-1):
|
||||||
name = '.'.join(split[i:])
|
name = '.'.join(split[i:])
|
||||||
domain = Domain.objects.filter(name=name)
|
domain = Domain.objects.filter(name=name)
|
||||||
if domain:
|
if domain:
|
||||||
top = domain.get()
|
parent = domain.get()
|
||||||
return top
|
if not top:
|
||||||
|
return parent
|
||||||
|
return parent
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def origin(self):
|
def origin(self):
|
||||||
|
@ -57,7 +60,7 @@ class Domain(models.Model):
|
||||||
""" create top relation """
|
""" create top relation """
|
||||||
update = False
|
update = False
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
top = self.get_top()
|
top = self.get_parent(top=True)
|
||||||
if top:
|
if top:
|
||||||
self.top = top
|
self.top = top
|
||||||
self.account_id = self.account_id or top.account_id
|
self.account_id = self.account_id or top.account_id
|
||||||
|
@ -90,8 +93,8 @@ class Domain(models.Model):
|
||||||
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
||||||
return self.origin.subdomain_set.all().prefetch_related('records')
|
return self.origin.subdomain_set.all().prefetch_related('records')
|
||||||
|
|
||||||
def get_top(self):
|
def get_parent(self, top=False):
|
||||||
return type(self).get_top_domain(self.name)
|
return type(self).get_parent_domain(self.name, top=top)
|
||||||
|
|
||||||
def render_zone(self):
|
def render_zone(self):
|
||||||
origin = self.origin
|
origin = self.origin
|
||||||
|
|
|
@ -30,7 +30,7 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
|
||||||
def clean_name(self, attrs, source):
|
def clean_name(self, attrs, source):
|
||||||
""" prevent users creating subdomains of other users domains """
|
""" prevent users creating subdomains of other users domains """
|
||||||
name = attrs[source]
|
name = attrs[source]
|
||||||
top = Domain.get_top_domain(name)
|
top = Domain.get_parent_domain(name)
|
||||||
if top and top.account != self.account:
|
if top and top.account != self.account:
|
||||||
raise ValidationError(_("Can not create subdomains of other users domains"))
|
raise ValidationError(_("Can not create subdomains of other users domains"))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.contacts import settings as contacts_settings
|
from orchestra.apps.contacts import settings as contacts_settings
|
||||||
from orchestra.apps.contacts.models import Contact
|
from orchestra.apps.contacts.models import Contact
|
||||||
|
from orchestra.core.translations import ModelTranslation
|
||||||
from orchestra.models.fields import MultiSelectField
|
from orchestra.models.fields import MultiSelectField
|
||||||
from orchestra.utils import send_email_template
|
from orchestra.utils import send_email_template
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ from . import settings
|
||||||
|
|
||||||
class Queue(models.Model):
|
class Queue(models.Model):
|
||||||
name = models.CharField(_("name"), max_length=128, unique=True)
|
name = models.CharField(_("name"), max_length=128, unique=True)
|
||||||
|
verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True)
|
||||||
default = models.BooleanField(_("default"), default=False)
|
default = models.BooleanField(_("default"), default=False)
|
||||||
notify = MultiSelectField(_("notify"), max_length=256, blank=True,
|
notify = MultiSelectField(_("notify"), max_length=256, blank=True,
|
||||||
choices=Contact.EMAIL_USAGES,
|
choices=Contact.EMAIL_USAGES,
|
||||||
|
@ -19,7 +21,7 @@ class Queue(models.Model):
|
||||||
help_text=_("Contacts to notify by email"))
|
help_text=_("Contacts to notify by email"))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.verbose_name or self.name
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" mark as default queue if needed """
|
""" mark as default queue if needed """
|
||||||
|
@ -190,3 +192,6 @@ class TicketTracker(models.Model):
|
||||||
unique_together = (
|
unique_together = (
|
||||||
('ticket', 'user'),
|
('ticket', 'user'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ModelTranslation.register(Queue, ('verbose_name',))
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
from orchestra.core.translations import ModelTranslation
|
||||||
from orchestra.core.validators import validate_name
|
from orchestra.core.validators import validate_name
|
||||||
from orchestra.models.fields import NullableCharField
|
from orchestra.models.fields import NullableCharField
|
||||||
|
|
||||||
|
@ -74,3 +75,5 @@ class Miscellaneous(models.Model):
|
||||||
|
|
||||||
|
|
||||||
services.register(Miscellaneous)
|
services.register(Miscellaneous)
|
||||||
|
|
||||||
|
ModelTranslation.register(MiscService, ('verbose_name',))
|
||||||
|
|
|
@ -40,7 +40,7 @@ class RouteAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def display_model(self, route):
|
def display_model(self, route):
|
||||||
try:
|
try:
|
||||||
return escape(route.backend_class().model)
|
return escape(route.backend_class.model)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||||
display_model.short_description = _("model")
|
display_model.short_description = _("model")
|
||||||
|
@ -48,7 +48,7 @@ class RouteAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def display_actions(self, route):
|
def display_actions(self, route):
|
||||||
try:
|
try:
|
||||||
return '<br>'.join(route.backend_class().get_actions())
|
return '<br>'.join(route.backend_class.get_actions())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||||
display_actions.short_description = _("actions")
|
display_actions.short_description = _("actions")
|
||||||
|
|
|
@ -186,11 +186,9 @@ class Route(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "%s@%s" % (self.backend, self.host)
|
return "%s@%s" % (self.backend, self.host)
|
||||||
|
|
||||||
# def clean(self):
|
@property
|
||||||
# backend, method = self.get_backend_class(), self.get_method_class()
|
def backend_class(self):
|
||||||
# if not backend.type in method.types:
|
return ServiceBackend.get_backend(self.backend)
|
||||||
# msg = _("%s backend is not compatible with %s method")
|
|
||||||
# raise ValidationError(msg % (self.backend, self.method)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_servers(cls, operation, **kwargs):
|
def get_servers(cls, operation, **kwargs):
|
||||||
|
@ -215,6 +213,22 @@ class Route(models.Model):
|
||||||
servers.append(route.host)
|
servers.append(route.host)
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not self.match:
|
||||||
|
self.match = 'True'
|
||||||
|
if self.backend:
|
||||||
|
backend_model = self.backend_class.model
|
||||||
|
try:
|
||||||
|
obj = backend_model.objects.all()[0]
|
||||||
|
except IndexError:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
bool(self.matches(obj))
|
||||||
|
except Exception, exception:
|
||||||
|
name = type(exception).__name__
|
||||||
|
message = exception.message
|
||||||
|
raise ValidationError(': '.join((name, message)))
|
||||||
|
|
||||||
def matches(self, instance):
|
def matches(self, instance):
|
||||||
safe_locals = {
|
safe_locals = {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
|
@ -223,15 +237,6 @@ class Route(models.Model):
|
||||||
}
|
}
|
||||||
return eval(self.match, safe_locals)
|
return eval(self.match, safe_locals)
|
||||||
|
|
||||||
def backend_class(self):
|
|
||||||
return ServiceBackend.get_backend(self.backend)
|
|
||||||
|
|
||||||
# def method_class(self):
|
|
||||||
# for method in MethodBackend.get_backends():
|
|
||||||
# if method.get_name() == self.method:
|
|
||||||
# return method
|
|
||||||
# raise ValueError('This method is not registered')
|
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
|
@ -4,7 +4,7 @@ from orchestra.apps.accounts.models import Account
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
|
||||||
|
|
||||||
def get_related_objects(origin, max_depth=2):
|
def get_related_object(origin, max_depth=2):
|
||||||
"""
|
"""
|
||||||
Introspects origin object and return the first related service object
|
Introspects origin object and return the first related service object
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ class OrderQuerySet(models.QuerySet):
|
||||||
order.old_billed_until = order.billed_until
|
order.old_billed_until = order.billed_until
|
||||||
lines = service.handler.generate_bill_lines(orders, account, **options)
|
lines = service.handler.generate_bill_lines(orders, account, **options)
|
||||||
bill_lines.extend(lines)
|
bill_lines.extend(lines)
|
||||||
# TODO make this consistent always returning the same fucking objects
|
# TODO make this consistent always returning the same fucking types
|
||||||
if commit:
|
if commit:
|
||||||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||||
else:
|
else:
|
||||||
|
@ -257,7 +257,8 @@ class MetricStorage(models.Model):
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
cls.objects.create(order=order, value=value, updated_on=now)
|
cls.objects.create(order=order, value=value, updated_on=now)
|
||||||
else:
|
else:
|
||||||
if metric.value != value:
|
threshold = decimal.Decimal(settings.ORDERS_METRIC_THRESHOLD)
|
||||||
|
if metric.value*(1+threshold) > value or metric.value*threshold < value:
|
||||||
cls.objects.create(order=order, value=value, updated_on=now)
|
cls.objects.create(order=order, value=value, updated_on=now)
|
||||||
else:
|
else:
|
||||||
metric.updated_on = now
|
metric.updated_on = now
|
||||||
|
@ -276,7 +277,7 @@ def cancel_orders(sender, **kwargs):
|
||||||
for order in Order.objects.by_object(instance).active():
|
for order in Order.objects.by_object(instance).active():
|
||||||
order.cancel()
|
order.cancel()
|
||||||
elif not hasattr(instance, 'account'):
|
elif not hasattr(instance, 'account'):
|
||||||
related = helpers.get_related_objects(instance)
|
related = helpers.get_related_object(instance)
|
||||||
if related and related != instance:
|
if related and related != instance:
|
||||||
Order.update_orders(related)
|
Order.update_orders(related)
|
||||||
|
|
||||||
|
@ -287,6 +288,6 @@ def update_orders(sender, **kwargs):
|
||||||
if type(instance) in services:
|
if type(instance) in services:
|
||||||
Order.update_orders(instance)
|
Order.update_orders(instance)
|
||||||
elif not hasattr(instance, 'account'):
|
elif not hasattr(instance, 'account'):
|
||||||
related = helpers.get_related_objects(instance)
|
related = helpers.get_related_object(instance)
|
||||||
if related and related != instance:
|
if related and related != instance:
|
||||||
Order.update_orders(related)
|
Order.update_orders(related)
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
# Pluggable backend for bill generation.
|
||||||
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
||||||
'orchestra.apps.orders.billing.BillsBackend')
|
'orchestra.apps.orders.billing.BillsBackend')
|
||||||
|
|
||||||
|
|
||||||
|
# Pluggable service class
|
||||||
ORDERS_SERVICE_MODEL = getattr(settings, 'ORDERS_SERVICE_MODEL', 'services.Service')
|
ORDERS_SERVICE_MODEL = getattr(settings, 'ORDERS_SERVICE_MODEL', 'services.Service')
|
||||||
|
|
||||||
|
|
||||||
|
# Prevent inspecting these apps for service accounting
|
||||||
ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
|
ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
|
||||||
'orders',
|
'orders',
|
||||||
'admin',
|
'admin',
|
||||||
|
@ -19,3 +22,8 @@ ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
|
||||||
'bills',
|
'bills',
|
||||||
'services',
|
'services',
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
# Only account for significative changes
|
||||||
|
# metric_storage new value: lastvalue*(1+threshold) > currentvalue or lastvalue*threshold < currentvalue
|
||||||
|
ORDERS_METRIC_THRESHOLD = getattr(settings, 'ORDERS_METRIC_THRESHOLD', 0.4)
|
||||||
|
|
|
@ -21,7 +21,7 @@ class PaymentSource(models.Model):
|
||||||
related_name='paymentsources')
|
related_name='paymentsources')
|
||||||
method = models.CharField(_("method"), max_length=32,
|
method = models.CharField(_("method"), max_length=32,
|
||||||
choices=PaymentMethod.get_plugin_choices())
|
choices=PaymentMethod.get_plugin_choices())
|
||||||
data = JSONField(_("data"))
|
data = JSONField(_("data"), default={})
|
||||||
is_active = models.BooleanField(_("active"), default=True)
|
is_active = models.BooleanField(_("active"), default=True)
|
||||||
|
|
||||||
objects = PaymentSourcesQueryset.as_manager()
|
objects = PaymentSourcesQueryset.as_manager()
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services, accounts
|
from orchestra.core import services, accounts
|
||||||
|
from orchestra.core.translations import ModelTranslation
|
||||||
from orchestra.core.validators import validate_name
|
from orchestra.core.validators import validate_name
|
||||||
from orchestra.models import queryset
|
from orchestra.models import queryset
|
||||||
|
|
||||||
|
@ -89,3 +90,5 @@ class Rate(models.Model):
|
||||||
|
|
||||||
accounts.register(ContractedPlan)
|
accounts.register(ContractedPlan)
|
||||||
services.register(ContractedPlan, menu=False)
|
services.register(ContractedPlan, menu=False)
|
||||||
|
|
||||||
|
ModelTranslation.register(Plan, ('verbose_name',))
|
||||||
|
|
|
@ -15,7 +15,7 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
default_route_match = "saas.service == 'phplist'"
|
default_route_match = "saas.service == 'phplist'"
|
||||||
block = True
|
block = True
|
||||||
|
|
||||||
def initialize_database(self, saas, server):
|
def _save(self, saas, server):
|
||||||
base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
|
base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
|
||||||
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
|
||||||
|
@ -25,21 +25,21 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
if install:
|
if install:
|
||||||
if not hasattr(saas, 'password'):
|
if not hasattr(saas, 'password'):
|
||||||
raise RuntimeError("Password is missing")
|
raise RuntimeError("Password is missing")
|
||||||
install = install.groups()[0]
|
install_path = install.groups()[0]
|
||||||
install_link = admin_link + install[1:]
|
install_link = admin_link + install_path[1:]
|
||||||
post = {
|
post = {
|
||||||
'adminname': saas.name,
|
'adminname': saas.name,
|
||||||
'orgname': saas.account.username,
|
'orgname': saas.account.username,
|
||||||
'adminemail': saas.account.username,
|
'adminemail': saas.account.username,
|
||||||
'adminpassword': saas.password,
|
'adminpassword': saas.password,
|
||||||
}
|
}
|
||||||
print json.dumps(post, indent=4)
|
|
||||||
response = requests.post(install_link, data=post)
|
response = requests.post(install_link, data=post)
|
||||||
print response.content
|
print response.content
|
||||||
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)
|
||||||
elif hasattr(saas, 'password'):
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError("Change password not implemented")
|
||||||
|
|
||||||
def save(self, saas):
|
def save(self, saas):
|
||||||
self.append(self.initialize_database, saas)
|
if hasattr(saas, 'password'):
|
||||||
|
self.append(self._save, saas)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.module_loading import autodiscover_modules
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import caches, validators
|
from orchestra.core import caches, validators
|
||||||
|
from orchestra.core.translations import ModelTranslation
|
||||||
from orchestra.core.validators import validate_name
|
from orchestra.core.validators import validate_name
|
||||||
from orchestra.models import queryset
|
from orchestra.models import queryset
|
||||||
|
|
||||||
|
@ -240,3 +241,6 @@ class Service(models.Model):
|
||||||
for instance in related_model.objects.all().select_related('account'):
|
for instance in related_model.objects.all().select_related('account'):
|
||||||
updates += order_model.update_orders(instance, service=self, commit=commit)
|
updates += order_model.update_orders(instance, service=self, commit=commit)
|
||||||
return updates
|
return updates
|
||||||
|
|
||||||
|
|
||||||
|
ModelTranslation.register(Service, ('description',))
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||||
|
from orchestra.apps.plans.models import Plan
|
||||||
from orchestra.utils.tests import random_ascii
|
from orchestra.utils.tests import random_ascii
|
||||||
|
|
||||||
from ...models import Service, Plan
|
from ...models import Service
|
||||||
|
|
||||||
from . import BaseBillingTest
|
from . import BaseBillingTest
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ class DomainBillingTest(BaseBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric='',
|
metric='',
|
||||||
pricing_period=Service.BILLING_PERIOD,
|
pricing_period=Service.BILLING_PERIOD,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.NOTHING,
|
on_cancel=Service.NOTHING,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -25,7 +25,7 @@ class FTPBillingTest(BaseBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric='',
|
metric='',
|
||||||
pricing_period=Service.NEVER,
|
pricing_period=Service.NEVER,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.COMPENSATE,
|
on_cancel=Service.COMPENSATE,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||||
|
from orchestra.apps.plans.models import Plan
|
||||||
from orchestra.utils.tests import random_ascii
|
from orchestra.utils.tests import random_ascii
|
||||||
|
|
||||||
from ...models import Service, Plan
|
from ...models import Service
|
||||||
|
|
||||||
from . import BaseBillingTest
|
from . import BaseBillingTest
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ class JobBillingTest(BaseBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric='miscellaneous.amount',
|
metric='miscellaneous.amount',
|
||||||
pricing_period=Service.BILLING_PERIOD,
|
pricing_period=Service.BILLING_PERIOD,
|
||||||
rate_algorithm=Service.MATCH_PRICE,
|
rate_algorithm='MATCH_PRICE',
|
||||||
on_cancel=Service.NOTHING,
|
on_cancel=Service.NOTHING,
|
||||||
payment_style=Service.POSTPAY,
|
payment_style=Service.POSTPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -4,10 +4,11 @@ from django.utils import timezone
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from orchestra.apps.mailboxes.models import Mailbox
|
from orchestra.apps.mailboxes.models import Mailbox
|
||||||
|
from orchestra.apps.plans.models import Plan
|
||||||
from orchestra.apps.resources.models import Resource, ResourceData
|
from orchestra.apps.resources.models import Resource, ResourceData
|
||||||
from orchestra.utils.tests import random_ascii
|
from orchestra.utils.tests import random_ascii
|
||||||
|
|
||||||
from ...models import Service, Plan
|
from ...models import Service
|
||||||
|
|
||||||
from . import BaseBillingTest
|
from . import BaseBillingTest
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ class MailboxBillingTest(BaseBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric='',
|
metric='',
|
||||||
pricing_period=Service.NEVER,
|
pricing_period=Service.NEVER,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.COMPENSATE,
|
on_cancel=Service.COMPENSATE,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
@ -44,7 +45,7 @@ class MailboxBillingTest(BaseBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric='max((mailbox.resources.disk.allocated or 0) -1, 0)',
|
metric='max((mailbox.resources.disk.allocated or 0) -1, 0)',
|
||||||
pricing_period=Service.NEVER,
|
pricing_period=Service.NEVER,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.DISCOUNT,
|
on_cancel=Service.DISCOUNT,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from ...models import Service, Plan, ContractedPlan
|
from orchestra.apps.plans.models import Plan, ContractedPlan
|
||||||
|
|
||||||
|
from ...models import Service
|
||||||
|
|
||||||
from . import BaseBillingTest
|
from . import BaseBillingTest
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@ class PlanBillingTest(BaseBillingTest):
|
||||||
is_fee=True,
|
is_fee=True,
|
||||||
metric='',
|
metric='',
|
||||||
pricing_period=Service.BILLING_PERIOD,
|
pricing_period=Service.BILLING_PERIOD,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.DISCOUNT,
|
on_cancel=Service.DISCOUNT,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -5,9 +5,10 @@ from freezegun import freeze_time
|
||||||
|
|
||||||
from orchestra.apps.accounts.models import Account
|
from orchestra.apps.accounts.models import Account
|
||||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||||
|
from orchestra.apps.plans.models import Plan
|
||||||
from orchestra.apps.resources.models import Resource, ResourceData, MonitorData
|
from orchestra.apps.resources.models import Resource, ResourceData, MonitorData
|
||||||
|
|
||||||
from ...models import Service, Plan
|
from ...models import Service
|
||||||
|
|
||||||
from . import BaseBillingTest
|
from . import BaseBillingTest
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ class BaseTrafficBillingTest(BaseBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric=self.TRAFFIC_METRIC,
|
metric=self.TRAFFIC_METRIC,
|
||||||
pricing_period=Service.BILLING_PERIOD,
|
pricing_period=Service.BILLING_PERIOD,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.NOTHING,
|
on_cancel=Service.NOTHING,
|
||||||
payment_style=Service.POSTPAY,
|
payment_style=Service.POSTPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
@ -107,7 +108,7 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric="miscellaneous.amount",
|
metric="miscellaneous.amount",
|
||||||
pricing_period=Service.NEVER,
|
pricing_period=Service.NEVER,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.NOTHING,
|
on_cancel=Service.NOTHING,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -41,7 +41,7 @@ class HandlerTests(BaseTestCase):
|
||||||
is_fee=False,
|
is_fee=False,
|
||||||
metric='',
|
metric='',
|
||||||
pricing_period=Service.NEVER,
|
pricing_period=Service.NEVER,
|
||||||
rate_algorithm=Service.STEP_PRICE,
|
rate_algorithm='STEP_PRICE',
|
||||||
on_cancel=Service.DISCOUNT,
|
on_cancel=Service.DISCOUNT,
|
||||||
payment_style=Service.PREPAY,
|
payment_style=Service.PREPAY,
|
||||||
tax=0,
|
tax=0,
|
||||||
|
|
|
@ -208,10 +208,7 @@ class Exim4Traffic(ServiceMonitor):
|
||||||
with open(mainlog, 'r') as mainlog:
|
with open(mainlog, 'r') as mainlog:
|
||||||
for line in mainlog.readlines():
|
for line in mainlog.readlines():
|
||||||
if ' <= ' in line and 'P=local' in line:
|
if ' <= ' in line and 'P=local' in line:
|
||||||
username = user_regex.search(line)
|
username = user_regex.search(line).groups()[0]
|
||||||
if not username:
|
|
||||||
continue
|
|
||||||
username = username.groups()[0]
|
|
||||||
try:
|
try:
|
||||||
sender = users[username]
|
sender = users[username]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -299,7 +296,7 @@ class FTPTraffic(ServiceMonitor):
|
||||||
users[username] = [ini_date, object_id, 0]
|
users[username] = [ini_date, object_id, 0]
|
||||||
|
|
||||||
def monitor(users, end_date, months, vsftplogs):
|
def monitor(users, end_date, months, vsftplogs):
|
||||||
user_regex = re.compile(r'\] \[([^ ]+)\] OK ')
|
user_regex = re.compile(r'\] \[([^ ]+)\] (OK|FAIL) ')
|
||||||
bytes_regex = re.compile(r', ([0-9]+) bytes, ')
|
bytes_regex = re.compile(r', ([0-9]+) bytes, ')
|
||||||
for vsftplog in vsftplogs:
|
for vsftplog in vsftplogs:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -97,7 +97,7 @@ class Apache2Backend(ServiceController):
|
||||||
def delete(self, site):
|
def delete(self, site):
|
||||||
context = self.get_context(site)
|
context = self.get_context(site)
|
||||||
self.append("a2dissite %(site_unique_name)s.conf && UPDATED=1" % context)
|
self.append("a2dissite %(site_unique_name)s.conf && UPDATED=1" % context)
|
||||||
self.append("rm -fr %(sites_available)s" % context)
|
self.append("rm -f %(sites_available)s" % context)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
""" reload Apache2 if necessary """
|
""" reload Apache2 if necessary """
|
||||||
|
|
|
@ -11,7 +11,6 @@ from orchestra.utils.functional import cached
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .directives import SiteDirective
|
from .directives import SiteDirective
|
||||||
from .utils import normurlpath
|
|
||||||
|
|
||||||
|
|
||||||
class Website(models.Model):
|
class Website(models.Model):
|
||||||
|
@ -141,8 +140,8 @@ class Content(models.Model):
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# TODO do it on the field?
|
if not self.path:
|
||||||
self.path = normurlpath(self.path)
|
self.path = '/'
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
domain = self.website.domains.first()
|
domain = self.website.domains.first()
|
||||||
|
|
|
@ -131,9 +131,10 @@ function install_requirements () {
|
||||||
libxslt1-dev \
|
libxslt1-dev \
|
||||||
wkhtmltopdf \
|
wkhtmltopdf \
|
||||||
xvfb \
|
xvfb \
|
||||||
ca-certificates"
|
ca-certificates \
|
||||||
|
gettext"
|
||||||
|
|
||||||
PIP="django==1.7.1 \
|
PIP="django==1.7.7 \
|
||||||
django-celery-email==1.0.4 \
|
django-celery-email==1.0.4 \
|
||||||
django-fluent-dashboard==0.3.5 \
|
django-fluent-dashboard==0.3.5 \
|
||||||
https://bitbucket.org/izi/django-admin-tools/get/a0abfffd76a0.zip \
|
https://bitbucket.org/izi/django-admin-tools/get/a0abfffd76a0.zip \
|
||||||
|
@ -158,7 +159,8 @@ function install_requirements () {
|
||||||
requests \
|
requests \
|
||||||
phonenumbers \
|
phonenumbers \
|
||||||
django-countries \
|
django-countries \
|
||||||
django-localflavor"
|
django-localflavor \
|
||||||
|
pip==6.0.8"
|
||||||
|
|
||||||
if $testing; then
|
if $testing; then
|
||||||
APT="${APT} \
|
APT="${APT} \
|
||||||
|
|
0
orchestra/conf/project_template/locale/.gitignore
vendored
Normal file
0
orchestra/conf/project_template/locale/.gitignore
vendored
Normal file
|
@ -55,6 +55,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
|
||||||
|
|
||||||
|
# Path used for database translations files
|
||||||
|
LOCALE_PATHS = (
|
||||||
|
os.path.join(BASE_DIR, 'locale'),
|
||||||
|
)
|
||||||
|
|
||||||
# EMAIL_HOST = 'smtp.yourhost.eu'
|
# EMAIL_HOST = 'smtp.yourhost.eu'
|
||||||
# EMAIL_PORT = ''
|
# EMAIL_PORT = ''
|
||||||
# EMAIL_HOST_USER = ''
|
# EMAIL_HOST_USER = ''
|
||||||
|
|
13
orchestra/core/translations.py
Normal file
13
orchestra/core/translations.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class ModelTranslation(object):
|
||||||
|
"""
|
||||||
|
Collects all model fields that would be translated
|
||||||
|
|
||||||
|
using 'makemessages --domain database' management command
|
||||||
|
"""
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, model, fields):
|
||||||
|
if model in cls._registry:
|
||||||
|
raise ValueError("Model %s already registered." % model.__name__)
|
||||||
|
cls._registry[model] = fields
|
53
orchestra/management/commands/makemessages.py
Normal file
53
orchestra/management/commands/makemessages.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.management.commands import makemessages
|
||||||
|
|
||||||
|
from orchestra.core.translations import ModelTranslation
|
||||||
|
from orchestra.utils.paths import get_site_root
|
||||||
|
|
||||||
|
|
||||||
|
class Command(makemessages.Command):
|
||||||
|
""" Provides database translations support """
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
do_database = os.getcwd() == get_site_root()
|
||||||
|
self.generated_database_files = []
|
||||||
|
if do_database:
|
||||||
|
self.project_locale_path = get_site_root()
|
||||||
|
self.generate_database_files()
|
||||||
|
super(Command, self).handle(*args, **options)
|
||||||
|
self.remove_database_files()
|
||||||
|
|
||||||
|
def get_contents(self):
|
||||||
|
for model, fields in ModelTranslation._registry.iteritems():
|
||||||
|
contents = []
|
||||||
|
for field in fields:
|
||||||
|
for content in model.objects.values_list('id', field):
|
||||||
|
pk, value = content
|
||||||
|
contents.append(
|
||||||
|
(pk, u"_(u'%s')" % value)
|
||||||
|
)
|
||||||
|
yield ('_'.join((model._meta.db_table, field)), contents)
|
||||||
|
|
||||||
|
def generate_database_files(self):
|
||||||
|
""" tmp files are generated because of having a nice gettext location """
|
||||||
|
for name, contents in self.get_contents():
|
||||||
|
name = unicode(name)
|
||||||
|
maximum = None
|
||||||
|
content = {}
|
||||||
|
for pk, value in contents:
|
||||||
|
if not maximum or pk > maximum:
|
||||||
|
maximum = pk
|
||||||
|
content[pk] = value
|
||||||
|
tmpcontent = []
|
||||||
|
for ix in xrange(maximum+1):
|
||||||
|
tmpcontent.append(content.get(ix, ''))
|
||||||
|
tmpcontent = u'\n'.join(tmpcontent) + '\n'
|
||||||
|
filepath = os.path.join(self.project_locale_path, 'database_%s.sql.py' % name)
|
||||||
|
self.generated_database_files.append(filepath)
|
||||||
|
with open(filepath, 'w') as tmpfile:
|
||||||
|
tmpfile.write(tmpcontent.encode('utf-8'))
|
||||||
|
|
||||||
|
def remove_database_files(self):
|
||||||
|
for path in self.generated_database_files:
|
||||||
|
os.unlink(path)
|
Loading…
Reference in a new issue