1 # -*- coding: utf-'8' "-*-"
5 from openerp.osv import osv, fields
6 from openerp.tools import float_round, float_repr
7 from openerp.tools.translate import _
9 _logger = logging.getLogger(__name__)
12 def _partner_format_address(address1=False, address2=False):
13 return ' '.join((address1 or '', address2 or '')).strip()
16 def _partner_split_name(partner_name):
17 return [' '.join(partner_name.split()[-1:]), ' '.join(partner_name.split()[:-1])]
20 class ValidationError(ValueError):
21 """ Used for value error when validating transaction data coming from acquirers. """
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.
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.
35 Methods that should be added in an acquirer-specific implementation:
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).
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.
52 _name = 'payment.acquirer'
53 _description = 'Payment Acquirer'
55 def _get_providers(self, cr, uid, context=None):
58 # indirection to ease inheritance
59 _provider_selection = lambda self, *args, **kwargs: self._get_providers(*args, **kwargs)
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),
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)'),
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',
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(c for c, f in self._all_columns.items() if getattr(f.column, 'required_if_provider', None) == acquirer.provider and not acquirer[c]):
108 (_check_required_if_provider, 'Required fields not filled', ['required for this provider']),
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)
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:
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 refernece, amount, currency_id (int or False),
125 currency (browse or False), partner (browse or False)
127 acquirer = self.browse(cr, uid, id, context=context)
130 tx = self.pool.get('payment.transaction').browse(cr, uid, tx_id, context=context)
132 'reference': tx.reference,
134 'currency_id': tx.currency_id.id,
135 'currency': tx.currency_id,
136 'partner': tx.partner_id,
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,
153 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
155 'name': partner.name,
156 'lang': partner.lang,
157 'email': partner.email,
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,
167 partner, partner_data = False, {}
168 partner_data.update(partner_values)
171 currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
173 currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
175 'reference': reference,
177 'currency_id': currency.id,
178 'currency': currency,
183 tx_data.update(tx_values)
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],
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)
202 return (partner_data, tx_data)
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:
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:
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
223 - 'cancel_url': URL if the client cancels the payment -> FIXME
224 - 'error_url': URL if there is an issue with the payment -> FIXME
226 - context: OpenERP context dictionary
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
242 if tx_values is None:
244 if partner_values is None:
246 acquirer = self.browse(cr, uid, id, context=context)
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)
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)
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,
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)
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>
290 </div>""" % (amount, payment_header)
291 return result % html_block.decode("utf-8")
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):
295 domain = [('website_published', '=', True), ('validation', '=', 'automatic')]
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,
305 html_forms.append(button)
308 html_block = '\n'.join(filter(None, html_forms))
309 return self._wrap_payment_block(cr, uid, html_block, amount, currency_id, context=context)
312 class PaymentTransaction(osv.Model):
313 """ Transaction Model. Each specific acquirer can extend the model by adding
316 Methods that can be added in an acquirer-specific implementation:
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.
322 Methods defined for convention, depending on your controllers:
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.
328 _name = 'payment.transaction'
329 _description = 'Payment Transaction'
330 _inherit = ['mail.thread']
332 _rec_name = 'reference'
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',
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'),
353 'amount': fields.float('Amount', required=True,
355 track_visibility='always',
356 help='Amount in cents'),
357 'fees': fields.float('Fees',
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'),
380 ('reference_uniq', 'UNIQUE(reference)', 'The payment transaction reference must be unique!'),
384 'date_create': fields.datetime.now,
387 'partner_lang': 'en_US',
390 def create(self, cr, uid, values, context=None):
391 Acquirer = self.pool['payment.acquirer']
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'])
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)
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)
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))
412 return super(PaymentTransaction, self).create(cr, uid, values, context=context)
414 def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
417 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
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,
429 # --------------------------------------------------
430 # FORM RELATED METHODS
431 # --------------------------------------------------
433 def form_feedback(self, cr, uid, data, acquirer_name, context=None):
434 invalid_parameters, tx = None, None
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)
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)
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)
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)
457 # --------------------------------------------------
458 # SERVER2SERVER RELATED METHODS
459 # --------------------------------------------------
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)
466 def s2s_send(self, cr, uid, values, cc_values, context=None):
467 """ Create and send server-to-server transaction.
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.
483 tx_id, result = None, None
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)
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)
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
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)
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)
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)
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)
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)