Project

General

Profile

Download (11.5 KB) Statistics
| Branch: | Tag: | Revision:

root / extra / modules / payments.py @ 5aa61d1d

1
import random
2
import string
3
import urllib
4
from datetime import datetime as dt
5
import hashlib
6

    
7
from decimal import Decimal
8

    
9
from quixote import redirect, get_request, get_session
10
from quixote.directory import Directory
11

    
12
if not set:
13
    from sets import Set as set
14

    
15
try:
16
    import eopayment
17
except ImportError:
18
    eopayment = None
19

    
20
from qommon.storage import StorableObject
21
from qommon.form import *
22
from qommon import errors, get_logger
23

    
24
from wcs.formdef import FormDef
25
from wcs.formdata import Evolution
26
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
27

    
28
def is_payment_supported():
29
    if not eopayment:
30
        return False
31
    return get_cfg('aq-permissions', {}).get('payments', None) is not None
32

    
33

    
34
class Regie(StorableObject):
35
    _names = 'regies'
36

    
37
    label = None
38
    description = None
39
    service = None
40
    service_options = None
41

    
42
    def get_payment_object(self):
43
        return eopayment.Payment(kind=self.service,
44
                                 options=self.service_options)
45

    
46

    
47
class Invoice(StorableObject):
48
    _names = 'invoices'
49
    _hashed_indexes = ['user_id', 'regie_id']
50
    _indexes = ['external_id']
51

    
52
    user_id = None
53
    regie_id = None
54
    formdef_id = None
55
    formdata_id = None
56
    subject = None
57
    details = None
58
    amount = None
59
    date = None
60
    paid = False
61
    paid_date = None
62
    external_id = None
63

    
64
    def __init__(self, id=None, regie_id=None, formdef_id=None):
65
        self.id = id
66
        self.regie_id = regie_id
67
        self.formdef_id = formdef_id
68
        if get_publisher() and not self.id:
69
            self.id = self.get_new_id()
70

    
71
    def get_new_id(self, create=False):
72
        # format : date-regie-formdef-alea-check
73
        r = random.SystemRandom()
74
        while True:
75
            id = '-'.join([
76
                dt.now().strftime('%Y%m%d'),
77
                'r%s' % (self.regie_id or 'x'),
78
                'f%s' % (self.formdef_id or 'x'),
79
                ''.join([r.choice(string.digits) for x in range(5)])
80
                ])
81
            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
82
            id = id + '-' + crc
83
            if not self.has_key(id):
84
                return id
85

    
86
    def check_crc(cls, id):
87
        try:
88
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
89
        except:
90
            return False
91
    check_crc = classmethod(check_crc)
92

    
93

    
94
class Transaction(StorableObject):
95
    _names = 'transactions'
96
    _hashed_indexes = ['invoice_ids']
97
    _indexes = ['eop_transaction_id']
98

    
99
    invoice_ids = None
100

    
101
    eop_transaction_id = None
102
    start = None
103
    end = None
104
    bank_data = None
105

    
106
    def __init__(self, *args, **kwargs):
107
        self.invoice_ids = list()
108
        StorableObject.__init__(self, *args, **kwargs)
109

    
110
    def get_new_id(cls, create=False):
111
        r = random.SystemRandom()
112
        while True:
113
            id = ''.join([r.choice(string.digits) for x in range(16)])
114
            if not cls.has_key(id):
115
                return id
116
    get_new_id = classmethod(get_new_id)
117

    
118
class PaymentWorkflowStatusItem(WorkflowStatusItem):
119
    description = N_('Payment Creation')
120
    key = 'payment'
121
    endpoint = False
122
    support_substitution_variables = True
123

    
124
    subject = None
125
    details = None
126
    amount = None
127
    waiting_status = None
128
    regie_id = None
129

    
130
    def render_as_line(self):
131
        if self.regie_id:
132
            return _('Payable to %s' % Regie.get(self.regie_id).label)
133
        else:
134
            return _('Payable (not completed)')
135

    
136
    def get_parameters(self):
137
        return ('subject', 'details', 'amount', 'waiting_status', 'regie_id')
138

    
139
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
140
        if 'subject' in parameters:
141
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
142
                     value=self.subject, size=40)
143
        if 'details' in parameters:
144
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
145
                     value=self.details, cols=80, rows=10)
146
        if 'amount' in parameters:
147
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
148
        if 'waiting_status' in parameters:
149
            form.add(SingleSelectWidget, '%swaiting_status' % prefix,
150
                title=_('Status while waiting validation'), value = self.waiting_status,
151
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
152
        if 'regie_id' in parameters:
153
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
154
                title=_('Regie'), value=self.regie_id,
155
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
156

    
157
    def calculate_amount(self, formdata):
158
        if not self.amount:
159
            return 0
160
        if not self.amount.startswith('='):
161
            return self.amount
162
        vars = get_publisher().substitutions.get_context_variables()
163
        vars['Decimal'] = Decimal
164
        amount = eval(self.amount[1:], vars)
165
        # XXX: catch and report the error somehow
166
        return amount
167

    
168
    def perform(self, formdata):
169
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
170
        invoice.user_id = get_request().user.id  # FIXME: handle user_hash
171
        invoice.formdata_id = formdata.id
172
        if self.subject:
173
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
174
        else:
175
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
176
                    'form_name': formdata.formdef.name,
177
                    'formdata_id': formdata.id }
178
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
179
        invoice.amount = self.calculate_amount(formdata)
180
        invoice.date = dt.now()
181
        invoice.store()
182

    
183
        wf_status = [x for x in self.parent.parent.possible_status \
184
                     if x.id == self.waiting_status][0]
185
        formdata.status = 'wf-%s' % wf_status.id
186
        # FIXME: add a link to /invoices/<id> in evolution
187
        formdata.store()
188

    
189
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
190

    
191
register_item_class(PaymentWorkflowStatusItem)
192

    
193
def request_payment(invoice_ids, url, add_regie=True):
194
    for invoice_id in invoice_ids:
195
        if not Invoice.check_crc(invoice_id):
196
            raise KeyError()
197
    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
198
    invoices = filter(lambda x: not x.paid, invoices)
199
    regie_ids = set([invoice.regie_id for invoice in invoices])
200
    # Do not apply if more than one regie is used or no invoice is not paid
201
    if len(invoices) == 0 or len(regie_ids) != 1:
202
        url = get_publisher().get_frontoffice_url()
203
        if get_session().user:
204
            # FIXME: add error messages
205
            url += '/myspace/invoices/'
206
        return redirect(url)
207
    if add_regie:
208
        url = '%s%s' % (url, list(regie_ids)[0])
209

    
210
    transaction = Transaction()
211
    transaction.store()
212
    transaction.invoice_ids = invoice_ids
213
    transaction.start = dt.now()
214

    
215
    amount = Decimal(0)
216
    for invoice in invoices:
217
        amount += Decimal(invoice.amount)
218

    
219
    regie = Regie.get(invoice.regie_id)
220
    payment = regie.get_payment_object()
221
    (eop_transaction_id, kind, data) = payment.request(amount, next_url=url)
222
    transaction.eop_transaction_id = eop_transaction_id
223
    transaction.store()
224

    
225
    if kind == eopayment.URL:
226
        return redirect(data)
227
    elif kind == eopayment.FORM:
228
        raise NotImplementedError()
229
    else:
230
        raise NotImplementedError()
231

    
232

    
233
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
234
    description = N_('Payment Validation')
235
    key = 'payment-validation'
236
    endpoint = False
237

    
238
    next_status = None
239

    
240
    def render_as_line(self):
241
        return _('Wait for payment validation')
242

    
243
    def get_parameters(self):
244
        return ('next_status',)
245

    
246
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
247
        if 'next_status' in parameters:
248
            form.add(SingleSelectWidget, '%snext_status' % prefix,
249
                title=_('Status once validated'), value = self.next_status,
250
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
251

    
252
    def validated(self, formdata, invoice_id):
253
        invoice = Invoice.get(invoice_id)
254
        if invoice.formdata_id != formdata.id:
255
            raise 'XXX'
256
        if invoice.formdef_id != formdata.formdef.id:
257
            raise 'XXX'
258
        invoice.paid = True
259
        invoice.paid_date = dt.now()
260
        invoice.store()
261

    
262
        evo = Evolution()
263
        evo.time = time.localtime()
264
        wf_status = [x for x in self.parent.parent.possible_status \
265
                     if x.id == self.next_status][0]
266
        evo.status = 'wf-%s' % wf_status.id
267
        formdata.status = evo.status
268
        if not formdata.evolution:
269
            formdata.evolution = []
270
        formdata.evolution.append(evo)
271
        formdata.store()
272
        # performs the items of the new status
273
        formdata.perform_workflow()
274

    
275
register_item_class(PaymentValidationWorkflowStatusItem)
276

    
277

    
278
class PublicPaymentRegieBackDirectory(Directory):
279
    def _q_lookup(self, component):
280
        logger = get_logger()
281
        try:
282
            regie = Regie.get(component)
283
        except KeyError:
284
            raise errors.TraversalError()
285
        query_string = get_request().get_query()
286
        payment = regie.get_payment_object()
287
        (result, eop_transaction_id, bank_data, return_content) = payment.response(query_string)
288
        if not return_content:
289
            if result:
290
                # TODO: Here return success message
291
                get_session().message = ('error', _('Payment succeeded'))
292
            else:
293
                # TODO: here return failure message
294
                get_session().message = ('info', _('Payment failed'))
295
            url = get_publisher().get_frontoffice_url()
296
            if get_session().user:
297
                url += '/myspace/invoices/'
298
            return redirect(url)
299
        else:
300
            get_response().set_content_type('text/plain')
301
            if result is False:
302
                logger.error('transaction %s finished with failure, bank_data:%s' % (
303
                                        eop_transaction_id, bank_data))
304
                return return_content
305
            logger.info('transaction %s successful, bank_data:%s' % (
306
                                    eop_transaction_id, bank_data))
307
            transaction = Transaction.get_on_index(eop_transaction_id,
308
                    'eop_transaction_id')
309
            transaction.end = dt.now()
310
            transaction.bank_data = bank_data
311
            transaction.store()
312

    
313
            for invoice_id in transaction.invoice_ids:
314
                invoice = Invoice.get(invoice_id)
315
                if invoice.formdef_id and invoice.formdata_id:
316
                    formdef = FormDef.get(invoice.formdef_id)
317
                    formdata = formdef.data_class().get(invoice.formdata_id)
318
                    wf_status = formdata.get_workflow_status()
319
                    for item in wf_status.items:
320
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
321
                            item.validated(formdata, invoice_id)
322
            return return_content
323

    
324
class PublicPaymentDirectory(Directory):
325
    _q_exports = ['', 'init', 'back']
326

    
327
    back = PublicPaymentRegieBackDirectory()
328

    
329
    def init(self):
330
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
331

    
332
        for invoice_id in invoice_ids:
333
            if not Invoice.check_crc(invoice_id):
334
                raise KeyError()
335

    
336
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
337

    
338
        return request_payment(invoice_ids, url)
(24-24/30)