Projet

Général

Profil

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

root / extra / modules / payments_ui.py @ c182b1ab

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 errors, misc, template, get_logger
16
from qommon.form import *
17
from qommon.strftime import strftime
18
from qommon.admin.emails import EmailsDirectory
19
from qommon.backoffice.menu import html_top
20
from qommon import get_cfg
21

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

    
25
from qommon.admin.texts import TextsDirectory
26

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

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

    
71

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

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

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

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

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

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

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

    
126
        return r.getvalue()
127

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

    
131

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

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

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

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

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

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

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

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

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

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

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

    
191

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

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

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

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

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

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

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

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

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

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

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

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

    
345
    PAGINATION = 50
346

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

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

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

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

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

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

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

    
483

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

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

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

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

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

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

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

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

    
520

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

    
525
    regie = RegiesDirectory()
526

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

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

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

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

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

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

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

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

    
575

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

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

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

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