Fixes on payments and saas domains

This commit is contained in:
Marc Aymerich 2015-09-29 12:35:22 +00:00
parent 764301555c
commit 835a4ab872
13 changed files with 86 additions and 59 deletions

View file

@ -101,16 +101,16 @@ class PaymentStateListFilter(SimpleListFilter):
elif self.value() == 'PAID': elif self.value() == 'PAID':
zeros = queryset.filter(approx_total=0, approx_total__isnull=True) zeros = queryset.filter(approx_total=0, approx_total__isnull=True)
zeros = zeros.values_list('id', flat=True) zeros = zeros.values_list('id', flat=True)
ammounts = Transaction.objects.exclude(bill_id__in=zeros).secured().group_by('bill_id') amounts = Transaction.objects.exclude(bill_id__in=zeros).secured().group_by('bill_id')
paid = [] paid = []
relevant = queryset.exclude(approx_total=0, approx_total__isnull=True, is_open=True) relevant = queryset.exclude(approx_total=0, approx_total__isnull=True, is_open=True)
for bill_id, total in relevant.values_list('id', 'approx_total'): for bill_id, total in relevant.values_list('id', 'approx_total'):
try: try:
ammount = sum([t.ammount for t in ammounts[bill_id]]) amount = sum([t.amount for t in amounts[bill_id]])
except KeyError: except KeyError:
pass pass
else: else:
if abs(total) <= abs(ammount): if abs(total) <= abs(amount):
paid.append(bill_id) paid.append(bill_id)
return queryset.filter( return queryset.filter(
Q(approx_total=0) | Q(approx_total=0) |
@ -120,8 +120,9 @@ class PaymentStateListFilter(SimpleListFilter):
elif self.value() == 'PENDING': elif self.value() == 'PENDING':
has_transaction = queryset.exclude(transactions__isnull=True) has_transaction = queryset.exclude(transactions__isnull=True)
non_rejected = has_transaction.exclude(transactions__state=Transaction.REJECTED) non_rejected = has_transaction.exclude(transactions__state=Transaction.REJECTED)
non_rejected = non_rejected.values_list('id', flat=True).distinct() paid = non_rejected.exclude(transactions__state=Transaction.SECURED)
return queryset.filter(pk__in=non_rejected) paid = paid.values_list('id', flat=True).distinct()
return queryset.filter(pk__in=paid)
elif self.value() == 'BAD_DEBT': elif self.value() == 'BAD_DEBT':
closed = queryset.filter(is_open=False).exclude(approx_total=0) closed = queryset.filter(is_open=False).exclude(approx_total=0)
return closed.filter( return closed.filter(

View file

@ -94,7 +94,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
link = '<a href="%s">%s %s</a>' % (admin_url, website.name, site_link) link = '<a href="%s">%s %s</a>' % (admin_url, website.name, site_link)
links.append(link) links.append(link)
return '<br>'.join(links) return '<br>'.join(links)
return _("No website") context = {
'title': _("View on site"),
'url': 'http://%s' % domain.name,
'image': '<img src="%s"></img>' % static('orchestra/images/view-on-site.png'),
}
site_link = '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context
return _("No website %s") % site_link
display_websites.admin_order_field = 'websites__name' display_websites.admin_order_field = 'websites__name'
display_websites.short_description = _("Websites") display_websites.short_description = _("Websites")
display_websites.allow_tags = True display_websites.allow_tags = True

View file

@ -105,7 +105,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
source_link = admin_link('source') source_link = admin_link('source')
process_link = admin_link('process', short_description=_("proc")) process_link = admin_link('process', short_description=_("proc"))
account_link = admin_link('bill__account') account_link = admin_link('bill__account')
display_state = admin_colored('state', colors=STATE_COLORS)
def get_change_view_actions(self, obj=None): def get_change_view_actions(self, obj=None):
actions = super(TransactionAdmin, self).get_change_view_actions() actions = super(TransactionAdmin, self).get_change_view_actions()
@ -121,6 +120,15 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
return [] return []
return [action for action in actions if action.__name__ not in exclude] return [action for action in actions if action.__name__ not in exclude]
def display_state(self, obj):
state = admin_colored('state', colors=STATE_COLORS)(obj)
help_text = obj.get_state_help()
state = state.replace('<span ', '<span title="%s" ' % help_text)
return state
display_state.admin_order_field = 'state'
display_state.short_description = _("State")
display_state.allow_tags = True
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
list_display = ('id', 'file_url', 'display_transactions', 'created_at') list_display = ('id', 'file_url', 'display_transactions', 'created_at')

View file

@ -17,6 +17,7 @@ class PaymentMethod(plugins.Plugin):
process_credit = False process_credit = False
due_delta = relativedelta.relativedelta(months=1) due_delta = relativedelta.relativedelta(months=1)
plugin_field = 'method' plugin_field = 'method'
state_help = {}
@classmethod @classmethod
@cached @cached

View file

@ -43,6 +43,12 @@ class SEPADirectDebit(PaymentMethod):
form = SEPADirectDebitForm form = SEPADirectDebitForm
serializer = SEPADirectDebitSerializer serializer = SEPADirectDebitSerializer
due_delta = datetime.timedelta(days=5) due_delta = datetime.timedelta(days=5)
state_help = {
'WAITTING_PROCESSING': _("The transaction is created and requires the generation of "
"the SEPA direct debit XML file."),
'WAITTING_EXECUTION': _("SEPA Direct Debit XML file is generated but needs to be sent "
"to the financial institution."),
}
def get_bill_message(self): def get_bill_message(self):
context = { context = {

View file

@ -98,6 +98,16 @@ class Transaction(models.Model):
(SECURED, _("Secured")), (SECURED, _("Secured")),
(REJECTED, _("Rejected")), (REJECTED, _("Rejected")),
) )
STATE_HELP = {
WAITTING_PROCESSING: _("The transaction is created and requires processing by the "
"specific payment method."),
WAITTING_EXECUTION: _("The transaction is processed and its pending execution on "
"the related financial institution."),
EXECUTED: _("The transaction is executed on the financial institution."),
SECURED: _("The transaction ammount is secured."),
REJECTED: _("The transaction has failed and the ammount is lost, a new transaction "
"should be created for recharging."),
}
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"), bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
related_name='transactions') related_name='transactions')
@ -127,22 +137,20 @@ class Transaction(models.Model):
if amount >= self.bill.total: if amount >= self.bill.total:
raise ValidationError(_("New transactions can not be allocated for this bill.")) raise ValidationError(_("New transactions can not be allocated for this bill."))
def check_state(self, *args): def get_state_help(self):
if self.state not in args: if self.source:
raise TypeError("Transaction not in %s" % ' or '.join(args)) return self.source.method_instance.state_help.get(self.state) or self.STATE_HELP.get(self.state)
return self.STATE_HELP.get(self.state)
def mark_as_processed(self): def mark_as_processed(self):
self.check_state(self.WAITTING_PROCESSING)
self.state = self.WAITTING_EXECUTION self.state = self.WAITTING_EXECUTION
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
def mark_as_executed(self): def mark_as_executed(self):
self.check_state(self.WAITTING_EXECUTION)
self.state = self.EXECUTED self.state = self.EXECUTED
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
def mark_as_secured(self): def mark_as_secured(self):
self.check_state(self.EXECUTED)
self.state = self.SECURED self.state = self.SECURED
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
@ -178,26 +186,19 @@ class TransactionProcess(models.Model):
def __str__(self): def __str__(self):
return '#%i' % self.id return '#%i' % self.id
def check_state(self, *args):
if self.state not in args:
raise TypeError("Transaction process not in %s" % ' or '.join(args))
def mark_as_executed(self): def mark_as_executed(self):
self.check_state(self.CREATED)
self.state = self.EXECUTED self.state = self.EXECUTED
for transaction in self.transactions.all(): for transaction in self.transactions.all():
transaction.mark_as_executed() transaction.mark_as_executed()
self.save(update_fields=('state',)) self.save(update_fields=('state',))
def abort(self): def abort(self):
self.check_state(self.CREATED, self.EXCECUTED)
self.state = self.ABORTED self.state = self.ABORTED
for transaction in self.transaction.all(): for transaction in self.transaction.all():
transaction.mark_as_aborted() transaction.mark_as_aborted()
self.save(update_fields=('state',)) self.save(update_fields=('state',))
def commit(self): def commit(self):
self.check_state(self.CREATED, self.EXECUTED)
self.state = self.COMMITED self.state = self.COMMITED
for transaction in self.transactions.processing(): for transaction in self.transactions.processing():
transaction.mark_as_secured() transaction.mark_as_secured()

View file

@ -17,24 +17,24 @@ class WordpressMuBackend(ServiceController):
model = 'saas.SaaS' model = 'saas.SaaS'
default_route_match = "saas.service == 'wordpress'" default_route_match = "saas.service == 'wordpress'"
doc_settings = (settings, doc_settings = (settings,
('SAAS_WORDPRESS_ADMIN_PASSWORD', 'SAAS_WORDPRESS_BASE_URL') ('SAAS_WORDPRESS_ADMIN_PASSWORD', 'SAAS_WORDPRESS_MAIN_URL')
) )
def login(self, session): def login(self, session):
base_url = self.get_base_url() main_url = self.get_main_url()
login_url = base_url + '/wp-login.php' login_url = main_url + '/wp-login.php'
login_data = { login_data = {
'log': 'admin', 'log': 'admin',
'pwd': settings.SAAS_WORDPRESS_ADMIN_PASSWORD, 'pwd': settings.SAAS_WORDPRESS_ADMIN_PASSWORD,
'redirect_to': '/wp-admin/' 'redirect_to': '/wp-admin/'
} }
response = session.post(login_url, data=login_data) response = session.post(login_url, data=login_data)
if response.url != base_url + '/wp-admin/': if response.url != main_url + '/wp-admin/':
raise IOError("Failure login to remote application") raise IOError("Failure login to remote application")
def get_base_url(self): def get_main_url(self):
base_url = settings.SAAS_WORDPRESS_BASE_URL main_url = settings.SAAS_WORDPRESS_MAIN_URL
return base_url.rstrip('/') return main_url.rstrip('/')
def validate_response(self, response): def validate_response(self, response):
if response.status_code != 200: if response.status_code != 200:
@ -42,7 +42,7 @@ class WordpressMuBackend(ServiceController):
raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code) raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code)
def get_id(self, session, saas): def get_id(self, session, saas):
search = self.get_base_url() search = self.get_main_url()
search += '/wp-admin/network/sites.php?s=%s&action=blogs' % saas.name search += '/wp-admin/network/sites.php?s=%s&action=blogs' % saas.name
regex = re.compile( regex = re.compile(
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+' '<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
@ -69,7 +69,7 @@ class WordpressMuBackend(ServiceController):
try: try:
self.get_id(session, saas) self.get_id(session, saas)
except RuntimeError: except RuntimeError:
url = self.get_base_url() url = self.get_main_url()
url += '/wp-admin/network/site-new.php' url += '/wp-admin/network/site-new.php'
content = session.get(url).content.decode('utf8') content = session.get(url).content.decode('utf8')
@ -97,7 +97,7 @@ class WordpressMuBackend(ServiceController):
except RuntimeError: except RuntimeError:
pass pass
else: else:
delete = self.get_base_url() delete = self.get_main_url()
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog' delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
delete += '&id=%d&_wpnonce=%s' % (id, wpnonce) delete += '&id=%d&_wpnonce=%s' % (id, wpnonce)
@ -110,7 +110,7 @@ class WordpressMuBackend(ServiceController):
'_wpnonce': wpnonce, '_wpnonce': wpnonce,
'_wp_http_referer': '/wp-admin/network/sites.php', '_wp_http_referer': '/wp-admin/network/sites.php',
} }
delete = self.get_base_url() delete = self.get_main_url()
delete += '/wp-admin/network/sites.php?action=deleteblog' delete += '/wp-admin/network/sites.php?action=deleteblog'
response = session.post(delete, data=data) response = session.post(delete, data=data)
self.validate_response(response) self.validate_response(response)

View file

@ -22,12 +22,17 @@ class SaaSBaseForm(PluginDataForm):
site_domain = self.instance.get_site_domain() site_domain = self.instance.get_site_domain()
else: else:
site_domain = self.plugin.site_domain site_domain = self.plugin.site_domain
if site_domain: context = {
site_link = '<a href="http://%s">%s</a>' % (site_domain, site_domain) 'site_name': '&lt;site_name&gt;',
'name': '&lt;site_name&gt;',
}
site_domain = site_domain % context
if '&lt;site_name&gt;' in site_domain:
site_link = site_domain
else: else:
site_link = '&lt;site_name&gt;.%s' % self.plugin.site_base_domain site_link = '<a href="http://%s">%s</a>' % (site_domain, site_domain)
self.fields['site_url'].widget.display = site_link self.fields['site_url'].widget.display = site_link
self.fields['name'].label = _("Site name") if self.plugin.site_base_domain else _("Username") self.fields['name'].label = _("Site name") if '%(' in self.plugin.site_domain else _("Username")
class SaaSPasswordForm(SaaSBaseForm): class SaaSPasswordForm(SaaSBaseForm):

View file

@ -1,12 +1,9 @@
from .options import SoftwareService from .options import SoftwareService
from .. import settings
class DokuWikiService(SoftwareService): class DokuWikiService(SoftwareService):
name = 'dokuwiki' name = 'dokuwiki'
verbose_name = "Dowkuwiki" verbose_name = "Dowkuwiki"
icon = 'orchestra/icons/apps/Dokuwiki.png' icon = 'orchestra/icons/apps/Dokuwiki.png'
site_domain = settings.SAAS_DOKUWIKI_DOMAIN
@property
def site_base_domain(self):
from .. import settings
return settings.SAAS_DOKUWIKI_BASE_DOMAIN

View file

@ -13,7 +13,6 @@ from ..forms import SaaSPasswordForm
class SoftwareService(plugins.Plugin): class SoftwareService(plugins.Plugin):
form = SaaSPasswordForm form = SaaSPasswordForm
site_domain = None site_domain = None
site_base_domain = None
has_custom_domain = False has_custom_domain = False
icon = 'orchestra/icons/apps.png' icon = 'orchestra/icons/apps.png'
class_verbose_name = _("Software as a Service") class_verbose_name = _("Software as a Service")
@ -32,9 +31,11 @@ class SoftwareService(plugins.Plugin):
return fields + ('name',) return fields + ('name',)
def get_site_domain(self): def get_site_domain(self):
return self.site_domain or '.'.join( context = {
(self.instance.name, self.site_base_domain) 'site_name': self.instance.name,
) 'name': self.instance.name,
}
return self.site_domain % context
def clean_data(self): def clean_data(self):
data = super(SoftwareService, self).clean_data() data = super(SoftwareService, self).clean_data()

View file

@ -29,8 +29,12 @@ class PHPListForm(SaaSPasswordForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PHPListForm, self).__init__(*args, **kwargs) super(PHPListForm, self).__init__(*args, **kwargs)
self.fields['name'].label = _("Site name") self.fields['name'].label = _("Site name")
base_domain = self.plugin.site_base_domain context = {
help_text = _("Admin URL http://&lt;site_name&gt;.{}/admin/").format(base_domain) 'site_name': '&lt;site_name&gt;',
'name': '&lt;site_name&gt;',
}
domain = self.plugin.site_domain % context
help_text = _("Admin URL http://{}/admin/").format(domain)
self.fields['site_url'].help_text = help_text self.fields['site_url'].help_text = help_text
@ -66,7 +70,7 @@ class PHPListService(SoftwareService):
form = PHPListForm form = PHPListForm
change_form = PHPListChangeForm change_form = PHPListChangeForm
icon = 'orchestra/icons/apps/Phplist.png' icon = 'orchestra/icons/apps/Phplist.png'
site_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN site_domain = settings.SAAS_PHPLIST_DOMAIN
def get_db_name(self): def get_db_name(self):
context = { context = {

View file

@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from .options import SoftwareService from .options import SoftwareService
from .. import settings
from ..forms import SaaSBaseForm from ..forms import SaaSBaseForm
@ -31,8 +32,4 @@ class WordPressService(SoftwareService):
serializer = WordPressDataSerializer serializer = WordPressDataSerializer
icon = 'orchestra/icons/apps/WordPress.png' icon = 'orchestra/icons/apps/WordPress.png'
change_readonly_fileds = ('email',) change_readonly_fileds = ('email',)
site_domain = settings.SAAS_WORDPRESS_DOMAIN
@property
def site_base_domain(self):
from .. import settings
return settings.SAAS_WORDPRESS_BASE_DOMAIN

View file

@ -43,13 +43,13 @@ SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESS_ADMIN_PASSWORD',
'secret' 'secret'
) )
SAAS_WORDPRESS_BASE_URL = Setting('SAAS_WORDPRESS_BASE_URL', SAAS_WORDPRESS_MAIN_URL = Setting('SAAS_WORDPRESS_MAIN_URL',
'https://blogs.{}/'.format(ORCHESTRA_BASE_DOMAIN), 'https://blogs.{}/'.format(ORCHESTRA_BASE_DOMAIN),
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.", help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
) )
SAAS_WORDPRESS_BASE_DOMAIN = Setting('SAAS_WORDPRESS_BASE_DOMAIN', SAAS_WORDPRESS_DOMAIN = Setting('SAAS_WORDPRESS_DOMAIN',
'blogs.{}'.format(ORCHESTRA_BASE_DOMAIN), '%(site_name)s.blogs.{}'.format(ORCHESTRA_BASE_DOMAIN),
) )
@ -63,8 +63,8 @@ SAAS_DOKUWIKI_FARM_PATH = Setting('WEBSITES_DOKUWIKI_FARM_PATH',
'/home/httpd/htdocs/wikifarm/farm' '/home/httpd/htdocs/wikifarm/farm'
) )
SAAS_DOKUWIKI_BASE_DOMAIN = Setting('SAAS_DOKUWIKI_BASE_DOMAIN', SAAS_DOKUWIKI_DOMAIN = Setting('SAAS_DOKUWIKI_DOMAIN',
'dokuwiki.{}'.format(ORCHESTRA_BASE_DOMAIN), '%(site_name)s.dokuwiki.{}'.format(ORCHESTRA_BASE_DOMAIN),
) )
SAAS_DOKUWIKI_TEMPLATE_PATH = Setting('SAAS_DOKUWIKI_TEMPLATE_PATH', SAAS_DOKUWIKI_TEMPLATE_PATH = Setting('SAAS_DOKUWIKI_TEMPLATE_PATH',
@ -125,8 +125,8 @@ SAAS_PHPLIST_BOUNCES_MAILBOX_PASSWORD = Setting('SAAS_PHPLIST_BOUNCES_MAILBOX_PA
'secret', 'secret',
) )
SAAS_PHPLIST_BASE_DOMAIN = Setting('SAAS_PHPLIST_BASE_DOMAIN', SAAS_PHPLIST_DOMAIN = Setting('SAAS_PHPLIST_DOMAIN',
'lists.{}'.format(ORCHESTRA_BASE_DOMAIN), '%(site_name)s.lists.{}'.format(ORCHESTRA_BASE_DOMAIN),
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.", help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
) )