Projet

Général

Profil

Télécharger (22,8 ko) Statistiques
| Branche: | Tag: | Révision:

root / auquotidien / modules / payments_ui.py @ 8b02623d

1
import time
2
import pprint
3
import locale
4
import decimal
5
import datetime
6

    
7
from quixote import get_request, get_response, get_session, redirect
8
from quixote.directory import Directory, AccessControlled
9
from quixote.html import TemplateIO, htmltext
10

    
11
import wcs
12
import wcs.admin.root
13
from wcs.formdef import FormDef
14

    
15
from qommon import _
16
from qommon import errors, misc, template, get_logger
17
from qommon.form import *
18
from qommon.strftime import strftime
19
from qommon.admin.emails import EmailsDirectory
20
from qommon.backoffice.menu import html_top
21
from qommon import get_cfg
22

    
23
from payments import (eopayment, Regie, is_payment_supported, Invoice,
24
        Transaction, notify_paid_invoice)
25

    
26
from qommon.admin.texts import TextsDirectory
27

    
28
if not set:
29
    from sets import Set as set
30

    
31
def invoice_as_html(invoice):
32
    r = TemplateIO(html=True)
33
    r += htmltext('<div id="invoice">')
34
    r += htmltext('<h2>%s</h2>') % _('Invoice: %s') % invoice.subject
35
    r += htmltext('<h3>%s') % _('Amount: %s') % invoice.amount
36
    r += htmltext(' &euro;</h3>')
37
    r += htmltext('<!-- DEBUG \n')
38
    r += 'Invoice:\n'
39
    r += pprint.pformat(invoice.__dict__)
40
    for transaction in Transaction.get_with_indexed_value('invoice_ids', invoice.id):
41
        r += '\nTransaction:\n'
42
        r += pprint.pformat(transaction.__dict__)
43
    r += htmltext('\n-->')
44
    if invoice.formdef_id and invoice.formdata_id and \
45
            get_session().user == invoice.user_id:
46
        formdef = FormDef.get(invoice.formdef_id)
47
        if formdef:
48
            formdata = formdef.data_class().get(invoice.formdata_id, ignore_errors=True)
49
            if formdata:
50
                name = _('%(form_name)s #%(formdata_id)s') % {
51
                        'form_name': formdata.formdef.name,
52
                        'formdata_id': formdata.id }
53
                r += htmltext('<p class="from">%s <a href="%s">%s</a></p>') % (_('From:'), formdata.get_url(), name)
54
    r += htmltext('<p class="regie">%s</p>') % _('Regie: %s') % Regie.get(invoice.regie_id).label
55
    r += htmltext('<p class="date">%s</p>') % _('Created on: %s') % misc.localstrftime(invoice.date)
56
    if invoice.details:
57
        r += htmltext('<p class="details">%s</p>') % _('Details:')
58
        r += htmltext('<div class="details">')
59
        r += htmltext(invoice.details)
60
        r += htmltext('</div>')
61
    if invoice.canceled:
62
        r += htmltext('<p class="canceled">')
63
        r += '%s' % _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
64
        if invoice.canceled_reason:
65
            r += ' (%s)' % invoice.canceled_reason
66
        r += htmltext('</p>')
67
    if invoice.paid:
68
        r += htmltext('<p class="paid">%s</p>') % _('paid on %s') % misc.localstrftime(invoice.paid_date)
69
    r += htmltext('</div>')
70
    return r.getvalue()
71

    
72

    
73
class InvoicesDirectory(Directory):
74
    _q_exports = ['', 'multiple']
75

    
76
    def _q_traverse(self, path):
77
        if not is_payment_supported():
78
            raise errors.TraversalError()
79
        get_response().filter['bigdiv'] = 'profile'
80
        if get_session().user:
81
            # fake breadcrumb
82
            get_response().breadcrumb.append(('myspace/', _('My Space')))
83
            get_response().breadcrumb.append(('invoices/', _('Invoices')))
84
        return Directory._q_traverse(self, path)
85

    
86
    def multiple(self):
87
        invoice_ids = get_request().form.get('invoice')
88
        if type(invoice_ids) is not list:
89
            return redirect('%s' % invoice_ids)
90
        return redirect('+'.join(invoice_ids))
91

    
92
    def _q_lookup(self, component):
93
        if str('+') in component:
94
            invoice_ids = component.split(str('+'))
95
        else:
96
            invoice_ids = [component]
97
        for invoice_id in invoice_ids:
98
            if not Invoice.check_crc(invoice_id):
99
                raise errors.TraversalError()
100

    
101
        template.html_top(_('Invoices'))
102
        r = TemplateIO(html=True)
103
        r += TextsDirectory.get_html_text('aq-invoice')
104

    
105
        regies_id = set()
106
        for invoice_id in invoice_ids:
107
            try:
108
                invoice = Invoice.get(invoice_id)
109
            except KeyError:
110
                raise errors.TraversalError()
111
            r += invoice_as_html(invoice)
112
            if not (invoice.paid or invoice.canceled):
113
                regies_id.add(invoice.regie_id)
114

    
115
        if len(regies_id) == 1:
116
            r += htmltext('<p class="command">')
117
            r += htmltext('<a href="%s/payment/init?invoice_ids=%s">') % (
118
                    get_publisher().get_frontoffice_url(), component)
119
            if len(invoice_ids) > 1:
120
                r += _('Pay Selected Invoices')
121
            else:
122
                r += _('Pay')
123
            r += htmltext('</a></p>')
124
        if len(regies_id) > 1:
125
            r += _('You can not pay to different regies.')
126

    
127
        return r.getvalue()
128

    
129
    def _q_index(self):
130
        return redirect('..')
131

    
132

    
133
class RegieDirectory(Directory):
134
    _q_exports = ['', 'edit', 'delete', 'options']
135

    
136
    def __init__(self, regie):
137
        self.regie = regie
138

    
139
    def _q_index(self):
140
        html_top('payments', title = _('Regie: %s') % self.regie.label)
141
        r = TemplateIO(html=True)
142
        get_response().filter['sidebar'] = self.get_sidebar()
143
        r += htmltext('<h2>%s</h2>') % _('Regie: %s') % self.regie.label
144

    
145
        r += get_session().display_message()
146

    
147
        if self.regie.description:
148
            r += htmltext('<div class="bo-block">')
149
            r += htmltext('<p>')
150
            r += self.regie.description
151
            r += htmltext('</p>')
152
            r += htmltext('</div>')
153

    
154
        if self.regie.service:
155
            r += htmltext('<div class="bo-block">')
156
            url = get_publisher().get_frontoffice_url() + '/payment/back_asynchronous/'
157
            url += str(self.regie.id)
158
            r += htmltext('<p>')
159
            r += '%s %s' % (_('Banking Service:'), self.regie.service)
160
            r += htmltext(' (<a href="options">%s</a>)') % _('options')
161
            r += htmltext('</p>')
162
            r += htmltext('<p>')
163
            r += '%s %s' % (_('Payment notification URL:'), url)
164
            r += htmltext('</div>')
165

    
166
        r += self.invoice_listing()
167
        return r.getvalue()
168

    
169
    def get_sidebar(self):
170
        r = TemplateIO(html=True)
171
        r += htmltext('<ul>')
172
        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
173
        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
174
        r += htmltext('</ul>')
175
        return r.getvalue()
176

    
177
    def edit(self):
178
        form = self.form()
179
        if form.get_submit() == 'cancel':
180
            return redirect('.')
181

    
182
        if form.is_submitted() and not form.has_errors():
183
            self.submit(form)
184
            return redirect('..')
185

    
186
        html_top('payments', title = _('Edit Regie: %s') % self.regie.label)
187
        r = TemplateIO(html=True)
188
        r += htmltext('<h2>%s</h2>') % _('Edit Regie: %s') % self.regie.label
189
        r += form.render()
190
        return r.getvalue()
191

    
192

    
193
    def form(self):
194
        form = Form(enctype='multipart/form-data')
195
        form.add(StringWidget, 'label', title=_('Label'), required=True,
196
                value=self.regie.label)
197
        form.add(TextWidget, 'description', title=_('Description'),
198
                value=self.regie.description, rows=5, cols=60)
199
        form.add(SingleSelectWidget, 'service', title=_('Banking Service'),
200
                value=self.regie.service, required=True,
201
                options = [
202
                        ('dummy', _('Dummy (for tests)')),
203
                        ('sips', 'SIPS'),
204
                        ('systempayv2', 'systempay (Banque Populaire)'),
205
                        ('spplus', _('SP+ (Caisse d\'epargne)'))])
206
        form.add_submit('submit', _('Submit'))
207
        form.add_submit('cancel', _('Cancel'))
208
        return form
209

    
210
    def submit(self, form):
211
        for k in ('label', 'description', 'service'):
212
            widget = form.get_widget(k)
213
            if widget:
214
                setattr(self.regie, k, widget.parse())
215
        self.regie.store()
216

    
217
    def delete(self):
218
        form = Form(enctype='multipart/form-data')
219
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
220
                        'You are about to irrevocably delete this regie.')))
221
        form.add_submit('submit', _('Submit'))
222
        form.add_submit('cancel', _('Cancel'))
223
        if form.get_submit() == 'cancel':
224
            return redirect('..')
225
        if not form.is_submitted() or form.has_errors():
226
            get_response().breadcrumb.append(('delete', _('Delete')))
227
            r = TemplateIO(html=True)
228
            html_top('payments', title = _('Delete Regie'))
229
            r += htmltext('<h2>%s</h2>') % _('Deleting Regie: %s') % self.regie.label
230
            r += form.render()
231
            return r.getvalue()
232
        else:
233
            self.regie.remove_self()
234
            return redirect('..')
235

    
236
    def option_form(self):
237
        form = Form(enctype='multipart/form-data')
238
        module = eopayment.get_backend(self.regie.service)
239
        service_options = {}
240
        for infos in module.description['parameters']:
241
            if 'default' in infos:
242
                service_options[infos['name']] = infos['default']
243
        service_options.update(self.regie.service_options or {})
244

    
245
        banking_titles = {
246
            ('dummy', 'direct_notification_url'): N_('Direct Notification URL'),
247
            ('dummy', 'siret'): N_('Dummy SIRET'),
248
        }
249

    
250
        for infos in module.description['parameters']:
251
            name = infos['name']
252
            caption = infos.get('caption', name).encode(get_publisher().site_charset)
253
            title = banking_titles.get((self.regie.service, name), caption)
254
            kwargs = {}
255
            widget = StringWidget
256
            if infos.get('help_text') is not None:
257
                kwargs['hint'] = _(infos['help_text'].encode(get_publisher().site_charset))
258
            if infos.get('required', False):
259
                kwargs['required'] = True
260
            if infos.get('max_length') is not None:
261
                kwargs['size'] = infos['max_length']
262
            elif infos.get('length') is not None:
263
                kwargs['size'] = infos['length']
264
            else:
265
                kwargs['size'] = 80
266
            if kwargs['size'] > 100:
267
                widget = TextWidget
268
                kwargs['cols'] = 80
269
                kwargs['rows'] = 5
270
            if 'type' not in infos or infos['type'] is str:
271
                form.add(widget, name, title=_(title),
272
                         value=service_options.get(name), **kwargs)
273
            elif infos['type'] is bool:
274
                form.add(CheckboxWidget, name, title=title,
275
                         value=service_options.get(name), **kwargs)
276
        form.add_submit('submit', _('Submit'))
277
        form.add_submit('cancel', _('Cancel'))
278
        return form
279

    
280
    def options(self):
281
        r = TemplateIO(html=True)
282
        form = self.option_form()
283

    
284
        module = eopayment.get_backend(self.regie.service)
285
        try:
286
            r += htmltext('<!-- Payment backend description: \n')
287
            r += pprint.pformat(module.description)
288
            r += htmltext('-->')
289
        except:
290
            return template.error_page(_('Payment backend do not list its options'))
291
            raise errors.TraversalError()
292
        r += htmltext('<!-- \n')
293
        r += 'Service options\n'
294
        r += pprint.pformat(self.regie.service_options)
295
        r += htmltext('-->')
296

    
297
        if form.get_submit() == 'cancel':
298
            return redirect('.')
299

    
300
        if form.is_submitted() and not form.has_errors():
301
             if self.submit_options(form, module):
302
                return redirect('..')
303

    
304
        html_top('payments', title=_('Edit Service Options'))
305
        r += htmltext('<h2>%s</h2>') % _('Edit Service Options')
306
        r += form.render()
307
        return r.getvalue()
308

    
309
    def submit_options(self, form, module):
310
        # extra validation
311
        error = False
312
        for infos in module.description['parameters']:
313
            widget = form.get_widget(infos['name'])
314
            value = widget.parse()
315
            if value and 'validation' in infos:
316
                try:
317
                    if not infos['validation'](value):
318
                        widget.set_error(_('Valeur invalide'))
319
                        error = True
320
                except ValueError, e:
321
                    widget.set_error(_(e.message))
322
                    error = True
323
        if error:
324
            return False
325
        if not self.regie.service_options:
326
            self.regie.service_options = {}
327
        for infos in module.description['parameters']:
328
            name = infos['name']
329
            value = form.get_widget(name).parse()
330
            if value is None:
331
                value = ''
332
            if hasattr(value, 'strip'):
333
                value = value.strip()
334
            if infos.get('default') is not None:
335
                if value == infos['default']:
336
                    self.regie.service_options.pop(name, None)
337
                else:
338
                    self.regie.service_options[name] = form.get_widget(name).parse()
339
            elif not value:
340
                self.regie.service_options.pop(name, None)
341
            else:
342
                self.regie.service_options[name] = form.get_widget(name).parse()
343
        self.regie.store()
344
        return True
345

    
346
    PAGINATION = 50
347

    
348
    def monetary_amount(self, val):
349
        if not val:
350
            return ''
351
        if isinstance(val, basestring):
352
            val = val.replace(',', '.')
353
        return '%.2f' % decimal.Decimal(val)
354

    
355
    def get_sort_by(self):
356
        request = get_request()
357
        sort_by = request.form.get('sort_by')
358
        if sort_by not in ('date', 'paid_date', 'username'):
359
            sort_by = 'date'
360
        return sort_by
361

    
362
    def get_invoices(self):
363
        sort_by = self.get_sort_by()
364
        invoices = Invoice.get_with_indexed_value('regie_id', self.regie.id,
365
                ignore_errors=True)
366
        if 'date' in sort_by:
367
            reverse = True
368
            key = lambda i: getattr(i, sort_by) or datetime.datetime.now()
369
        else:
370
            reverse = False
371
            key = lambda i: getattr(i, sort_by) or ''
372
        invoices.sort(reverse=reverse, key=key)
373
        return invoices
374

    
375
    def unpay(self, request, invoice):
376
        get_logger().info(_('manually set unpaid invoice %(invoice_id)s in regie %(regie)s')
377
            % dict(invoice_id=invoice.id, regie=self.regie.id))
378
        transaction = Transaction()
379
        transaction.invoice_ids = [ invoice.id ]
380
        transaction.order_id = 'Manual action'
381
        transaction.start = datetime.datetime.now()
382
        transaction.end = transaction.start
383
        transaction.bank_data = { 
384
            'action': 'Set unpaid', 
385
            'by': request.user.get_display_name() + ' (%s)' % request.user.id
386
        }
387
        transaction.store()
388
        invoice.unpay()
389

    
390
    def pay(self, request, invoice):
391
        get_logger().info(_('manually set paid invoice %(invoice_id)s in regie %(regie)s')
392
            % dict(invoice_id=invoice.id, regie=self.regie.id))
393
        transaction = Transaction()
394
        transaction.invoice_ids = [ invoice.id ]
395
        transaction.order_id = 'Manual action'
396
        transaction.start = datetime.datetime.now()
397
        transaction.end = transaction.start
398
        transaction.bank_data = { 
399
            'action': 'Set paid', 
400
            'by': request.user.get_display_name() + ' (%s)' % request.user.id
401
        }
402
        transaction.store()
403
        invoice.pay()
404

    
405
    def invoice_listing(self):
406
        request = get_request()
407
        get_response().add_css_include('../../themes/auquotidien/admin.css')
408
        if request.get_method() == 'POST':
409
            invoice_id = request.form.get('id')
410
            invoice = Invoice.get(invoice_id, ignore_errors=True)
411
            if invoice:
412
                if 'unpay' in request.form:
413
                    self.unpay(request, invoice)
414
                elif 'pay' in request.form:
415
                    self.pay(request, invoice)
416
                return redirect('')
417
        try:
418
            offset = int(request.form.get('offset', 0))
419
        except ValueError:
420
            offset = 0
421
        r = TemplateIO(html=True)
422
        r += htmltext('<table id="invoice-listing" borderspacing="0">')
423
        r += htmltext('<thead>')
424
        r += htmltext('<tr>')
425
        r += htmltext('<td><a href="?sort_by=date&offset=%d">Creation</a></td>') % offset
426
        r += htmltext('<td>Amount</td>')
427
        r += htmltext('<td><a href="?sort_by=paid_date&offset=%d">Paid</a></td>') % offset
428
        r += htmltext('<td><a href="?sort_by=username&offset=%d">User</a></td>') % offset
429
        r += htmltext('<td>Titre</td>')
430
        r += htmltext('<td></td>')
431
        r += htmltext('</tr>')
432
        r += htmltext('</thead>')
433
        invoices = self.get_invoices()
434
        for invoice in invoices[offset:offset+self.PAGINATION]:
435
            r += htmltext('<tbody class="invoice-rows">')
436
            r += htmltext('<tr class="invoice-row"><td>')
437
            r += misc.localstrftime(invoice.date)
438
            r += htmltext('</td><td class="amount">')
439
            r += self.monetary_amount(invoice.amount)
440
            r += htmltext('</td><td>')
441
            if invoice.paid:
442
                r += misc.localstrftime(invoice.paid_date)
443
            else:
444
                r += ''
445
            r += htmltext('</td><td>')
446
            user = invoice.get_user()
447
            if user:
448
                r += user.name
449
            r += htmltext('</td><td class="subject">%s</td>') % (invoice.subject or '')
450
            r += htmltext('<td>')
451
            r += htmltext('<form method="post">')
452
            r += htmltext('<input type="hidden" name="id" value="%s"/> ') % invoice.id
453
            if invoice.paid:
454
                r += htmltext('<input type="submit" name="unpay" value="%s"/>') % _('Set unpaid')
455
            else:
456
                r += htmltext('<input type="submit" name="pay" value="%s"/>') % _('Set paid')
457
            r += htmltext('</form>')
458

    
459
            r += htmltext('</td></tr>')
460
            transactions = Transaction.get_with_indexed_value('invoice_ids',
461
                    invoice.id)
462
            for transaction in sorted(transactions, key=lambda x: x.start):
463
                r += htmltext('<tr>')
464
                r += htmltext('<td></td>')
465
                r += htmltext('<td colspan="5">')
466
                r += 'OrderID: %s' % transaction.order_id
467
                r += ' Start: %s' % transaction.start
468
                if transaction.end:
469
                    r += ' End: %s' % transaction.end
470
                if transaction.bank_data:
471
                    r += ' Bank data: %r' % transaction.bank_data
472
                r += htmltext('</td>')
473
                r += htmltext('</tr>')
474
            r += htmltext('</tbody>')
475
        r += htmltext('</tbody></table>')
476
        if offset != 0:
477
            r += htmltext('<a href="?offset=%d>%s</a> ') % (
478
                 max(0, offset-self.PAGINATION), _('Previous'))
479
        if offset + self.PAGINATION < len(invoices):
480
            r += htmltext('<a href="?offset=%d>%s</a> ') % (
481
                 max(0, offset-self.PAGINATION), _('Previous'))
482
        return r.getvalue()
483

    
484

    
485
class RegiesDirectory(Directory):
486
    _q_exports = ['', 'new']
487

    
488
    def _q_traverse(self, path):
489
        get_response().breadcrumb.append(('regie/', _('Regies')))
490
        return Directory._q_traverse(self, path)
491

    
492
    def _q_index(self):
493
        return redirect('..')
494

    
495
    def new(self):
496
        regie_ui = RegieDirectory(Regie())
497

    
498
        form = regie_ui.form()
499
        if form.get_submit() == 'cancel':
500
            return redirect('.')
501

    
502
        if form.is_submitted() and not form.has_errors():
503
            regie_ui.submit(form)
504
            return redirect('%s/' % regie_ui.regie.id)
505

    
506
        get_response().breadcrumb.append(('new', _('New Regie')))
507
        html_top('payments', title = _('New Regie'))
508
        r = TemplateIO(html=True)
509
        r += htmltext('<h2>%s</h2>') % _('New Regie')
510
        r += form.render()
511
        return r.getvalue()
512

    
513
    def _q_lookup(self, component):
514
        try:
515
            regie = Regie.get(component)
516
        except KeyError:
517
            raise errors.TraversalError()
518
        get_response().breadcrumb.append((str(regie.id), regie.label))
519
        return RegieDirectory(regie)
520

    
521

    
522
class PaymentsDirectory(AccessControlled, Directory):
523
    _q_exports = ['', 'regie']
524
    label = N_('Payments')
525

    
526
    regie = RegiesDirectory()
527

    
528
    def is_accessible(self, user):
529
        from .backoffice import check_visibility
530
        return check_visibility('payments', user)
531

    
532
    def _q_access(self):
533
        user = get_request().user
534
        if not user:
535
            raise errors.AccessUnauthorizedError()
536

    
537
        if not self.is_accessible(user):
538
            raise errors.AccessForbiddenError(
539
                    public_msg = _('You are not allowed to access Payments Management'),
540
                    location_hint = 'backoffice')
541

    
542
        get_response().breadcrumb.append(('payments/', _('Payments')))
543

    
544
    def _q_index(self):
545
        html_top('payments', _('Payments'))
546
        get_response().filter['sidebar'] = self.get_sidebar()
547
        r = TemplateIO(html=True)
548

    
549
        if not is_payment_supported:
550
            r += htmltext('<p class="infonotice">')
551
            r += _('Payment is not supported.')
552
            r += htmltext('</p>')
553

    
554
        regies = Regie.select()
555
        r += htmltext('<h2>%s</h2>') % _('Regies')
556
        if not regies:
557
            r += htmltext('<p>')
558
            r += _('There are no regies defined at the moment.')
559
            r += htmltext('</p>')
560
        r += htmltext('<ul class="biglist" id="regies-list">')
561
        for l in regies:
562
            regie_id = l.id
563
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % regie_id
564
            r += htmltext('<strong class="label"><a href="regie/%s/">%s</a></strong>') % (regie_id, l.label)
565
            r += htmltext('</li>')
566
        r += htmltext('</ul>')
567
        return r.getvalue()
568

    
569
    def get_sidebar(self):
570
        r = TemplateIO(html=True)
571
        r += htmltext('<ul id="sidebar-actions">')
572
        r += htmltext('  <li><a class="new-item" href="regie/new">%s</a></li>') % _('New Regie')
573
        r += htmltext('</ul>')
574
        return r.getvalue()
575

    
576

    
577
TextsDirectory.register('aq-invoice',
578
        N_('Message on top of an invoice'),
579
        category = N_('Invoices'))
580

    
581
EmailsDirectory.register('payment-new-invoice-email',
582
        N_('New invoice'),
583
        N_('Available variables: user, regie, invoice, invoice_url'),
584
        category = N_('Invoices'),
585
        default_subject = N_('New invoice'),
586
        default_body = N_('''
587
A new invoice is available at [invoice_url].
588
'''))
589

    
590
EmailsDirectory.register('payment-invoice-paid-email',
591
        N_('Paid invoice'),
592
        N_('Available variables: user, regie, invoice, invoice_url'),
593
        category = N_('Invoices'),
594
        default_subject = N_('Paid invoice'),
595
        default_body = N_('''
596
The invoice [invoice_url] has been paid.
597
'''))
598

    
599
EmailsDirectory.register('payment-invoice-canceled-email',
600
        N_('Canceled invoice'),
601
        N_('Available variables: user, regie, invoice, invoice_url'),
602
        category = N_('Invoices'),
603
        default_subject = N_('Canceled invoice'),
604
        default_body = N_('''
605
The invoice [invoice.id] has been canceled.
606
'''))
(22-22/27)