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':
zeros = queryset.filter(approx_total=0, approx_total__isnull=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 = []
relevant = queryset.exclude(approx_total=0, approx_total__isnull=True, is_open=True)
for bill_id, total in relevant.values_list('id', 'approx_total'):
try:
ammount = sum([t.ammount for t in ammounts[bill_id]])
amount = sum([t.amount for t in amounts[bill_id]])
except KeyError:
pass
else:
if abs(total) <= abs(ammount):
if abs(total) <= abs(amount):
paid.append(bill_id)
return queryset.filter(
Q(approx_total=0) |
@ -120,8 +120,9 @@ class PaymentStateListFilter(SimpleListFilter):
elif self.value() == 'PENDING':
has_transaction = queryset.exclude(transactions__isnull=True)
non_rejected = has_transaction.exclude(transactions__state=Transaction.REJECTED)
non_rejected = non_rejected.values_list('id', flat=True).distinct()
return queryset.filter(pk__in=non_rejected)
paid = non_rejected.exclude(transactions__state=Transaction.SECURED)
paid = paid.values_list('id', flat=True).distinct()
return queryset.filter(pk__in=paid)
elif self.value() == 'BAD_DEBT':
closed = queryset.filter(is_open=False).exclude(approx_total=0)
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)
links.append(link)
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.short_description = _("Websites")
display_websites.allow_tags = True

View File

@ -105,7 +105,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
source_link = admin_link('source')
process_link = admin_link('process', short_description=_("proc"))
account_link = admin_link('bill__account')
display_state = admin_colored('state', colors=STATE_COLORS)
def get_change_view_actions(self, obj=None):
actions = super(TransactionAdmin, self).get_change_view_actions()
@ -120,6 +119,15 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
elif obj.state in [Transaction.REJECTED, Transaction.SECURED]:
return []
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):

View File

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

View File

@ -43,6 +43,12 @@ class SEPADirectDebit(PaymentMethod):
form = SEPADirectDebitForm
serializer = SEPADirectDebitSerializer
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):
context = {

View File

@ -98,6 +98,16 @@ class Transaction(models.Model):
(SECURED, _("Secured")),
(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"),
related_name='transactions')
@ -127,22 +137,20 @@ class Transaction(models.Model):
if amount >= self.bill.total:
raise ValidationError(_("New transactions can not be allocated for this bill."))
def check_state(self, *args):
if self.state not in args:
raise TypeError("Transaction not in %s" % ' or '.join(args))
def get_state_help(self):
if self.source:
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):
self.check_state(self.WAITTING_PROCESSING)
self.state = self.WAITTING_EXECUTION
self.save(update_fields=('state', 'modified_at'))
def mark_as_executed(self):
self.check_state(self.WAITTING_EXECUTION)
self.state = self.EXECUTED
self.save(update_fields=('state', 'modified_at'))
def mark_as_secured(self):
self.check_state(self.EXECUTED)
self.state = self.SECURED
self.save(update_fields=('state', 'modified_at'))
@ -178,26 +186,19 @@ class TransactionProcess(models.Model):
def __str__(self):
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):
self.check_state(self.CREATED)
self.state = self.EXECUTED
for transaction in self.transactions.all():
transaction.mark_as_executed()
self.save(update_fields=('state',))
def abort(self):
self.check_state(self.CREATED, self.EXCECUTED)
self.state = self.ABORTED
for transaction in self.transaction.all():
transaction.mark_as_aborted()
self.save(update_fields=('state',))
def commit(self):
self.check_state(self.CREATED, self.EXECUTED)
self.state = self.COMMITED
for transaction in self.transactions.processing():
transaction.mark_as_secured()

View File

@ -17,24 +17,24 @@ class WordpressMuBackend(ServiceController):
model = 'saas.SaaS'
default_route_match = "saas.service == 'wordpress'"
doc_settings = (settings,
('SAAS_WORDPRESS_ADMIN_PASSWORD', 'SAAS_WORDPRESS_BASE_URL')
('SAAS_WORDPRESS_ADMIN_PASSWORD', 'SAAS_WORDPRESS_MAIN_URL')
)
def login(self, session):
base_url = self.get_base_url()
login_url = base_url + '/wp-login.php'
main_url = self.get_main_url()
login_url = main_url + '/wp-login.php'
login_data = {
'log': 'admin',
'pwd': settings.SAAS_WORDPRESS_ADMIN_PASSWORD,
'redirect_to': '/wp-admin/'
}
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")
def get_base_url(self):
base_url = settings.SAAS_WORDPRESS_BASE_URL
return base_url.rstrip('/')
def get_main_url(self):
main_url = settings.SAAS_WORDPRESS_MAIN_URL
return main_url.rstrip('/')
def validate_response(self, response):
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)
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
regex = re.compile(
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
@ -69,7 +69,7 @@ class WordpressMuBackend(ServiceController):
try:
self.get_id(session, saas)
except RuntimeError:
url = self.get_base_url()
url = self.get_main_url()
url += '/wp-admin/network/site-new.php'
content = session.get(url).content.decode('utf8')
@ -97,7 +97,7 @@ class WordpressMuBackend(ServiceController):
except RuntimeError:
pass
else:
delete = self.get_base_url()
delete = self.get_main_url()
delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
delete += '&id=%d&_wpnonce=%s' % (id, wpnonce)
@ -110,7 +110,7 @@ class WordpressMuBackend(ServiceController):
'_wpnonce': wpnonce,
'_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'
response = session.post(delete, data=data)
self.validate_response(response)

View File

@ -22,12 +22,17 @@ class SaaSBaseForm(PluginDataForm):
site_domain = self.instance.get_site_domain()
else:
site_domain = self.plugin.site_domain
if site_domain:
site_link = '<a href="http://%s">%s</a>' % (site_domain, site_domain)
context = {
'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:
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['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):

View File

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

View File

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

View File

@ -29,8 +29,12 @@ class PHPListForm(SaaSPasswordForm):
def __init__(self, *args, **kwargs):
super(PHPListForm, self).__init__(*args, **kwargs)
self.fields['name'].label = _("Site name")
base_domain = self.plugin.site_base_domain
help_text = _("Admin URL http://&lt;site_name&gt;.{}/admin/").format(base_domain)
context = {
'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
@ -66,7 +70,7 @@ class PHPListService(SoftwareService):
form = PHPListForm
change_form = PHPListChangeForm
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):
context = {

View File

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

View File

@ -43,13 +43,13 @@ SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESS_ADMIN_PASSWORD',
'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),
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
)
SAAS_WORDPRESS_BASE_DOMAIN = Setting('SAAS_WORDPRESS_BASE_DOMAIN',
'blogs.{}'.format(ORCHESTRA_BASE_DOMAIN),
SAAS_WORDPRESS_DOMAIN = Setting('SAAS_WORDPRESS_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'
)
SAAS_DOKUWIKI_BASE_DOMAIN = Setting('SAAS_DOKUWIKI_BASE_DOMAIN',
'dokuwiki.{}'.format(ORCHESTRA_BASE_DOMAIN),
SAAS_DOKUWIKI_DOMAIN = Setting('SAAS_DOKUWIKI_DOMAIN',
'%(site_name)s.dokuwiki.{}'.format(ORCHESTRA_BASE_DOMAIN),
)
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',
)
SAAS_PHPLIST_BASE_DOMAIN = Setting('SAAS_PHPLIST_BASE_DOMAIN',
'lists.{}'.format(ORCHESTRA_BASE_DOMAIN),
SAAS_PHPLIST_DOMAIN = Setting('SAAS_PHPLIST_DOMAIN',
'%(site_name)s.lists.{}'.format(ORCHESTRA_BASE_DOMAIN),
help_text="Uses <tt>ORCHESTRA_BASE_DOMAIN</tt> by default.",
)