Compare commits
74 Commits
main
...
wip/lingo-
Author | SHA1 | Date |
---|---|---|
Frédéric Péters | 43820628d0 | |
Frédéric Péters | 90171fd0ef | |
Frédéric Péters | 14657cb7e2 | |
Frédéric Péters | 6cb1097b75 | |
Frédéric Péters | b3f68df240 | |
Serghei Mihai | 006fa51b4a | |
Serghei Mihai | b130f0298a | |
Serghei Mihai | a2fe92d59d | |
Serghei Mihai | 1b6b570981 | |
Serghei Mihai | a33f6d4200 | |
Frédéric Péters | 5bbd0193cf | |
Frédéric Péters | e177ce4946 | |
Serghei Mihai | 3dddbdc142 | |
Frédéric Péters | e5cfb178e4 | |
Frédéric Péters | 928a9b3647 | |
Frédéric Péters | 1a08a07c7b | |
Serghei Mihai | d551a21475 | |
Serghei Mihai | 43630cfdec | |
Frédéric Péters | 546a937dff | |
Serghei Mihai | b5d6c6b4cb | |
Serghei Mihai | e9102a2b20 | |
Benjamin Dauvergne | 017dfb0789 | |
Serghei Mihai | b644de0cad | |
Serghei Mihai | 403cc51741 | |
Serghei Mihai | 96be8c4f6d | |
Serghei Mihai | 44ace86186 | |
Serghei Mihai | 39926ba4f5 | |
Serghei Mihai | df0a09f23d | |
Serghei Mihai | 3ffcc1c941 | |
Serghei Mihai | 0e1fb7dc68 | |
Serghei Mihai | 721c813513 | |
Serghei Mihai | 993f576622 | |
Serghei Mihai | dc636e1926 | |
Serghei Mihai | 2c4c7bce33 | |
Serghei Mihai | 4272039a3f | |
Serghei Mihai | 82a7b81f03 | |
Serghei Mihai | 9a63364d5c | |
Serghei Mihai | d078408b57 | |
Serghei Mihai | c0945978ad | |
Serghei Mihai | 38b2e40194 | |
Frédéric Péters | 654982d778 | |
Frédéric Péters | 8188b36d38 | |
Frédéric Péters | 48938c98f5 | |
Frédéric Péters | 20ec4fa1ff | |
Frédéric Péters | b8e1e2e03d | |
Frédéric Péters | 0a12ffdb67 | |
Frédéric Péters | 16692b692b | |
Frédéric Péters | 9c29693dcd | |
Frédéric Péters | 81dc29d045 | |
Frédéric Péters | 11725f31da | |
Frédéric Péters | 7378aeaf88 | |
Frédéric Péters | ec8b79d98e | |
Frédéric Péters | f7fca2f99a | |
Frédéric Péters | eda81a82f8 | |
Frédéric Péters | 839fe0686a | |
Frédéric Péters | ad80027ad5 | |
Frédéric Péters | 3a19ec0e01 | |
Frédéric Péters | a1812cd48b | |
Frédéric Péters | a857abb8d8 | |
Frédéric Péters | 6cd32e39b5 | |
Frédéric Péters | 1d42ebc729 | |
Frédéric Péters | e11ef16656 | |
Frédéric Péters | 98100a5d20 | |
Frédéric Péters | d13d62dd73 | |
Frédéric Péters | fa45b9b6ab | |
Frédéric Péters | 6bed69145a | |
Frédéric Péters | 0932cbcd6a | |
Frédéric Péters | 6c6c5c8635 | |
Frédéric Péters | 2e510a06e2 | |
Frédéric Péters | e24b7bac2b | |
Frédéric Péters | ba24ce9bc8 | |
Frédéric Péters | 81a940367d | |
Frédéric Péters | 29c9abc5a2 | |
Frédéric Péters | b5ceeea21c |
|
@ -1,5 +1,6 @@
|
|||
# locales
|
||||
recursive-include combo/locale *.po *.mo
|
||||
recursive-include combo/apps/lingo/locale *.po *.mo
|
||||
|
||||
# static
|
||||
recursive-include combo/apps/dataviz/static *.css *.js *.ico *.gif *.png *.jpg
|
||||
|
@ -10,6 +11,7 @@ recursive-include data/themes *.css *.js *.gif *.png *.jpg *.jpeg *.html
|
|||
# templates
|
||||
recursive-include combo/apps/dataviz/templates *.html
|
||||
recursive-include combo/apps/momo/templates *.html
|
||||
recursive-include combo/apps/lingo/templates *.html
|
||||
recursive-include combo/apps/wcs/templates *.html
|
||||
recursive-include combo/apps/family/templates *.html
|
||||
recursive-include combo/manager/templates *.html
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
Lingo
|
||||
=====
|
||||
|
||||
Lingo is an extension module for the combo content management system, adding
|
||||
basket and payment features.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
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/>.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
Items amount can be added to basket through API by posting to
|
||||
*/api/lingo/add-basket-item* endpoint a json payload containing *amount* and/or
|
||||
*"extra": {"amount": ...}* attribute or passing *amount* in the query string
|
||||
|
||||
For example:
|
||||
|
||||
{"display_name": "<item display name>",
|
||||
"url": "http://<item url>",
|
||||
"amount": "42.42",
|
||||
"extra": {"amount": "10.42", ...},
|
||||
...
|
||||
}
|
||||
|
||||
or
|
||||
/api/lingo/add-basket-item?amount=10
|
||||
|
||||
The "amount" attribute should be float or decimal or a list of
|
||||
floats/decimals. For example:
|
||||
|
||||
{"amount": [14.12], "extra": {"amount": ["10.42", "5", "10"], ... }, ...}
|
|
@ -0,0 +1,37 @@
|
|||
# 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 django.apps
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
class Plugin(object):
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'combo.apps.lingo'
|
||||
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_extra_manager_actions(self):
|
||||
return [{'href': reverse('lingo-manager-homepage'),
|
||||
'text': _('Online Payment')}]
|
||||
|
||||
default_app_config = 'combo.apps.lingo.AppConfig'
|
|
@ -0,0 +1,25 @@
|
|||
# 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/>.
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Regie
|
||||
|
||||
|
||||
class RegieAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {'slug': ('label',)}
|
||||
|
||||
admin.site.register(Regie, RegieAdmin)
|
|
@ -0,0 +1,284 @@
|
|||
# Linfo French Translation.
|
||||
# Copyright (C) 2015 Entr'ouvert
|
||||
# This file is distributed under the same license as the Lingo package.
|
||||
# Frederic Peters <fpeters@entrouvert.com>, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lingo 0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-10-05 15:37+0000\n"
|
||||
"PO-Revision-Date: 2015-03-10 11:05+0100\n"
|
||||
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
|
||||
"Language: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: lingo/__init__.py:35 lingo/templates/lingo/manager_base.html:10
|
||||
msgid "Online Payment"
|
||||
msgstr "Paiement en ligne"
|
||||
|
||||
#: lingo/models.py:43
|
||||
msgid "Dummy (for tests)"
|
||||
msgstr "Factice (pour tests)"
|
||||
|
||||
#: lingo/models.py:46
|
||||
msgid "SP+ (Caisse d'epargne)"
|
||||
msgstr "SP+ (Caisse d'épargne)"
|
||||
|
||||
#: lingo/models.py:47
|
||||
msgid "Ingenico (formerly Ogone)"
|
||||
msgstr "Ingenico (précédemment Ogone)"
|
||||
|
||||
#: lingo/models.py:48
|
||||
msgid "Paybox"
|
||||
msgstr ""
|
||||
|
||||
#: lingo/models.py:65 lingo/templates/lingo/combo/items.html:9
|
||||
msgid "Label"
|
||||
msgstr "Libellé"
|
||||
|
||||
#: lingo/models.py:67
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
#: lingo/models.py:68
|
||||
msgid "Payment Service"
|
||||
msgstr "Service de paiement"
|
||||
|
||||
#: lingo/models.py:71
|
||||
msgid "Payment Service Options"
|
||||
msgstr "Options du service de paiement"
|
||||
|
||||
#: lingo/models.py:72
|
||||
msgid "Webservice URL to retrieve remote items"
|
||||
msgstr "Webservice de récuperation des factures distantes"
|
||||
|
||||
#: lingo/models.py:74
|
||||
msgid "Minimal payment amount"
|
||||
msgstr "Montant minimum de paiement"
|
||||
|
||||
#: lingo/models.py:81 lingo/models.py:306
|
||||
msgid "Regie"
|
||||
msgstr "Régie"
|
||||
|
||||
#: lingo/models.py:161
|
||||
msgid "Subject"
|
||||
msgstr "Objet"
|
||||
|
||||
#: lingo/models.py:162
|
||||
msgid "Source URL"
|
||||
msgstr "URL de source"
|
||||
|
||||
#: lingo/models.py:163
|
||||
msgid "Details"
|
||||
msgstr "Détails"
|
||||
|
||||
#: lingo/models.py:164 lingo/templates/lingo/combo/items.html:12
|
||||
msgid "Amount"
|
||||
msgstr "Montant(TTC)"
|
||||
|
||||
#: lingo/models.py:216
|
||||
msgid "Running"
|
||||
msgstr "En cours"
|
||||
|
||||
#: lingo/models.py:217
|
||||
msgid "Paid"
|
||||
msgstr "Payé"
|
||||
|
||||
#: lingo/models.py:218
|
||||
msgid "Cancelled"
|
||||
msgstr "Annulé"
|
||||
|
||||
#: lingo/models.py:219
|
||||
msgid "Unknown"
|
||||
msgstr "Inconnu"
|
||||
|
||||
#: lingo/models.py:226 lingo/templates/lingo/combo/basket.html:3
|
||||
msgid "Basket"
|
||||
msgstr "Panier"
|
||||
|
||||
#: lingo/models.py:253 lingo/templates/lingo/combo/recent_transactions.html:2
|
||||
msgid "Recent Transactions"
|
||||
msgstr "Transactions récentes"
|
||||
|
||||
#: lingo/models.py:282
|
||||
msgid "Basket Link"
|
||||
msgstr "Lien vers le Panier"
|
||||
|
||||
#: lingo/models.py:307
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: lingo/models.py:327
|
||||
msgid "All"
|
||||
msgstr "Tout"
|
||||
|
||||
#: lingo/models.py:357
|
||||
msgid "Items History Cell"
|
||||
msgstr "Cellule Historique des factures"
|
||||
|
||||
#: lingo/models.py:370
|
||||
msgid "Active Items Cell"
|
||||
msgstr "Cellule Factures actives"
|
||||
|
||||
#: lingo/templates/lingo/combo/basket.html:10
|
||||
msgid "Total:"
|
||||
msgstr "Total :"
|
||||
|
||||
#: lingo/templates/lingo/combo/basket.html:12
|
||||
msgid "Pay selected items"
|
||||
msgstr "Payer les éléments sélectionnés"
|
||||
|
||||
#: lingo/templates/lingo/combo/basket_link.html:3
|
||||
msgid "Basket:"
|
||||
msgstr "Panier :"
|
||||
|
||||
#: lingo/templates/lingo/combo/basket_link.html:3
|
||||
msgid "items"
|
||||
msgstr "factures"
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:5
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Item nr. %(number)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Facture nr. %(number)s\n"
|
||||
" "
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:12
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Label: %(label)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Libellé : %(label)s\n"
|
||||
" "
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:17
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Total amount: %(amount)s€\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Montant total : %(amount)s€\n"
|
||||
" "
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:23
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Amount to pay: %(amount)s€\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Montant à régler : %(amount)s€\n"
|
||||
" "
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:28
|
||||
msgid "Issued on:"
|
||||
msgstr "Émise le :"
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:30
|
||||
msgid "Payed on:"
|
||||
msgstr "Payée le :"
|
||||
|
||||
#: lingo/templates/lingo/combo/item.html:36
|
||||
msgid "Pay"
|
||||
msgstr "Payer"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:8
|
||||
msgid "Number"
|
||||
msgstr "Numéro"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:10
|
||||
msgid "Issue date"
|
||||
msgstr "Date d'émission"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:11
|
||||
msgid "Payment limit date"
|
||||
msgstr "Date limite de paiement"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:23
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" %(amount)s€\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:28
|
||||
msgid "View"
|
||||
msgstr "Voir"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:29
|
||||
msgid "and pay"
|
||||
msgstr "et payer"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:33
|
||||
msgid "Download"
|
||||
msgstr "Télécharger"
|
||||
|
||||
#: lingo/templates/lingo/combo/items.html:41
|
||||
msgid "No items yet"
|
||||
msgstr "Aucune facture"
|
||||
|
||||
#: lingo/templates/lingo/manager_base.html:5
|
||||
#: lingo/templates/lingo/regie_list.html:5
|
||||
msgid "Regies"
|
||||
msgstr "Régies"
|
||||
|
||||
#: lingo/templates/lingo/regie_confirm_delete.html:11
|
||||
msgid "Are you sure you want to delete this?"
|
||||
msgstr ""
|
||||
|
||||
#: lingo/templates/lingo/regie_confirm_delete.html:13
|
||||
msgid "Confirm Deletion"
|
||||
msgstr ""
|
||||
|
||||
#: lingo/templates/lingo/regie_confirm_delete.html:14
|
||||
#: lingo/templates/lingo/regie_form.html:19
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: lingo/templates/lingo/regie_form.html:6
|
||||
msgid "Edit Regie"
|
||||
msgstr "Editer une Régie"
|
||||
|
||||
#: lingo/templates/lingo/regie_form.html:8
|
||||
msgid "New Regie"
|
||||
msgstr "Nouvelle Régie"
|
||||
|
||||
#: lingo/templates/lingo/regie_form.html:18
|
||||
msgid "Save"
|
||||
msgstr "Sauvegarder"
|
||||
|
||||
#: lingo/templates/lingo/regie_form.html:21
|
||||
msgid "Delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: lingo/templates/lingo/regie_list.html:6
|
||||
msgid "New"
|
||||
msgstr "Nouvelle"
|
||||
|
||||
#: lingo/templates/lingo/regie_list.html:21
|
||||
msgid ""
|
||||
"\n"
|
||||
" This site doesn't have any regie yet. Click on the \"New\" button in the "
|
||||
"top\n"
|
||||
" right of the page to add a first one.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Ce site n'a pas encore de régie définie.Cliquez sur \"Nouvelle\" en haut à "
|
||||
"droite pour en ajouter une.\n"
|
||||
" "
|
|
@ -0,0 +1,54 @@
|
|||
# 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/>.
|
||||
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views.generic import (CreateView, UpdateView, ListView,
|
||||
DeleteView, TemplateView)
|
||||
|
||||
import eopayment
|
||||
|
||||
from .models import Regie, Transaction
|
||||
|
||||
class RegieListView(ListView):
|
||||
model = Regie
|
||||
|
||||
|
||||
class RegieCreateView(CreateView):
|
||||
model = Regie
|
||||
success_url = reverse_lazy('lingo-manager-regie-list')
|
||||
|
||||
|
||||
class RegieUpdateView(UpdateView):
|
||||
model = Regie
|
||||
success_url = reverse_lazy('lingo-manager-regie-list')
|
||||
|
||||
|
||||
class RegieDeleteView(DeleteView):
|
||||
model = Regie
|
||||
success_url = reverse_lazy('lingo-manager-regie-list')
|
||||
|
||||
|
||||
class TransactionListView(ListView):
|
||||
model = Transaction
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
return Transaction.objects.filter(status=eopayment.PAID).order_by('-start_date')
|
||||
|
||||
|
||||
class ManagerHomeView(TemplateView):
|
||||
template_name = 'lingo/manager_home.html'
|
|
@ -0,0 +1,77 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import jsonfield.fields
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('data', '0005_auto_20150226_0903'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BasketItem',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('subject', models.CharField(max_length=64, verbose_name='Subject')),
|
||||
('source_url', models.URLField(verbose_name='Source URL')),
|
||||
('details', models.TextField(verbose_name='Details', blank=True)),
|
||||
('amount', models.DecimalField(verbose_name='Amount', max_digits=8, decimal_places=2)),
|
||||
('creation_date', models.DateTimeField(auto_now_add=True)),
|
||||
('cancellation_date', models.DateTimeField(null=True)),
|
||||
('payment_date', models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LingoBasketCell',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Basket',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Regie',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('label', models.CharField(max_length=64, verbose_name='Label')),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
('service', models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)")])),
|
||||
('service_options', jsonfield.fields.JSONField(default=dict, verbose_name='Payment Service Options', blank=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Regie',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='basketitem',
|
||||
name='regie',
|
||||
field=models.ForeignKey(to='lingo.Regie'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='basketitem',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('start_date', models.DateTimeField(auto_now_add=True)),
|
||||
('end_date', models.DateTimeField(null=True)),
|
||||
('bank_data', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
('order_id', models.CharField(max_length=200)),
|
||||
('items', models.ManyToManyField(to='lingo.BasketItem', blank=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0002_transaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='regie',
|
||||
name='service',
|
||||
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0003_auto_20150306_1047'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='basketitem',
|
||||
name='notification_date',
|
||||
field=models.DateTimeField(null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('data', '0005_auto_20150226_0903'),
|
||||
('lingo', '0004_basketitem_notification_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LingoRecentTransactionsCell',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Recent Transactions',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=0, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0005_auto_20150307_1242'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='status',
|
||||
field=models.IntegerField(null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
('data', '0005_auto_20150226_0903'),
|
||||
('lingo', '0006_transaction_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LingoBasketLinkCell',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Basket Link',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import ckeditor.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
('data', '0010_feedcell'),
|
||||
('lingo', '0007_lingobasketlinkcell'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ActiveItems',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
|
||||
('title', ckeditor.fields.RichTextField(verbose_name='Title', blank=True)),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Active Items Cell',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ItemsHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
|
||||
('title', ckeditor.fields.RichTextField(verbose_name='Title', blank=True)),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Items History Cell',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='regie',
|
||||
name='webservice_url',
|
||||
field=models.URLField(verbose_name='Webservice URL to retrieve remote items', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='regie',
|
||||
name='service',
|
||||
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0008_auto_20150908_1538'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='regie',
|
||||
field=models.ForeignKey(to='lingo.Regie', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='remote_items',
|
||||
field=models.CharField(default='', max_length=512),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0009_auto_20150917_1456'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='regie',
|
||||
name='payment_min_amount',
|
||||
field=models.DecimalField(default=0, verbose_name='Minimal payment amount', max_digits=7, decimal_places=2),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0010_regie_payment_min_amount'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='activeitems',
|
||||
name='restricted_to_unlogged',
|
||||
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemshistory',
|
||||
name='restricted_to_unlogged',
|
||||
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lingobasketcell',
|
||||
name='restricted_to_unlogged',
|
||||
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lingobasketlinkcell',
|
||||
name='restricted_to_unlogged',
|
||||
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lingorecenttransactionscell',
|
||||
name='restricted_to_unlogged',
|
||||
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0011_auto_20151029_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='regie',
|
||||
name='service',
|
||||
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox'), (b'payzen', 'PayZen')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lingo', '0012_auto_20151130_1047'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='amount',
|
||||
field=models.DecimalField(default=0, max_digits=7, decimal_places=2),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,383 @@
|
|||
# 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 = 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, transaction_id, transaction_date):
|
||||
url = self.webservice_url + '/invoice/pay/'
|
||||
data = {'invoice_id': item, '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 +'&' + urllib.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=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
|
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}
|
||||
{% if total %}
|
||||
<h2>{% trans "Basket" %}</h2>
|
||||
<form action="{% url 'lingo-pay' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li><label><input type="checkbox" name="item" value="{{ item.id }}" checked/> {{ item.subject }}: {{ item.amount }} €</label> (<a href="{{ item.source_url}}">voir</a>)</li>
|
||||
{% endfor %}
|
||||
<li><strong>{% trans "Total:" %}</strong> {{ total }} €</li>
|
||||
</ul>
|
||||
<input type="submit" value="{% trans "Pay selected items" %}"/>
|
||||
</form>
|
||||
{% endif %}
|
|
@ -0,0 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% if total %}
|
||||
<p><a href="{{site_base}}">{% trans 'Basket:' %} {{items|length}} {% trans 'items' %} ({{ total }} €)</a></p>
|
||||
</form>
|
||||
{% endif %}
|
|
@ -0,0 +1,41 @@
|
|||
{% load i18n %}
|
||||
<div id="content">
|
||||
<div id="appbar">
|
||||
<h2>
|
||||
{% blocktrans with number=item.display_id %}
|
||||
Item nr. {{ number }}
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="{% url 'lingo-pay' %}" method="POST">
|
||||
<div id="item">
|
||||
<h4 class="label">
|
||||
{% blocktrans with label=item.subject %}
|
||||
Label: {{ label }}
|
||||
{% endblocktrans %}
|
||||
</h4>
|
||||
<div class="total_amount">
|
||||
{% blocktrans with amount=item.total_amount %}
|
||||
Total amount: {{ amount }}€
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% if item.amount %}
|
||||
<div class="amount">
|
||||
{% blocktrans with amount=item.amount %}
|
||||
Amount to pay: {{ amount }}€
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="issued">{% trans "Issued on:" %} {{ item.creation_date|date:"SHORT_DATE_FORMAT" }}</div>
|
||||
{% if item.payment_date %}
|
||||
<div class="paid">{% trans "Payed on:" %} {{ item.payment_date|date:"SHORT_DATE_FORMAT" }}</div>
|
||||
{% endif %}
|
||||
{% if item.online_payment and item.amount >= regie.payment_min_amount %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="regie" value="{{ regie.pk }}" />
|
||||
<input type="hidden" name="item" value="{{ item.id }}" />
|
||||
<button>{% trans "Pay" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,42 @@
|
|||
{% load i18n %}
|
||||
|
||||
<h3>{{ title|safe }}</h3>
|
||||
{% if items %}
|
||||
<table id="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{% trans "Number" %}</td>
|
||||
<td>{% trans "Label" %}</td>
|
||||
<td>{% trans "Issue date" %}</td>
|
||||
<td>{% trans "Payment limit date" %}</td>
|
||||
<td>{% trans "Amount" %}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.display_id }}</td>
|
||||
<td>{{ item.subject }}</td>
|
||||
<td>{{ item.creation_date|date:"SHORT_DATE_FORMAT" }}</td>
|
||||
<td>{{ item.payment_limit_date|date:"SHORT_DATE_FORMAT" }}</td>
|
||||
<td>{% blocktrans with amount=item.total_amount %}
|
||||
{{ amount }}€
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'view-item' regie_id=item.regie.pk item_id=item.id %}" rel="popup" class="icon-view">{% trans "View" %}
|
||||
{% if item.online_payment and item.amount >= item.regie.payment_min_amount %}{% trans "and pay" %}{% endif %}
|
||||
</a>
|
||||
{% if item.has_pdf %} /
|
||||
<a href="{% url 'download-item-pdf' regie_id=item.regie.pk item_id=item.id %}" class="icon-pdf">
|
||||
{% trans "Download" %}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{% trans "No items yet" %}
|
||||
{% endif %}
|
|
@ -0,0 +1,18 @@
|
|||
{% load i18n %}
|
||||
<h2>{% trans "Recent Transactions" %}</h2>
|
||||
{% csrf_token %}
|
||||
<ul>
|
||||
{% for transaction in transactions %}
|
||||
<li>{{ transaction.start_date|date:"d E o H:i"|lower }}
|
||||
{% if transaction.is_paid %}
|
||||
<ul>
|
||||
{% for item in transaction.items.all %}
|
||||
<li>{{ item.subject }}: {{ item.amount }} €</label> (<a href="{{ item.source_url}}">voir</a>)</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ transaction.get_status_label }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "combo/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Online Payment' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'lingo-manager-homepage' %}">{% trans 'Online Payment' %}</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "lingo/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<ul class="apps">
|
||||
<li><a class="button" href="{% url 'lingo-manager-transactions-list' %}">{% trans 'Transactions' %}</a></li>
|
||||
<li><a class="button" href="{% url 'lingo-manager-regie-list' %}">{% trans 'Regies' %}</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
|||
{% load i18n %}
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body onload="document.forms[0].submit()">
|
||||
<p>
|
||||
{% trans "Please wait while your browser is being redirected to the payment website..." %}
|
||||
</p>
|
||||
<form action="{{ form.url }}" method="{{ form.method }}" style="display: none">
|
||||
{% for field in form.fields %}
|
||||
<input type="{{ field.type }}" name="{{ field.name }}" value="{{ field.value }}"/>
|
||||
{% endfor %}
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "combo/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{{ view.model.get_verbose_name }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% blocktrans %}Are you sure you want to delete this?{% endblocktrans %}
|
||||
<div class="buttons">
|
||||
<button>{% trans 'Confirm Deletion' %}</button>
|
||||
<a class="cancel" href="{% url 'lingo-manager-homepage' %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "lingo/regie_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
{% if object.id %}
|
||||
<h2>{% trans "Edit Regie" %}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans "New Regie" %}</h2>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Save" %}</button>
|
||||
<a class="cancel" href="{% url 'lingo-manager-homepage' %}">{% trans 'Cancel' %}</a>
|
||||
{% if object.id %}
|
||||
<a class="delete" rel="popup" href="{% url 'lingo-manager-regie-delete' pk=object.id %}">{% trans 'Delete' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "lingo/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Regies' %}</h2>
|
||||
<a rel="popup" href="{% url 'lingo-manager-regie-add' %}">{% trans 'New' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'lingo-manager-regie-list' %}">{% trans 'Regies' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if object_list %}
|
||||
<div class="objects-list">
|
||||
{% for regie in object_list %}
|
||||
<div>
|
||||
<a href="{% url 'lingo-manager-regie-edit' pk=regie.id %}">{{ regie.label }} <span>({{regie.service}})</span></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans %}
|
||||
This site doesn't have any regie yet. Click on the "New" button in the top
|
||||
right of the page to add a first one.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,74 @@
|
|||
{% extends "lingo/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Transactions' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'lingo-manager-transactions-list' %}">{% trans 'Transactions' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if object_list %}
|
||||
<table class="main">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Transaction Identifier' %}</th>
|
||||
<th>{% trans 'Date' %}</th>
|
||||
<th>{% trans 'User' %}</th>
|
||||
<th>{% trans 'Amount' %}</th>
|
||||
<th colspan="2">{% trans 'Items' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr>
|
||||
<td rowspan="{{object.items.all.count}}">{{object.order_id}}</td>
|
||||
<td rowspan="{{object.items.all.count}}">{{object.start_date}}</td>
|
||||
<td rowspan="{{object.items.all.count}}">{{object.user.first_name}} {{object.user.last_name}}</td>
|
||||
<td rowspan="{{object.items.all.count}}">{{object.amount}} €</td>
|
||||
{% for item in object.items.all %}
|
||||
{% if not forloop.first %}<tr>{% endif %}
|
||||
<td style="font-size: smaller">{{item.subject}}</td> <td style="font-size: smaller" class="price">{{item.amount}} €</td></tr>
|
||||
{% endfor %}
|
||||
{% if not object.items.all %}</tr>{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<p class="paginator">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}"><<</a>
|
||||
{% else %}
|
||||
<span><<</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<span class="current">
|
||||
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">>></a>
|
||||
{% else %}
|
||||
<span>>></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans %}
|
||||
This site doesn't have any transaction yet.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,50 @@
|
|||
# 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/>.
|
||||
|
||||
from django.conf.urls import patterns, url, include
|
||||
|
||||
from combo.urls_utils import decorated_includes, manager_required
|
||||
|
||||
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
|
||||
ReturnView, ItemDownloadView, ItemView)
|
||||
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
|
||||
RegieDeleteView, TransactionListView, ManagerHomeView)
|
||||
|
||||
lingo_manager_urls = patterns('lingo.manager_views',
|
||||
url('^$', ManagerHomeView.as_view(), name='lingo-manager-homepage'),
|
||||
url('^transactions/$', TransactionListView.as_view(), name='lingo-manager-transactions-list'),
|
||||
url('^regies/$', RegieListView.as_view(), name='lingo-manager-regie-list'),
|
||||
url('^regies/add/$', RegieCreateView.as_view(), name='lingo-manager-regie-add'),
|
||||
url('^regies/(?P<pk>\w+)/edit$', RegieUpdateView.as_view(),
|
||||
name='lingo-manager-regie-edit'),
|
||||
url('^regies/(?P<pk>\w+)/delete$', RegieDeleteView.as_view(),
|
||||
name='lingo-manager-regie-delete'),
|
||||
)
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url('^api/lingo/regies$', RegiesApiView.as_view(), name='api-regies'),
|
||||
url('^api/lingo/add-basket-item$', AddBasketItemApiView.as_view(),
|
||||
name='api-add-basket-item'),
|
||||
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
|
||||
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),
|
||||
url(r'^lingo/return/(?P<regie_pk>\w+)/$', ReturnView.as_view(), name='lingo-return'),
|
||||
url(r'^manage/lingo/', decorated_includes(manager_required,
|
||||
include(lingo_manager_urls))),
|
||||
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_id>[\w,-]+)/pdf$',
|
||||
ItemDownloadView.as_view(), name='download-item-pdf'),
|
||||
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_id>[\w,-]+)/$',
|
||||
ItemView.as_view(), name='view-item'),
|
||||
)
|
|
@ -0,0 +1,265 @@
|
|||
# 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/>.
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import json
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import View, ListView, TemplateView
|
||||
|
||||
import eopayment
|
||||
|
||||
try:
|
||||
from mellon.models import UserSAMLIdentifier
|
||||
except ImportError:
|
||||
UserSAMLIdentifier = None
|
||||
|
||||
from .models import Regie, BasketItem, Transaction
|
||||
|
||||
class RegiesApiView(ListView):
|
||||
model = Regie
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = HttpResponse(content_type='application/json')
|
||||
data = {'data': [x.as_api_dict() for x in self.get_queryset()]}
|
||||
json_str = json.dumps(data)
|
||||
if 'jsonpCallback' in request.GET:
|
||||
json_str = '%s(%s);' % (request.GET['jsonpCallback'], json_str)
|
||||
response.write(json_str)
|
||||
return response
|
||||
|
||||
|
||||
class AddBasketItemApiView(View):
|
||||
http_method_names = ['post', 'options']
|
||||
|
||||
@csrf_exempt
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(AddBasketItemApiView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_amount(self, amount):
|
||||
if isinstance(amount, list):
|
||||
d = Decimal(sum([Decimal(a) for a in amount]))
|
||||
else:
|
||||
d = Decimal(amount)
|
||||
return d.quantize(Decimal('0.01'), ROUND_HALF_UP)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# XXX: check request signature
|
||||
|
||||
request_body = json.loads(self.request.body)
|
||||
extra = request_body.get('extra', {})
|
||||
|
||||
item = BasketItem(amount=0)
|
||||
item.amount = self.get_amount(request.GET.getlist('amount'))
|
||||
|
||||
if request_body.get('amount'):
|
||||
item.amount += self.get_amount(request_body['amount'])
|
||||
|
||||
if extra.get('amount'):
|
||||
item.amount += self.get_amount(extra['amount'])
|
||||
|
||||
try:
|
||||
if request.GET.get('NameId'):
|
||||
if UserSAMLIdentifier is None:
|
||||
raise Exception('missing mellon?')
|
||||
try:
|
||||
user = UserSAMLIdentifier.objects.get(name_id=request.GET.get('NameId')).user
|
||||
except UserSAMLIdentifier.DoesNotExist:
|
||||
raise Exception('unknown name id')
|
||||
elif request.GET.get('email'):
|
||||
user = User.objects.get(email=request.GET.get('email'))
|
||||
else:
|
||||
raise Exception('no user specified')
|
||||
except User.DoesNotExist:
|
||||
raise Exception('unknown user')
|
||||
|
||||
item.user = user
|
||||
if request.GET.get('regie_id'):
|
||||
item.regie = Regie.objects.get(id=request.GET.get('regie_id'))
|
||||
else:
|
||||
# if there's no regie specified, use the first one we get from the
|
||||
# database...
|
||||
item.regie = Regie.objects.all()[0]
|
||||
|
||||
item.subject = request_body.get('display_name')
|
||||
item.source_url = request_body.get('url')
|
||||
|
||||
item.save()
|
||||
|
||||
response = HttpResponse(content_type='application/json')
|
||||
response.write(json.dumps({'result': 'success'}))
|
||||
return response
|
||||
|
||||
class PayView(View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
regie_id = request.POST.get('regie')
|
||||
if regie_id:
|
||||
regie = Regie.objects.get(pk=regie_id)
|
||||
if regie.is_remote():
|
||||
items = []
|
||||
remote_items_data = []
|
||||
# get all items data from regie webservice
|
||||
for item in request.POST.getlist('item'):
|
||||
remote_items_data.append(regie.get_item(request, item))
|
||||
remote_items = ','.join([x.id for x in remote_items_data])
|
||||
else:
|
||||
items = BasketItem.objects.filter(id__in=request.POST.getlist('item'), regie=regie)
|
||||
remote_items = ''
|
||||
else:
|
||||
items = BasketItem.objects.filter(id__in=request.POST.getlist('item'))
|
||||
# XXX: check all items are going to the same regie
|
||||
regie = items[0].regie
|
||||
remote_items = ''
|
||||
|
||||
transaction = Transaction()
|
||||
if request.user.is_authenticated():
|
||||
transaction.user = request.user
|
||||
else:
|
||||
transaction.user = None
|
||||
transaction.save()
|
||||
transaction.regie = regie
|
||||
transaction.items = items
|
||||
transaction.remote_items = remote_items
|
||||
transaction.status = 0
|
||||
|
||||
if remote_items:
|
||||
total_amount = sum([x.amount for x in remote_items_data])
|
||||
else:
|
||||
total_amount = sum([x.amount for x in items])
|
||||
transaction.amount = total_amount
|
||||
transaction.save()
|
||||
|
||||
if total_amount < regie.payment_min_amount:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
payment = eopayment.Payment(regie.service, regie.service_options)
|
||||
return_url = request.build_absolute_uri(
|
||||
reverse('lingo-callback', kwargs={'regie_pk': regie.id}))
|
||||
(order_id, kind, data) = payment.request(total_amount,
|
||||
email=request.user.email,
|
||||
next_url=return_url)
|
||||
transaction.order_id = order_id
|
||||
transaction.save()
|
||||
|
||||
# XXX: mark basket items as being processed (?)
|
||||
|
||||
if kind == eopayment.URL:
|
||||
return HttpResponseRedirect(data)
|
||||
elif kind == eopayment.FORM:
|
||||
return TemplateResponse(request, 'lingo/payment_form.html', {'form': data})
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CallbackView(View):
|
||||
def handle_callback(self, request, backend_response, **kwargs):
|
||||
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
|
||||
payment = eopayment.Payment(regie.service, regie.service_options)
|
||||
payment_response = payment.response(backend_response)
|
||||
if not payment_response.result == eopayment.CANCELLED:
|
||||
# cancellation are not signed...
|
||||
assert payment_response.signed is True
|
||||
|
||||
transaction = Transaction.objects.get(order_id=payment_response.order_id)
|
||||
transaction.status = payment_response.result
|
||||
transaction.bank_data = payment_response.bank_data
|
||||
transaction.end_date = timezone.now()
|
||||
transaction.save()
|
||||
|
||||
# check if transaction belongs to right regie
|
||||
assert transaction.regie == regie
|
||||
|
||||
if payment_response.result != eopayment.PAID:
|
||||
return HttpResponse()
|
||||
|
||||
for item in transaction.items.all():
|
||||
item.payment_date = transaction.end_date
|
||||
item.save()
|
||||
try:
|
||||
item.notify()
|
||||
except:
|
||||
# ignore errors, it will be retried later on if it fails
|
||||
pass
|
||||
if transaction.remote_items:
|
||||
for item in transaction.remote_items.split(','):
|
||||
regie.pay_item(request, item, transaction.order_id,
|
||||
transaction.end_date)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.handle_callback(request, request.environ['QUERY_STRING'], **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.handle_callback(request, request.body, **kwargs)
|
||||
|
||||
@csrf_exempt
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(CallbackView, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class ReturnView(View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
|
||||
payment = eopayment.Payment(regie.service, regie.service_options)
|
||||
try:
|
||||
payment_response = payment.response(request.environ['QUERY_STRING'])
|
||||
except:
|
||||
# if eopayment can't get response from query string redirect to
|
||||
# homepage
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
transaction = Transaction.objects.get(order_id=payment_response.order_id)
|
||||
|
||||
if transaction.items:
|
||||
# redirect to first transaction local item view
|
||||
return HttpResponseRedirect(reverse('view-item',
|
||||
kwargs={'regie_id': regie.pk,
|
||||
'item_id': transaction.items[0].id}))
|
||||
if transaction.remote_items:
|
||||
# redirect to first transaction remote item view
|
||||
item = transaction.remote_items.split(',')[0]
|
||||
return HttpResponseRedirect(reverse('view-item',
|
||||
kwargs={'regie_id': regie.pk,
|
||||
'item_id': item.id}))
|
||||
|
||||
|
||||
class ItemDownloadView(View):
|
||||
http_method_names = [u'get']
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
regie = Regie.objects.get(pk=kwargs['regie_id'])
|
||||
data = regie.download_item(request, kwargs['item_id'])
|
||||
r = HttpResponse(data, content_type='application/pdf')
|
||||
r['Content-Disposition'] = 'attachment; filename="%(item_id)s.pdf"' % kwargs
|
||||
return r
|
||||
|
||||
|
||||
class ItemView(TemplateView):
|
||||
http_method_names = [u'get']
|
||||
template_name = 'lingo/combo/item.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
regie = Regie.objects.get(pk=kwargs['regie_id'])
|
||||
item = regie.get_item(self.request, kwargs['item_id'])
|
||||
return {'item': item, 'regie': regie}
|
|
@ -64,6 +64,7 @@ INSTALLED_APPS = (
|
|||
'combo.apps.publik',
|
||||
'combo.apps.family',
|
||||
'combo.apps.dataviz',
|
||||
'combo.apps.lingo',
|
||||
'xstatic.pkg.chartnew_js',
|
||||
)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ Depends: ${misc:Depends}, ${python:Depends},
|
|||
python-django-cmsplugin-blurp,
|
||||
python-xstatic-chartnew-js
|
||||
Recommends: python-django-mellon
|
||||
Conflicts: python-lingo
|
||||
Description: Portal Management System (Python module)
|
||||
|
||||
Package: combo
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import RequestFactory
|
||||
from django.template import Context
|
||||
from django.utils import timezone
|
||||
|
||||
from combo.data.models import Page
|
||||
from combo.apps.lingo.models import Regie, BasketItem, Transaction
|
||||
from combo.apps.lingo.models import LingoBasketCell, LingoRecentTransactionsCell
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
try:
|
||||
user = User.objects.get(username='admin')
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.create_user('admin', email=None, password='admin')
|
||||
return user
|
||||
|
||||
@pytest.fixture
|
||||
def regie():
|
||||
try:
|
||||
regie = Regie.objects.get(slug='test')
|
||||
except Regie.DoesNotExist:
|
||||
regie = Regie()
|
||||
regie.label = 'Test'
|
||||
regie.slug = 'test'
|
||||
regie.description = 'test'
|
||||
regie.save()
|
||||
return regie
|
||||
|
||||
def test_cell_disabled():
|
||||
Regie.objects.all().delete()
|
||||
assert LingoBasketCell.is_enabled() is False
|
||||
assert LingoRecentTransactionsCell.is_enabled() is False
|
||||
|
||||
def test_cell_enabled(regie):
|
||||
assert LingoBasketCell.is_enabled() is True
|
||||
assert LingoRecentTransactionsCell.is_enabled() is True
|
||||
|
||||
def test_basket_cell(regie, user):
|
||||
page = Page(title='xxx', slug='test_basket_cell', template_name='standard')
|
||||
page.save()
|
||||
cell = LingoBasketCell(page=page, placeholder='content', order=0)
|
||||
|
||||
context = Context({'request': RequestFactory().get('/')})
|
||||
context['request'].user = None
|
||||
assert cell.is_relevant(context) is False
|
||||
context['request'].user = user
|
||||
assert cell.is_relevant(context) is False
|
||||
item = BasketItem()
|
||||
item.user = user
|
||||
item.regie = regie
|
||||
item.subject = 'foo'
|
||||
item.source_url = 'http://example.net'
|
||||
item.amount = 12345
|
||||
item.save()
|
||||
assert cell.is_relevant(context) is True
|
||||
|
||||
item.cancellation_date = timezone.now()
|
||||
item.save()
|
||||
assert cell.is_relevant(context) is False
|
||||
|
||||
item.cancellation_date = None
|
||||
item.save()
|
||||
|
||||
content = cell.render(context)
|
||||
assert '12345' in content
|
|
@ -0,0 +1,68 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from webtest import TestApp
|
||||
import pytest
|
||||
|
||||
from combo.apps.lingo.models import Regie
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user():
|
||||
try:
|
||||
user = User.objects.get(username='admin')
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.create_superuser('admin', email=None, password='admin')
|
||||
return user
|
||||
|
||||
def login(app, username='admin', password='admin'):
|
||||
login_page = app.get('/login/')
|
||||
login_form = login_page.forms[0]
|
||||
login_form['username'] = username
|
||||
login_form['password'] = password
|
||||
resp = login_form.submit()
|
||||
assert resp.status_int == 302
|
||||
return app
|
||||
|
||||
def test_access(admin_user):
|
||||
app = login(TestApp(get_wsgi_application()))
|
||||
resp = app.get('/manage/', status=200)
|
||||
assert '/manage/lingo/' in resp.body
|
||||
|
||||
def test_add_regie(admin_user):
|
||||
Regie.objects.all().delete()
|
||||
app = login(TestApp(get_wsgi_application()))
|
||||
resp = app.get('/manage/lingo/regies/', status=200)
|
||||
resp = resp.click('New')
|
||||
resp.forms[0]['label'] = 'Test'
|
||||
resp.forms[0]['slug'] = 'test'
|
||||
resp.forms[0]['description'] = 'description'
|
||||
resp.forms[0]['service'] = 'dummy'
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://localhost:80/manage/lingo/regies/'
|
||||
assert Regie.objects.count() == 1
|
||||
regie = Regie.objects.all()[0]
|
||||
assert regie.label == 'Test'
|
||||
|
||||
def test_edit_regie(admin_user):
|
||||
test_add_regie(admin_user)
|
||||
app = login(TestApp(get_wsgi_application()))
|
||||
resp = app.get('/manage/lingo/regies/', status=200)
|
||||
resp = resp.click('Test')
|
||||
resp.forms[0]['description'] = 'other description'
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://localhost:80/manage/lingo/regies/'
|
||||
assert Regie.objects.count() == 1
|
||||
regie = Regie.objects.all()[0]
|
||||
assert regie.description == 'other description'
|
||||
|
||||
def test_delete_regie(admin_user):
|
||||
test_add_regie(admin_user)
|
||||
app = login(TestApp(get_wsgi_application()))
|
||||
resp = app.get('/manage/lingo/regies/', status=200)
|
||||
resp = resp.click('Test')
|
||||
resp = resp.click('Delete')
|
||||
assert 'Are you sure you want to delete this?' in resp.body
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://localhost:80/manage/lingo/regies/'
|
||||
assert Regie.objects.count() == 0
|
|
@ -0,0 +1,147 @@
|
|||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
import urlparse
|
||||
import urllib
|
||||
from decimal import Decimal
|
||||
import json
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from webtest import TestApp
|
||||
|
||||
from django.test import Client
|
||||
|
||||
from combo.apps.lingo.models import Regie, BasketItem, Transaction, RemoteItem
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
client = Client()
|
||||
|
||||
@pytest.fixture
|
||||
def regie():
|
||||
try:
|
||||
regie = Regie.objects.get(slug='test')
|
||||
except Regie.DoesNotExist:
|
||||
regie = Regie()
|
||||
regie.label = 'Test'
|
||||
regie.slug = 'test'
|
||||
regie.description = 'test'
|
||||
regie.payment_min_amount = Decimal(4.5)
|
||||
regie.service = 'dummy'
|
||||
regie.service_options = {'siret': '1234'}
|
||||
regie.save()
|
||||
return regie
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
try:
|
||||
user = User.objects.get(username='admin')
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.create_user('admin', email=None, password='admin')
|
||||
return user
|
||||
|
||||
def login(username='admin', password='admin'):
|
||||
resp = client.post('/login/', {'username': username, 'password': password})
|
||||
assert resp.status_code == 302
|
||||
|
||||
def test_payment_min_amount(regie, user):
|
||||
items = {'item1': {'amount': '1.5', 'source_url': '/item/1'},
|
||||
'item2': {'amount': '2.4', 'source_url': '/item/2'}
|
||||
}
|
||||
b_items = []
|
||||
for subject, details in items.iteritems():
|
||||
b_item = BasketItem.objects.create(user=user, regie=regie,
|
||||
subject=subject, **details)
|
||||
b_items.append(b_item.pk)
|
||||
login()
|
||||
resp = client.post(reverse('lingo-pay'), {'item': b_items, 'regie': regie.pk})
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_successfull_items_payment(regie, user):
|
||||
items = {'item1': {'amount': '10.5', 'source_url': '/item/1'},
|
||||
'item2': {'amount': '42', 'source_url': '/item/2'},
|
||||
'item3': {'amount': '100', 'source_url': '/item/3'},
|
||||
'item4': {'amount': '354', 'source_url': '/item/4'}
|
||||
}
|
||||
b_items = []
|
||||
for subject, details in items.iteritems():
|
||||
b_item = BasketItem.objects.create(user=user, regie=regie,
|
||||
subject=subject, **details)
|
||||
b_items.append(b_item.pk)
|
||||
login()
|
||||
resp = client.post(reverse('lingo-pay'), {'item': b_items, 'regie': regie.pk})
|
||||
assert resp.status_code == 302
|
||||
location = resp.get('location')
|
||||
assert 'dummy-payment' in location
|
||||
parsed = urlparse.urlparse(location)
|
||||
# get return_url and transaction id from location
|
||||
qs = urlparse.parse_qs(parsed.query)
|
||||
args = {'transaction_id': qs['transaction_id'][0], 'signed': True,
|
||||
'ok': True, 'reason': 'Paid'}
|
||||
# simulate backend callback call
|
||||
resp = client.get(qs['return_url'][0], args)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_add_amount_to_basket(regie, user):
|
||||
user_email = 'foo@example.com'
|
||||
User.objects.get_or_create(email=user_email)
|
||||
amount = 42
|
||||
data = {'amount': amount, 'display_name': 'test amount',
|
||||
'url': 'http://example.com'}
|
||||
url = '%s?email=%s' % (reverse('api-add-basket-item'), user_email)
|
||||
resp = client.post(url, json.dumps(data), content_type='application/json')
|
||||
assert resp.status_code == 200
|
||||
assert json.loads(resp.content) == {'result': 'success'}
|
||||
assert BasketItem.objects.filter(amount=amount).exists()
|
||||
|
||||
data['extra'] = {'amount': '22.22'}
|
||||
resp = client.post(url, json.dumps(data), content_type='application/json')
|
||||
assert resp.status_code == 200
|
||||
assert json.loads(resp.content) == {'result': 'success'}
|
||||
assert BasketItem.objects.filter(amount=Decimal('64.22')).exists()
|
||||
|
||||
data['amount'] = [amount]
|
||||
data['extra'] = {'amount': ['22.22', '12']}
|
||||
resp = client.post('%s&amount=5' % url, json.dumps(data),
|
||||
content_type='application/json')
|
||||
assert resp.status_code == 200
|
||||
assert json.loads(resp.content) == {'result': 'success'}
|
||||
assert BasketItem.objects.filter(amount=Decimal('81.22')).exists()
|
||||
|
||||
|
||||
def test_payment_callback(regie, user):
|
||||
item = BasketItem.objects.create(user=user, regie=regie,
|
||||
subject='test_item', amount='10.5',
|
||||
source_url='/testitem')
|
||||
login()
|
||||
resp = client.post(reverse('lingo-pay'), {'item': [item.pk],
|
||||
'regie': regie.pk})
|
||||
assert resp.status_code == 302
|
||||
location = resp.get('location')
|
||||
parsed = urlparse.urlparse(location)
|
||||
qs = urlparse.parse_qs(parsed.query)
|
||||
transaction_id = qs['transaction_id'][0]
|
||||
data = {'transaction_id': transaction_id, 'signed': True,
|
||||
'amount': qs['amount'][0], 'ok': True}
|
||||
|
||||
# call callback with GET
|
||||
get_resp = client.get(qs['return_url'][0], data)
|
||||
assert get_resp.status_code == 200
|
||||
assert Transaction.objects.get(order_id=transaction_id).status == 3
|
||||
|
||||
resp = client.post(reverse('lingo-pay'), {'item': [item.pk],
|
||||
'regie': regie.pk})
|
||||
assert resp.status_code == 302
|
||||
location = resp.get('location')
|
||||
parsed = urlparse.urlparse(location)
|
||||
qs = urlparse.parse_qs(parsed.query)
|
||||
transaction_id = qs['transaction_id'][0]
|
||||
data = {'transaction_id': transaction_id, 'signed': True,
|
||||
'amount': qs['amount'][0], 'ok': True}
|
||||
|
||||
# call callback with POST
|
||||
post_resp = client.post(qs['return_url'][0], urllib.urlencode(data),
|
||||
content_type='text/html')
|
||||
assert post_resp.status_code == 200
|
||||
assert Transaction.objects.get(order_id=transaction_id).status == 3
|
Loading…
Reference in New Issue