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