combo/combo/apps/lingo/models.py

380 lines
13 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 requests
import urllib
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 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
SERVICES = [
(eopayment.DUMMY, _('Dummy (for tests)')),
(eopayment.SYSTEMPAY, 'systempay (Banque Populaire)'),
(eopayment.SIPS, 'SIPS'),
(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'))
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 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 = self.get_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 self.get_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 = self.get_url(request, url,
NameID=mellon['name_id_content']).json()
return build_remote_item(item.get('data'), self)
return {}
def pay_item(self, request, item):
url = self.webservice_url + '/invoice/%s/pay/' % item
return self.get_url(request, url)
def as_api_dict(self):
return {'slug': self.slug,
'label': self.label,
'description': self.description}
def get_url(self, request, url, **params):
orig = request.get_host()
url += '?orig=' + orig +'&' + urllib.urlencode(params)
signature_key = settings.LINGO_SIGNATURE_KEY
url = sign_url(url, key=signature_key)
return requests.get(url)
class BasketItem(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
regie = models.ForeignKey(Regie)
subject = models.CharField(verbose_name=_('Subject'), max_length=64)
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):
# TODO: sign with real values
url = self.source_url + 'jump/trigger/paid?email=trigger@localhost&orig=combo'
url = sign_url(url, key='xxx')
message = {'result': 'ok'}
r = requests.post(url, data=json.dumps(message), timeout=3)
self.notification_date = timezone.now()
self.save()
class RemoteItem(object):
def __init__(self, id, regie, creation_date, payment_limit_date,
total_amount, amount, display_id, subject, has_pdf,
online_payment):
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
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'),
}.get(self.status) or _('Unknown')
@register_cell_class
class LingoBasketCell(CellBase):
class Meta:
verbose_name = _('Basket')
@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 ''
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 = RichTextField(_('Title'), blank=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):
if Regie.objects.count() == 1:
return None
regies = [('', _('All'))]
regies.extend([(r.slug, r.label) for r in Regie.objects.all()])
return model_forms.modelform_factory(self.__class__,
fields=['regie', 'title'],
widgets={'regie': Select(choices=regies)})
def get_regies(self):
if self.regie:
return [Regie.objects.get(slug=self.regie)]
return Regie.objects.all()
def get_cell_extra_context(self):
ctx = {'title': self.title}
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