lingo: add cell to self declare and pay invoices (#13492)

This commit is contained in:
Frédéric Péters 2016-10-20 10:04:59 +02:00
parent 003d1315be
commit d107771bba
8 changed files with 239 additions and 14 deletions

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import ckeditor.fields
class Migration(migrations.Migration):
dependencies = [
('data', '0020_auto_20160928_1152'),
('lingo', '0023_auto_20160928_1152'),
]
operations = [
migrations.CreateModel(
name='SelfDeclaredInvoicePayment',
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)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
('title', models.CharField(max_length=200, verbose_name='Title', blank=True)),
('text', ckeditor.fields.RichTextField(null=True, verbose_name='Text', blank=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page')),
],
options={
'verbose_name': 'Self declared invoice payment',
},
),
]

View File

@ -33,7 +33,7 @@ from django.db import models
from django.forms import models as model_forms, Select
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.exceptions import PermissionDenied
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.utils.http import urlencode
from ckeditor.fields import RichTextField
@ -61,7 +61,7 @@ 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'],
display_id=data.get('display_id'),
total_amount=data.get('total_amount'),
amount=data.get('amount'),
subject=data.get('label'),
@ -137,8 +137,13 @@ class Regie(models.Model):
if not self.is_remote():
return self.basketitem_set.get(pk=invoice_id)
url = self.webservice_url + '/invoice/%s/' % invoice_id
item = requests.get(url, user=user, remote_service='auto').json()
return build_remote_item(item.get('data'), self)
response = requests.get(url, user=user, remote_service='auto')
if response.status_code == 404:
raise ObjectDoesNotExist()
response.raise_for_status()
if response.json().get('data') is None:
raise ObjectDoesNotExist()
return build_remote_item(response.json().get('data'), self)
def get_invoice_pdf(self, user, invoice_id):
"""
@ -219,7 +224,7 @@ class RemoteItem(object):
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.display_id = display_id or self.id
self.subject = subject
self.has_pdf = has_pdf
self.online_payment = online_payment
@ -237,7 +242,7 @@ class RemoteItem(object):
@property
def crypto_id(self):
return aes_hex_encrypt(settings.SECRET_KEY, self.id)
return aes_hex_encrypt(settings.SECRET_KEY, str(self.id))
class Transaction(models.Model):
@ -411,7 +416,7 @@ class Items(CellBase):
@classmethod
def is_enabled(cls):
return Regie.objects.count() > 0
return Regie.objects.exclude(webservice_url='').count() > 0
def is_relevant(self, context):
return (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated())
@ -419,9 +424,9 @@ class Items(CellBase):
def get_default_form_class(self):
fields = ['title', 'text']
widgets = {}
if Regie.objects.count() > 1:
if Regie.objects.exclude(webservice_url='').count() > 1:
regies = [('', _('All'))]
regies.extend([(r.slug, r.label) for r in Regie.objects.all()])
regies.extend([(r.slug, r.label) for r in Regie.objects.exclude(webservice_url='')])
widgets['regie'] = Select(choices=regies)
fields.insert(0, 'regie')
return model_forms.modelform_factory(self.__class__, fields=fields, widgets=widgets)
@ -431,8 +436,12 @@ class Items(CellBase):
return [Regie.objects.get(slug=self.regie)]
return Regie.objects.all()
def get_invoices(self, user):
return []
def get_cell_extra_context(self, context):
ctx = {'title': self.title, 'text': self.text}
ctx = super(Items, self).get_cell_extra_context(context)
ctx.update({'title': self.title, 'text': self.text})
items = self.get_invoices(user=context['user'])
items.sort(key=lambda i: i.creation_date, reverse=True)
ctx.update({'items': items})
@ -469,3 +478,20 @@ class ActiveItems(Items):
for r in self.get_regies():
items.extend(r.get_invoices(user))
return items
@register_cell_class
class SelfDeclaredInvoicePayment(Items):
user_dependant = False
template_name = 'lingo/combo/self-declared-invoice-payment.html'
class Meta:
verbose_name = _('Self declared invoice payment')
def is_relevant(self, context):
return self.is_enabled()
def render(self, context):
context['synchronous'] = True
context['page_path'] = context['request'].path
return super(Items, self).render(context)

View File

@ -0,0 +1,27 @@
{% load i18n %}
{% if title %}<h2>{{ title }}</h2>{% endif %}
{% if text %}<p>{{ text|safe }}</p>{% endif %}
<form class="quixote" action="{% url 'lingo-self-invoice' cell_id=cell.id %}">
<div class="widget">
<div class="title">
{% trans 'Invoice Number' %}
<span class="required">*</span>
</div>
<div class="content">
<input type="text" name="invoice-number" required="required">
</div>
</div>
<div class="widget">
<div class="title">
{% trans 'Invoice Amount' %}
<span class="required">*</span>
</div>
<div class="content">
<input type="text" name="invoice-amount" required="required" pattern="[1-9][0-9]*([,\.]?[0-9]+)?">
</div>
</div>
<input type="hidden" name="page_path" value="{{page_path}}">
<div class="buttons">
<button>{% trans "Pay" %}</button>
</div>
</form>

View File

@ -21,7 +21,7 @@ from combo.urls_utils import decorated_includes, manager_required
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
ReturnView, ItemDownloadView, ItemView, CancelItemView,
RemoveBasketItemApiView, ValidateTransactionApiView,
CancelTransactionApiView)
CancelTransactionApiView, SelfInvoiceView)
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
RegieDeleteView, TransactionListView, ManagerHomeView)
@ -56,4 +56,6 @@ urlpatterns = patterns('',
ItemDownloadView.as_view(), name='download-item-pdf'),
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$',
ItemView.as_view(), name='view-item'),
url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(),
name='lingo-self-invoice'),
)

View File

@ -21,12 +21,13 @@ import logging
import requests
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.http import HttpResponseForbidden, Http404
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.encoding import force_text
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View, DetailView, ListView, TemplateView
from django.conf import settings
@ -45,7 +46,7 @@ except ImportError:
UserSAMLIdentifier = None
from .models import (Regie, BasketItem, Transaction, TransactionOperation,
LingoBasketCell)
LingoBasketCell, SelfDeclaredInvoicePayment)
def get_eopayment_object(request, regie):
options = regie.service_options
@ -578,3 +579,47 @@ class CancelItemView(DetailView):
except requests.exceptions.HTTPError:
messages.error(request, _('An error occured when removing the item.'))
return HttpResponseRedirect(get_basket_url())
class SelfInvoiceView(View):
http_method_names = ['get', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(SelfInvoiceView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
try:
obj = SelfDeclaredInvoicePayment.objects.get(id=kwargs['cell_id'])
except SelfDeclaredInvoicePayment.DoesNotExist:
raise Http404()
invoice_id = request.GET.get('invoice-number', '')
invoice_amount = request.GET.get('invoice-amount', '').replace(',', '.')
msg = None
url = None
try:
invoice_amount = Decimal(invoice_amount)
except ArithmeticError:
invoice_amount = '-'
msg = _('Sorry, the provided amount is invalid.')
else:
for regie in obj.get_regies():
try:
invoice = regie.get_invoice(None, invoice_id)
except ObjectDoesNotExist:
continue
if invoice.total_amount != invoice_amount:
continue
url = reverse('view-item', kwargs={
'regie_id': regie.id, 'item_crypto_id': invoice.crypto_id})
break
else:
msg = _('Sorry, no invoice were found with that number and amount.')
if request.GET.get('ajax') == 'on':
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'url': url, 'msg': msg and force_text(msg)}))
return response
if url:
return HttpResponseRedirect(url)
messages.warning(request, msg)
return HttpResponseRedirect(request.GET.get('page_path') or '/')

View File

@ -130,4 +130,39 @@ $(function() {
}
}
});
$('.selfdeclaredinvoicepayment form').on('combo:load-invoice', window.displayPopup);
$('.selfdeclaredinvoicepayment form').on('submit', function(event) {
var url = $(this).attr('action');
var data = $(this).serialize();
var $form = $(this);
$.ajax({url: url + '?ajax=on',
xhrFields: { withCredentials: true },
data: data,
async: true,
dataType: 'json',
success: function(data) {
if (data.msg) {
var err_msg = $('<p></p>');
err_msg.text(data.msg);
$(err_msg).dialog({
dialogClass: 'alert',
modal: true,
width: 'auto',
buttons: {
'×': function() {
$(this).dialog('destroy');
}
},
});
return;
}
$form.data('url', data.url);
$form.trigger('combo:load-invoice', event);
},
error: function(error) {
window.console && console.log(':(', error);
}
});
return false;
});
});

View File

@ -14,6 +14,19 @@
<div id="title"><h1>{{ page.title }}</h1></div>
<div id="menu">{% block menu %}{% show_menu %}{% endblock %}</div>
<div id="content">
{% block messages %}
{% if messages %}
<div id="messages">
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block combo-content %}
{% placeholder "content" %}
{% endblock %}

View File

@ -12,7 +12,7 @@ from django.utils import timezone
from combo.utils import check_query, aes_hex_encrypt
from combo.data.models import Page
from combo.apps.lingo.models import Regie, ActiveItems
from combo.apps.lingo.models import Regie, ActiveItems, SelfDeclaredInvoicePayment
pytestmark = pytest.mark.django_db
@ -173,3 +173,44 @@ def test_wrong_crypted_item(mock_get, remote_regie, app):
mock_json.json.return_value = {'err': 0, 'data': INVOICES[0]}
mock_get.return_value = mock_json
resp = app.get('/lingo/item/%s/%s/' % (remote_regie.id, 'zrzer854sfaear45e6rzerzerzef'), status=404)
@mock.patch('combo.apps.lingo.models.requests.get')
def test_self_declared_invoice(mock_get, app, remote_regie):
mock_json = mock.Mock()
mock_json.json.return_value = {'err': 0, 'data': INVOICES[0]}
mock_get.return_value = mock_json
page = Page(title='xxx', slug='test-self-invoice', template_name='standard')
page.save()
cell = SelfDeclaredInvoicePayment(regie='remote', page=page, placeholder='content', order=0)
cell.save()
resp = app.get('/test-self-invoice/')
resp = resp.form.submit().follow()
assert 'Sorry, no invoice were found with that number and amount.'
resp = app.get('/test-self-invoice/')
resp.form['invoice-number'] = 'F201601'
resp.form['invoice-amount'] = 'FOOBAR' # wrong format
resp = resp.form.submit().follow()
assert 'Sorry, the provided amount is invalid.'
resp = app.get('/test-self-invoice/')
resp.form['invoice-number'] = 'F201602' # invalid number
resp.form['invoice-amount'] = '123.45'
resp = resp.form.submit().follow()
assert 'Sorry, no invoice were found with that number and amount.'
resp = app.get('/test-self-invoice/')
resp.form['invoice-number'] = 'F201601'
resp.form['invoice-amount'] = '123.46' # invalid amount
resp = resp.form.submit().follow()
assert 'Sorry, no invoice were found with that number and amount.'
resp = app.get('/test-self-invoice/')
resp.form['invoice-number'] = 'F201601'
resp.form['invoice-amount'] = '123.45'
resp = resp.form.submit()
path = urlparse.urlparse(resp.location).path
assert path.startswith('/lingo/item/%s/' % remote_regie.id)
resp = resp.follow()