# 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 . 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