[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / addons / payment / models / payment_acquirer.py
1 # -*- coding: utf-'8' "-*-"
2
3 import logging
4
5 from openerp.osv import osv, fields
6 from openerp.tools import float_round, float_repr
7 from openerp.tools.translate import _
8
9 _logger = logging.getLogger(__name__)
10
11
12 def _partner_format_address(address1=False, address2=False):
13     return ' '.join((address1 or '', address2 or '')).strip()
14
15
16 def _partner_split_name(partner_name):
17     return [' '.join(partner_name.split()[-1:]), ' '.join(partner_name.split()[:-1])]
18
19
20 class ValidationError(ValueError):
21     """ Used for value error when validating transaction data coming from acquirers. """
22     pass
23
24
25 class PaymentAcquirer(osv.Model):
26     """ Acquirer Model. Each specific acquirer can extend the model by adding
27     its own fields, using the acquirer_name as a prefix for the new fields.
28     Using the required_if_provider='<name>' attribute on fields it is possible
29     to have required fields that depend on a specific acquirer.
30
31     Each acquirer has a link to an ir.ui.view record that is a template of
32     a button used to display the payment form. See examples in ``payment_ogone``
33     and ``payment_paypal`` modules.
34
35     Methods that should be added in an acquirer-specific implementation:
36
37      - ``<name>_form_generate_values(self, cr, uid, id, reference, amount, currency,
38        partner_id=False, partner_values=None, tx_custom_values=None, context=None)``:
39        method that generates the values used to render the form button template.
40      - ``<name>_get_form_action_url(self, cr, uid, id, context=None):``: method
41        that returns the url of the button form. It is used for example in
42        ecommerce application, if you want to post some data to the acquirer.
43      - ``<name>_compute_fees(self, cr, uid, id, amount, currency_id, country_id,
44        context=None)``: computed the fees of the acquirer, using generic fields
45        defined on the acquirer model (see fields definition).
46
47     Each acquirer should also define controllers to handle communication between
48     OpenERP and the acquirer. It generally consists in return urls given to the
49     button form and that the acquirer uses to send the customer back after the
50     transaction, with transaction details given as a POST request.
51     """
52     _name = 'payment.acquirer'
53     _description = 'Payment Acquirer'
54
55     def _get_providers(self, cr, uid, context=None):
56         return []
57
58     # indirection to ease inheritance
59     _provider_selection = lambda self, *args, **kwargs: self._get_providers(*args, **kwargs)
60
61     _columns = {
62         'name': fields.char('Name', required=True),
63         'provider': fields.selection(_provider_selection, string='Provider', required=True),
64         'company_id': fields.many2one('res.company', 'Company', required=True),
65         'pre_msg': fields.html('Message', help='Message displayed to explain and help the payment process.'),
66         'post_msg': fields.html('Thanks Message', help='Message displayed after having done the payment process.'),
67         'validation': fields.selection(
68             [('manual', 'Manual'), ('automatic', 'Automatic')],
69             string='Process Method',
70             help='Static payments are payments like transfer, that require manual steps.'),
71         'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True),
72         'environment': fields.selection(
73             [('test', 'Test'), ('prod', 'Production')],
74             string='Environment', oldname='env'),
75         'website_published': fields.boolean(
76             'Visible in Portal / Website', copy=False,
77             help="Make this payment acquirer available (Customer invoices, etc.)"),
78         'auto_confirm': fields.selection(
79             [('none', 'No automatic confirmation'),
80              ('at_pay_confirm', 'At payment confirmation'),
81              ('at_pay_now', 'At payment')],
82             string='Order Confirmation', required=True),
83         # Fees
84         'fees_active': fields.boolean('Compute fees'),
85         'fees_dom_fixed': fields.float('Fixed domestic fees'),
86         'fees_dom_var': fields.float('Variable domestic fees (in percents)'),
87         'fees_int_fixed': fields.float('Fixed international fees'),
88         'fees_int_var': fields.float('Variable international fees (in percents)'),
89     }
90
91     _defaults = {
92         'company_id': lambda self, cr, uid, obj, ctx=None: self.pool['res.users'].browse(cr, uid, uid).company_id.id,
93         'environment': 'test',
94         'validation': 'automatic',
95         'website_published': True,
96         'auto_confirm': 'at_pay_confirm',
97     }
98
99     def _check_required_if_provider(self, cr, uid, ids, context=None):
100         """ If the field has 'required_if_provider="<provider>"' attribute, then it
101         required if record.provider is <provider>. """
102         for acquirer in self.browse(cr, uid, ids, context=context):
103             if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()):
104                 return False
105         return True
106
107     _constraints = [
108         (_check_required_if_provider, 'Required fields not filled', ['required for this provider']),
109     ]
110
111     def get_form_action_url(self, cr, uid, id, context=None):
112         """ Returns the form action URL, for form-based acquirer implementations. """
113         acquirer = self.browse(cr, uid, id, context=context)
114         if hasattr(self, '%s_get_form_action_url' % acquirer.provider):
115             return getattr(self, '%s_get_form_action_url' % acquirer.provider)(cr, uid, id, context=context)
116         return False
117
118     def form_preprocess_values(self, cr, uid, id, reference, amount, currency_id, tx_id, partner_id, partner_values, tx_values, context=None):
119         """  Pre process values before giving them to the acquirer-specific render
120         methods. Those methods will receive:
121
122              - partner_values: will contain name, lang, email, zip, address, city,
123                country_id (int or False), country (browse or False), phone, reference
124              - tx_values: will contain reference, amount, currency_id (int or False),
125                currency (browse or False), partner (browse or False)
126         """
127         acquirer = self.browse(cr, uid, id, context=context)
128
129         if tx_id:
130             tx = self.pool.get('payment.transaction').browse(cr, uid, tx_id, context=context)
131             tx_data = {
132                 'reference': tx.reference,
133                 'amount': tx.amount,
134                 'currency_id': tx.currency_id.id,
135                 'currency': tx.currency_id,
136                 'partner': tx.partner_id,
137             }
138             partner_data = {
139                 'name': tx.partner_name,
140                 'lang': tx.partner_lang,
141                 'email': tx.partner_email,
142                 'zip': tx.partner_zip,
143                 'address': tx.partner_address,
144                 'city': tx.partner_city,
145                 'country_id': tx.partner_country_id.id,
146                 'country': tx.partner_country_id,
147                 'phone': tx.partner_phone,
148                 'reference': tx.partner_reference,
149                 'state': None,
150             }
151         else:
152             if partner_id:
153                 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
154                 partner_data = {
155                     'name': partner.name,
156                     'lang': partner.lang,
157                     'email': partner.email,
158                     'zip': partner.zip,
159                     'city': partner.city,
160                     'address': _partner_format_address(partner.street, partner.street2),
161                     'country_id': partner.country_id.id,
162                     'country': partner.country_id,
163                     'phone': partner.phone,
164                     'state': partner.state_id,
165                 }
166             else:
167                 partner, partner_data = False, {}
168             partner_data.update(partner_values)
169
170             if currency_id:
171                 currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
172             else:
173                 currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
174             tx_data = {
175                 'reference': reference,
176                 'amount': amount,
177                 'currency_id': currency.id,
178                 'currency': currency,
179                 'partner': partner,
180             }
181
182         # update tx values
183         tx_data.update(tx_values)
184
185         # update partner values
186         if not partner_data.get('address'):
187             partner_data['address'] = _partner_format_address(partner_data.get('street', ''), partner_data.get('street2', ''))
188         if not partner_data.get('country') and partner_data.get('country_id'):
189             partner_data['country'] = self.pool['res.country'].browse(cr, uid, partner_data.get('country_id'), context=context)
190         partner_data.update({
191             'first_name': _partner_split_name(partner_data['name'])[0],
192             'last_name': _partner_split_name(partner_data['name'])[1],
193         })
194
195         # compute fees
196         fees_method_name = '%s_compute_fees' % acquirer.provider
197         if hasattr(self, fees_method_name):
198             fees = getattr(self, fees_method_name)(
199                 cr, uid, id, tx_data['amount'], tx_data['currency_id'], partner_data['country_id'], context=None)
200             tx_data['fees'] = float_round(fees, 2)
201
202         return (partner_data, tx_data)
203
204     def render(self, cr, uid, id, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, context=None):
205         """ Renders the form template of the given acquirer as a qWeb template.
206         All templates will receive:
207
208          - acquirer: the payment.acquirer browse record
209          - user: the current user browse record
210          - currency_id: id of the transaction currency
211          - amount: amount of the transaction
212          - reference: reference of the transaction
213          - partner: the current partner browse record, if any (not necessarily set)
214          - partner_values: a dictionary of partner-related values
215          - tx_values: a dictionary of transaction related values that depends on
216                       the acquirer. Some specific keys should be managed in each
217                       provider, depending on the features it offers:
218
219           - 'feedback_url': feedback URL, controler that manage answer of the acquirer
220                             (without base url) -> FIXME
221           - 'return_url': URL for coming back after payment validation (wihout
222                           base url) -> FIXME
223           - 'cancel_url': URL if the client cancels the payment -> FIXME
224           - 'error_url': URL if there is an issue with the payment -> FIXME
225
226          - context: OpenERP context dictionary
227
228         :param string reference: the transaction reference
229         :param float amount: the amount the buyer has to pay
230         :param res.currency browse record currency: currency
231         :param int tx_id: id of a transaction; if set, bypasses all other given
232                           values and only render the already-stored transaction
233         :param res.partner browse record partner_id: the buyer
234         :param dict partner_values: a dictionary of values for the buyer (see above)
235         :param dict tx_custom_values: a dictionary of values for the transction
236                                       that is given to the acquirer-specific method
237                                       generating the form values
238         :param dict context: OpenERP context
239         """
240         if context is None:
241             context = {}
242         if tx_values is None:
243             tx_values = {}
244         if partner_values is None:
245             partner_values = {}
246         acquirer = self.browse(cr, uid, id, context=context)
247
248         # pre-process values
249         amount = float_round(amount, 2)
250         partner_values, tx_values = self.form_preprocess_values(
251             cr, uid, id, reference, amount, currency_id, tx_id, partner_id,
252             partner_values, tx_values, context=context)
253
254         # call <name>_form_generate_values to update the tx dict with acqurier specific values
255         cust_method_name = '%s_form_generate_values' % (acquirer.provider)
256         if hasattr(self, cust_method_name):
257             method = getattr(self, cust_method_name)
258             partner_values, tx_values = method(cr, uid, id, partner_values, tx_values, context=context)
259
260         qweb_context = {
261             'tx_url': context.get('tx_url', self.get_form_action_url(cr, uid, id, context=context)),
262             'submit_class': context.get('submit_class', 'btn btn-link'),
263             'submit_txt': context.get('submit_txt'),
264             'acquirer': acquirer,
265             'user': self.pool.get("res.users").browse(cr, uid, uid, context=context),
266             'reference': tx_values['reference'],
267             'amount': tx_values['amount'],
268             'currency': tx_values['currency'],
269             'partner': tx_values.get('partner'),
270             'partner_values': partner_values,
271             'tx_values': tx_values,
272             'context': context,
273         }
274
275         # because render accepts view ids but not qweb -> need to use the xml_id
276         return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context)
277
278     def _wrap_payment_block(self, cr, uid, html_block, amount, currency_id, context=None):
279         payment_header = _('Pay safely online')
280         amount_str = float_repr(amount, self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))
281         currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
282         currency_str = currency.symbol or currency.name
283         amount = u"%s %s" % ((currency_str, amount_str) if currency.position == 'before' else (amount_str, currency_str))
284         result = u"""<div class="payment_acquirers">
285                          <div class="payment_header">
286                              <div class="payment_amount">%s</div>
287                              %s
288                          </div>
289                          %%s
290                      </div>""" % (amount, payment_header)
291         return result % html_block.decode("utf-8")
292
293     def render_payment_block(self, cr, uid, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, company_id=None, context=None):
294         html_forms = []
295         domain = [('website_published', '=', True), ('validation', '=', 'automatic')]
296         if company_id:
297             domain.append(('company_id', '=', company_id))
298         acquirer_ids = self.search(cr, uid, domain, context=context)
299         for acquirer_id in acquirer_ids:
300             button = self.render(
301                 cr, uid, acquirer_id,
302                 reference, amount, currency_id,
303                 tx_id, partner_id, partner_values, tx_values,
304                 context)
305             html_forms.append(button)
306         if not html_forms:
307             return ''
308         html_block = '\n'.join(filter(None, html_forms))
309         return self._wrap_payment_block(cr, uid, html_block, amount, currency_id, context=context)
310
311
312 class PaymentTransaction(osv.Model):
313     """ Transaction Model. Each specific acquirer can extend the model by adding
314     its own fields.
315
316     Methods that can be added in an acquirer-specific implementation:
317
318      - ``<name>_create``: method receiving values used when creating a new
319        transaction and that returns a dictionary that will update those values.
320        This method can be used to tweak some transaction values.
321
322     Methods defined for convention, depending on your controllers:
323
324      - ``<name>_form_feedback(self, cr, uid, data, context=None)``: method that
325        handles the data coming from the acquirer after the transaction. It will
326        generally receives data posted by the acquirer after the transaction.
327     """
328     _name = 'payment.transaction'
329     _description = 'Payment Transaction'
330     _inherit = ['mail.thread']
331     _order = 'id desc'
332     _rec_name = 'reference'
333
334     _columns = {
335         'date_create': fields.datetime('Creation Date', readonly=True, required=True),
336         'date_validate': fields.datetime('Validation Date'),
337         'acquirer_id': fields.many2one(
338             'payment.acquirer', 'Acquirer',
339             required=True,
340         ),
341         'type': fields.selection(
342             [('server2server', 'Server To Server'), ('form', 'Form')],
343             string='Type', required=True),
344         'state': fields.selection(
345             [('draft', 'Draft'), ('pending', 'Pending'),
346              ('done', 'Done'), ('error', 'Error'),
347              ('cancel', 'Canceled')
348              ], 'Status', required=True,
349             track_visiblity='onchange', copy=False),
350         'state_message': fields.text('Message',
351                                      help='Field used to store error and/or validation messages for information'),
352         # payment
353         'amount': fields.float('Amount', required=True,
354                                digits=(16, 2),
355                                track_visibility='always',
356                                help='Amount in cents'),
357         'fees': fields.float('Fees',
358                              digits=(16, 2),
359                              track_visibility='always',
360                              help='Fees amount; set by the system because depends on the acquirer'),
361         'currency_id': fields.many2one('res.currency', 'Currency', required=True),
362         'reference': fields.char('Order Reference', required=True),
363         'acquirer_reference': fields.char('Acquirer Order Reference',
364                                           help='Reference of the TX as stored in the acquirer database'),
365         # duplicate partner / transaction data to store the values at transaction time
366         'partner_id': fields.many2one('res.partner', 'Partner', track_visibility='onchange',),
367         'partner_name': fields.char('Partner Name'),
368         'partner_lang': fields.char('Lang'),
369         'partner_email': fields.char('Email'),
370         'partner_zip': fields.char('Zip'),
371         'partner_address': fields.char('Address'),
372         'partner_city': fields.char('City'),
373         'partner_country_id': fields.many2one('res.country', 'Country', required=True),
374         'partner_phone': fields.char('Phone'),
375         'partner_reference': fields.char('Partner Reference',
376                                          help='Reference of the customer in the acquirer database'),
377     }
378
379     _sql_constraints = [
380         ('reference_uniq', 'UNIQUE(reference)', 'The payment transaction reference must be unique!'),
381     ]
382
383     _defaults = {
384         'date_create': fields.datetime.now,
385         'type': 'form',
386         'state': 'draft',
387         'partner_lang': 'en_US',
388     }
389
390     def create(self, cr, uid, values, context=None):
391         Acquirer = self.pool['payment.acquirer']
392
393         if values.get('partner_id'):  # @TDENOTE: not sure
394             values.update(self.on_change_partner_id(cr, uid, None, values.get('partner_id'), context=context)['values'])
395
396         # call custom create method if defined (i.e. ogone_create for ogone)
397         if values.get('acquirer_id'):
398             acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
399
400             # compute fees
401             custom_method_name = '%s_compute_fees' % acquirer.provider
402             if hasattr(Acquirer, custom_method_name):
403                 fees = getattr(Acquirer, custom_method_name)(
404                     cr, uid, acquirer.id, values.get('amount', 0.0), values.get('currency_id'), values.get('country_id'), context=None)
405                 values['fees'] = float_round(fees, 2)
406
407             # custom create
408             custom_method_name = '%s_create' % acquirer.provider
409             if hasattr(self, custom_method_name):
410                 values.update(getattr(self, custom_method_name)(cr, uid, values, context=context))
411
412         return super(PaymentTransaction, self).create(cr, uid, values, context=context)
413
414     def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
415         partner = None
416         if partner_id:
417             partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
418         return {'values': {
419             'partner_name': partner and partner.name or False,
420             'partner_lang': partner and partner.lang or 'en_US',
421             'partner_email': partner and partner.email or False,
422             'partner_zip': partner and partner.zip or False,
423             'partner_address': _partner_format_address(partner and partner.street or '', partner and partner.street2 or ''),
424             'partner_city': partner and partner.city or False,
425             'partner_country_id': partner and partner.country_id.id or False,
426             'partner_phone': partner and partner.phone or False,
427         }}
428
429     # --------------------------------------------------
430     # FORM RELATED METHODS
431     # --------------------------------------------------
432
433     def form_feedback(self, cr, uid, data, acquirer_name, context=None):
434         invalid_parameters, tx = None, None
435
436         tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
437         if hasattr(self, tx_find_method_name):
438             tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context)
439
440         invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name
441         if hasattr(self, invalid_param_method_name):
442             invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
443
444         if invalid_parameters:
445             _error_message = '%s: incorrect tx data:\n' % (acquirer_name)
446             for item in invalid_parameters:
447                 _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
448             _logger.error(_error_message)
449             return False
450
451         feedback_method_name = '_%s_form_validate' % acquirer_name
452         if hasattr(self, feedback_method_name):
453             return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
454
455         return True
456
457     # --------------------------------------------------
458     # SERVER2SERVER RELATED METHODS
459     # --------------------------------------------------
460
461     def s2s_create(self, cr, uid, values, cc_values, context=None):
462         tx_id, tx_result = self.s2s_send(cr, uid, values, cc_values, context=context)
463         self.s2s_feedback(cr, uid, tx_id, tx_result, context=context)
464         return tx_id
465
466     def s2s_send(self, cr, uid, values, cc_values, context=None):
467         """ Create and send server-to-server transaction.
468
469         :param dict values: transaction values
470         :param dict cc_values: credit card values that are not stored into the
471                                payment.transaction object. Acquirers should
472                                handle receiving void or incorrect cc values.
473                                Should contain :
474
475                                 - holder_name
476                                 - number
477                                 - cvc
478                                 - expiry_date
479                                 - brand
480                                 - expiry_date_yy
481                                 - expiry_date_mm
482         """
483         tx_id, result = None, None
484
485         if values.get('acquirer_id'):
486             acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
487             custom_method_name = '_%s_s2s_send' % acquirer.provider
488             if hasattr(self, custom_method_name):
489                 tx_id, result = getattr(self, custom_method_name)(cr, uid, values, cc_values, context=context)
490
491         if tx_id is None and result is None:
492             tx_id = super(PaymentTransaction, self).create(cr, uid, values, context=context)
493         return (tx_id, result)
494
495     def s2s_feedback(self, cr, uid, tx_id, data, context=None):
496         """ Handle the feedback of a server-to-server transaction. """
497         tx = self.browse(cr, uid, tx_id, context=context)
498         invalid_parameters = None
499
500         invalid_param_method_name = '_%s_s2s_get_invalid_parameters' % tx.acquirer_id.provider
501         if hasattr(self, invalid_param_method_name):
502             invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
503
504         if invalid_parameters:
505             _error_message = '%s: incorrect tx data:\n' % (tx.acquirer_id.name)
506             for item in invalid_parameters:
507                 _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
508             _logger.error(_error_message)
509             return False
510
511         feedback_method_name = '_%s_s2s_validate' % tx.acquirer_id.provider
512         if hasattr(self, feedback_method_name):
513             return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
514
515         return True
516
517     def s2s_get_tx_status(self, cr, uid, tx_id, context=None):
518         """ Get the tx status. """
519         tx = self.browse(cr, uid, tx_id, context=context)
520
521         invalid_param_method_name = '_%s_s2s_get_tx_status' % tx.acquirer_id.provider
522         if hasattr(self, invalid_param_method_name):
523             return getattr(self, invalid_param_method_name)(cr, uid, tx, context=context)
524
525         return True