435 lines
16 KiB
Python
435 lines
16 KiB
Python
# lingo - basket and payment system
|
|
# Copyright (C) 2015 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import requests
|
|
import urlparse
|
|
|
|
from dateutil import parser
|
|
from decimal import Decimal
|
|
|
|
import eopayment
|
|
from jsonfield import JSONField
|
|
|
|
from django import template
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.forms import models as model_forms, Select
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils import timezone
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.utils.http import urlencode
|
|
|
|
from ckeditor.fields import RichTextField
|
|
|
|
from combo.data.models import CellBase
|
|
from combo.data.library import register_cell_class
|
|
from combo.utils import NothingInCacheException, sign_url
|
|
|
|
EXPIRED = 9999
|
|
|
|
|
|
SERVICES = [
|
|
(eopayment.DUMMY, _('Dummy (for tests)')),
|
|
(eopayment.SYSTEMPAY, 'systempay (Banque Populaire)'),
|
|
(eopayment.SIPS, _('SIPS (Atos, France)')),
|
|
(eopayment.SIPS2, _('SIPS (Atos, other countries)')),
|
|
(eopayment.SPPLUS, _('SP+ (Caisse d\'epargne)')),
|
|
(eopayment.OGONE, _('Ingenico (formerly Ogone)')),
|
|
(eopayment.PAYBOX, _('Paybox')),
|
|
(eopayment.PAYZEN, _('PayZen')),
|
|
]
|
|
|
|
def build_remote_item(data, regie):
|
|
return RemoteItem(id=data.get('id'), regie=regie,
|
|
creation_date=data['created'],
|
|
payment_limit_date=data['pay_limit_date'],
|
|
display_id=data['display_id'],
|
|
total_amount=data.get('total_amount'),
|
|
amount=data.get('amount'),
|
|
subject=data.get('label'),
|
|
has_pdf=data.get('has_pdf'),
|
|
online_payment=data.get('online_payment'),
|
|
payment_date=data.get('payment_date'))
|
|
|
|
|
|
class Regie(models.Model):
|
|
label = models.CharField(verbose_name=_('Label'), max_length=64)
|
|
slug = models.SlugField(unique=True)
|
|
description = models.TextField(verbose_name=_('Description'))
|
|
service = models.CharField(verbose_name=_('Payment Service'),
|
|
max_length=64, choices=SERVICES)
|
|
service_options = JSONField(blank=True,
|
|
verbose_name=_('Payment Service Options'))
|
|
webservice_url = models.URLField(_('Webservice URL to retrieve remote items'),
|
|
blank=True)
|
|
payment_min_amount = models.DecimalField(_('Minimal payment amount'),
|
|
max_digits=7, decimal_places=2, default=0)
|
|
|
|
def is_remote(self):
|
|
return self.webservice_url != ''
|
|
|
|
class Meta:
|
|
verbose_name = _('Regie')
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.webservice_url and self.webservice_url.endswith('/'):
|
|
self.webservice_url = self.webservice_url.strip('/')
|
|
super(Regie, self).save(*args, **kwargs)
|
|
|
|
def natural_key(self):
|
|
return (self.slug,)
|
|
|
|
def __unicode__(self):
|
|
return self.label
|
|
|
|
def get_past_items(self, context):
|
|
"""
|
|
returns past items
|
|
"""
|
|
return self.get_items(context, past=True)
|
|
|
|
|
|
def get_items(self, context, past=False):
|
|
"""
|
|
returns current or past items
|
|
"""
|
|
if not self.is_remote():
|
|
payed = not past
|
|
return self.basketitem_set.filter(payment_date__isnull=payed,
|
|
user=context.get('user'))
|
|
if context.get('user'):
|
|
if context.get('request') and hasattr(context['request'], 'session') and \
|
|
context['request'].session.get('mellon_session'):
|
|
mellon = context.get('request').session['mellon_session']
|
|
url = self.webservice_url + '/invoices/'
|
|
if past:
|
|
url += 'history/'
|
|
items = requests.get(self.signed_url(context['request'], url,
|
|
NameID=mellon['name_id_content'])).json()
|
|
if items.get('data'):
|
|
return [build_remote_item(item, self) for item in items.get('data')]
|
|
return []
|
|
return []
|
|
|
|
def download_item(self, request, item_id):
|
|
"""
|
|
downloads item's file
|
|
"""
|
|
if self.is_remote():
|
|
if hasattr(request, 'session') and request.session.get('mellon_session'):
|
|
mellon = request.session.get('mellon_session')
|
|
url = self.webservice_url + '/invoice/%s/pdf' % item_id
|
|
return requests.get(self.signed_url(request, url,
|
|
NameID=mellon['name_id_content']))
|
|
raise PermissionDenied
|
|
|
|
def get_item(self, request, item):
|
|
if not self.is_remote():
|
|
return self.basketitem_set.get(pk=item)
|
|
|
|
if hasattr(request, 'session') and request.session.get('mellon_session'):
|
|
mellon = request.session.get('mellon_session')
|
|
url = self.webservice_url + '/invoice/%s/' % item
|
|
item = requests.get(self.signed_url(request, url,
|
|
NameID=mellon['name_id_content'])).json()
|
|
return build_remote_item(item.get('data'), self)
|
|
return {}
|
|
|
|
def pay_item(self, request, item_id, transaction_id, transaction_date):
|
|
url = self.webservice_url + '/invoice/%s/pay/' % item_id
|
|
data = {'transaction_id': transaction_id,
|
|
'transaction_date': transaction_date.strftime('%Y-%m-%dT%H:%M:%S')}
|
|
headers = {'content-type': 'application/json'}
|
|
return requests.post(self.signed_url(request, url),
|
|
data=json.dumps(data), headers=headers).json()
|
|
|
|
def as_api_dict(self):
|
|
return {'slug': self.slug,
|
|
'label': self.label,
|
|
'description': self.description}
|
|
|
|
def signed_url(self, request, url, **params):
|
|
orig = request.get_host()
|
|
url += '?orig=' + orig +'&' + urlencode(params)
|
|
signature_key = settings.LINGO_SIGNATURE_KEY
|
|
return sign_url(url, key=signature_key)
|
|
|
|
|
|
class BasketItem(models.Model):
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
|
regie = models.ForeignKey(Regie)
|
|
subject = models.CharField(verbose_name=_('Subject'), max_length=200)
|
|
source_url = models.URLField(_('Source URL'))
|
|
details = models.TextField(verbose_name=_('Details'), blank=True)
|
|
amount = models.DecimalField(verbose_name=_('Amount'),
|
|
decimal_places=2, max_digits=8)
|
|
creation_date = models.DateTimeField(auto_now_add=True)
|
|
cancellation_date = models.DateTimeField(null=True)
|
|
payment_date = models.DateTimeField(null=True)
|
|
notification_date = models.DateTimeField(null=True)
|
|
|
|
def notify(self, status):
|
|
source_server = urlparse.urlparse(self.source_url).netloc.split(':')[0]
|
|
service_dict = None
|
|
for services in settings.KNOWN_SERVICES.values():
|
|
for service in services.values():
|
|
url = service.get('url')
|
|
verif_orig = urlparse.urlparse(url).netloc.split(':')[0]
|
|
if verif_orig == source_server:
|
|
service_dict = service
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
if not service_dict:
|
|
logging.getLogger(__name__).error(
|
|
'failed to find data for server %s', source_server)
|
|
raise RuntimeError('failed to find data for server')
|
|
params = urlencode({'orig': service_dict.get('orig')})
|
|
url = self.source_url + 'jump/trigger/%s?%s' % (status, params)
|
|
url = sign_url(url, key=service_dict.get('secret'))
|
|
message = {'result': 'ok'}
|
|
r = requests.post(url, data=json.dumps(message), timeout=3)
|
|
r.raise_for_status()
|
|
|
|
def notify_payment(self):
|
|
self.notify('paid')
|
|
self.notification_date = timezone.now()
|
|
self.save()
|
|
|
|
def notify_cancellation(self):
|
|
self.notify('cancelled')
|
|
self.cancellation_date = timezone.now()
|
|
self.save()
|
|
|
|
|
|
class RemoteItem(object):
|
|
payment_date = None
|
|
|
|
def __init__(self, id, regie, creation_date, payment_limit_date,
|
|
total_amount, amount, display_id, subject, has_pdf,
|
|
online_payment, payment_date):
|
|
self.id = id
|
|
self.regie = regie
|
|
self.creation_date = parser.parse(creation_date)
|
|
self.payment_limit_date = parser.parse(payment_limit_date)
|
|
self.total_amount = Decimal(total_amount)
|
|
self.amount = Decimal(amount)
|
|
self.display_id = display_id
|
|
self.subject = subject
|
|
self.has_pdf = has_pdf
|
|
self.online_payment = online_payment
|
|
if payment_date:
|
|
self.payment_date = parser.parse(payment_date)
|
|
|
|
|
|
class Transaction(models.Model):
|
|
regie = models.ForeignKey(Regie, null=True)
|
|
items = models.ManyToManyField(BasketItem, blank=True)
|
|
remote_items = models.CharField(max_length=512)
|
|
start_date = models.DateTimeField(auto_now_add=True)
|
|
end_date = models.DateTimeField(null=True)
|
|
bank_data = JSONField(blank=True)
|
|
order_id = models.CharField(max_length=200)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
|
|
status = models.IntegerField(null=True)
|
|
amount = models.DecimalField(default=0, max_digits=7, decimal_places=2)
|
|
|
|
def is_remote(self):
|
|
return self.remote_items != ''
|
|
|
|
def is_paid(self):
|
|
return self.status == eopayment.PAID
|
|
|
|
def get_status_label(self):
|
|
return {
|
|
0: _('Running'),
|
|
eopayment.PAID: _('Paid'),
|
|
eopayment.CANCELLED: _('Cancelled'),
|
|
EXPIRED: _('Expired')
|
|
}.get(self.status) or _('Unknown')
|
|
|
|
@register_cell_class
|
|
class LingoBasketCell(CellBase):
|
|
|
|
class Meta:
|
|
verbose_name = _('Basket')
|
|
|
|
class Media:
|
|
js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
|
|
css = {'all': ('xstatic/themes/smoothness/jquery-ui.min.css', )}
|
|
|
|
@classmethod
|
|
def is_enabled(cls):
|
|
return Regie.objects.count() > 0
|
|
|
|
def is_relevant(self, context):
|
|
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
|
|
return False
|
|
items = BasketItem.objects.filter(
|
|
user=context['request'].user, payment_date__isnull=True
|
|
).exclude(cancellation_date__isnull=False)
|
|
return len(items) > 0
|
|
|
|
def render(self, context):
|
|
basket_template = template.loader.get_template('lingo/combo/basket.html')
|
|
context['items'] = BasketItem.objects.filter(
|
|
user=context['request'].user, payment_date__isnull=True
|
|
).exclude(cancellation_date__isnull=False)
|
|
context['total'] = sum([x.amount for x in context['items']])
|
|
return basket_template.render(context)
|
|
|
|
|
|
@register_cell_class
|
|
class LingoRecentTransactionsCell(CellBase):
|
|
|
|
class Meta:
|
|
verbose_name = _('Recent Transactions')
|
|
|
|
@classmethod
|
|
def is_enabled(cls):
|
|
return Regie.objects.count() > 0
|
|
|
|
def is_relevant(self, context):
|
|
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
|
|
return False
|
|
transactions = Transaction.objects.filter(
|
|
user=context['request'].user,
|
|
start_date__gte=timezone.now()-datetime.timedelta(days=7))
|
|
return len(transactions) > 0
|
|
|
|
def render(self, context):
|
|
recent_transactions_template = template.loader.get_template(
|
|
'lingo/combo/recent_transactions.html')
|
|
context['transactions'] = Transaction.objects.filter(
|
|
user=context['request'].user,
|
|
start_date__gte=timezone.now()-datetime.timedelta(days=7)
|
|
).order_by('-start_date')
|
|
return recent_transactions_template.render(context)
|
|
|
|
|
|
@register_cell_class
|
|
class LingoBasketLinkCell(CellBase):
|
|
user_dependant = True
|
|
|
|
class Meta:
|
|
verbose_name = _('Basket Link')
|
|
|
|
@classmethod
|
|
def is_enabled(cls):
|
|
return Regie.objects.count() > 0
|
|
|
|
def is_relevant(self, context):
|
|
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
|
|
return False
|
|
items = BasketItem.objects.filter(
|
|
user=context['request'].user, payment_date__isnull=True
|
|
).exclude(cancellation_date__isnull=False)
|
|
return len(items) > 0
|
|
|
|
def render(self, context):
|
|
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
|
|
return ''
|
|
try:
|
|
context['basket_url'] = LingoBasketCell.objects.all()[0].page.get_online_url()
|
|
except IndexError:
|
|
return ''
|
|
basket_template = template.loader.get_template('lingo/combo/basket_link.html')
|
|
context['items'] = BasketItem.objects.filter(
|
|
user=context['request'].user, payment_date__isnull=True
|
|
).exclude(cancellation_date__isnull=False)
|
|
context['total'] = sum([x.amount for x in context['items']])
|
|
return basket_template.render(context)
|
|
|
|
|
|
class Items(CellBase):
|
|
regie = models.CharField(_('Regie'), max_length=50, blank=True)
|
|
title = models.CharField(_('Title'), max_length=200, blank=True)
|
|
text = RichTextField(_('Text'), blank=True, null=True)
|
|
|
|
user_dependant = True
|
|
template_name = 'lingo/combo/items.html'
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
|
|
class Media:
|
|
js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
|
|
css = {'all': ('xstatic/themes/smoothness/jquery-ui.min.css', )}
|
|
|
|
@classmethod
|
|
def is_enabled(cls):
|
|
return Regie.objects.count() > 0
|
|
|
|
def get_default_form_class(self):
|
|
fields = ['title', 'text']
|
|
widgets = {}
|
|
if Regie.objects.count() > 1:
|
|
regies = [('', _('All'))]
|
|
regies.extend([(r.slug, r.label) for r in Regie.objects.all()])
|
|
widgets['regie'] = Select(choices=regies)
|
|
fields.insert(0, 'regie')
|
|
return model_forms.modelform_factory(self.__class__, fields=fields, widgets=widgets)
|
|
|
|
def get_regies(self):
|
|
if self.regie:
|
|
return [Regie.objects.get(slug=self.regie)]
|
|
return Regie.objects.all()
|
|
|
|
def get_cell_extra_context(self, context):
|
|
ctx = {'title': self.title, 'text': self.text}
|
|
items = self.get_items()
|
|
# sort items by creation date
|
|
items.sort(key=lambda i: i.creation_date, reverse=True)
|
|
ctx.update({'items': items})
|
|
return ctx
|
|
|
|
def render(self, context):
|
|
self.context = context
|
|
if not context.get('synchronous'):
|
|
raise NothingInCacheException()
|
|
return super(Items, self).render(context)
|
|
|
|
|
|
@register_cell_class
|
|
class ItemsHistory(Items):
|
|
|
|
class Meta:
|
|
verbose_name = _('Items History Cell')
|
|
|
|
def get_items(self):
|
|
items = []
|
|
for r in self.get_regies():
|
|
items.extend(r.get_past_items(self.context))
|
|
return items
|
|
|
|
|
|
@register_cell_class
|
|
class ActiveItems(Items):
|
|
|
|
class Meta:
|
|
verbose_name = _('Active Items Cell')
|
|
|
|
def get_items(self):
|
|
items = []
|
|
for r in self.get_regies():
|
|
items.extend(r.get_items(self.context))
|
|
return items
|