1 # -*- coding: utf-'8' "-*-"
5 from openerp.osv import osv, fields
7 _logger = logging.getLogger(__name__)
10 def _partner_format_address(address1=False, address2=False):
11 return ' '.join((address1 or '', address2 or ''))
14 def _partner_split_name(partner_name):
15 return [' '.join(partner_name.split()[-1:]), ' '.join(partner_name.split()[:-1])]
18 class ValidationError(ValueError):
19 """ Used for value error when validating transaction data coming from acquirers. """
23 class PaymentAcquirer(osv.Model):
24 """ Acquirer Model. Each specific acquirer can extend the model by adding
25 its own fields, using the acquirer_name as a prefix for the new fields.
26 Using the required_if_provider='<name>' attribute on fields it is possible
27 to have required fields that depend on a specific acquirer.
29 Each acquirer has a link to an ir.ui.view record that is a template of
30 a button used to display the payment form. See examples in ``payment_acquirer_ogone``
31 and ``payment_acquirer_paypal`` modules.
33 Methods that should be added in an acquirer-specific implementation:
35 - ``<name>_form_generate_values(self, cr, uid, id, reference, amount, currency,
36 partner_id=False, partner_values=None, tx_custom_values=None, context=None)``:
37 method that generates the values used to render the form button template.
38 - ``<name>_get_form_action_url(self, cr, uid, id, context=None):``: method
39 that returns the url of the button form. It is used for example in
40 ecommerce application, if you want to post some data to the acquirer.
41 - ``<name>_compute_fees(self, cr, uid, id, amount, currency_id, country_id,
42 context=None)``: computed the fees of the acquirer, using generic fields
43 defined on the acquirer model (see fields definition).
45 Each acquirer should also define controllers to handle communication between
46 OpenERP and the acquirer. It generally consists in return urls given to the
47 button form and that the acquirer uses to send the customer back after the
48 transaction, with transaction details given as a POST request.
50 _name = 'payment.acquirer'
51 _description = 'Payment Acquirer'
54 'name': fields.char('Name', required=True),
55 'company_id': fields.many2one('res.company', 'Company', required=True),
56 'message': fields.html('Message', help='Message displayed to help payment and validation'),
57 'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True),
58 'env': fields.selection(
59 [('test', 'Test'), ('prod', 'Production')],
60 string='Environment'),
61 'portal_published': fields.boolean(
63 help="Make this payment acquirer available (Customer invoices, etc.)"),
65 'fees_active': fields.boolean('Compute fees'),
66 'fees_dom_fixed': fields.float('Fixed domestic fees'),
67 'fees_dom_var': fields.float('Variable domestic fees (in percents)'),
68 'fees_int_fixed': fields.float('Fixed international fees'),
69 'fees_int_var': fields.float('Variable international fees (in percents)'),
73 'company_id': lambda self, cr, uid, obj, ctx=None: self.pool['res.users'].browse(cr, uid, uid).company_id.id,
75 'portal_published': True,
78 def _check_required_if_provider(self, cr, uid, ids, context=None):
79 """ If the field has 'required_if_provider="<name>"' attribute, then it
80 required if record.name is <name>. """
81 for acquirer in self.browse(cr, uid, ids, context=context):
82 if any(c for c, f in self._all_columns.items() if getattr(f.column, 'required_if_provider', None) == acquirer.name and not acquirer[c]):
87 (_check_required_if_provider, 'Required fields not filled', ['required for this provider']),
90 def get_form_action_url(self, cr, uid, id, context=None):
91 """ Returns the form action URL, for form-based acquirer implementations. """
92 acquirer = self.browse(cr, uid, id, context=context)
93 if hasattr(self, '%s_get_form_action_url' % acquirer.name):
94 return getattr(self, '%s_get_form_action_url' % acquirer.name)(cr, uid, id, context=context)
97 def form_preprocess_values(self, cr, uid, id, reference, amount, currency_id, tx_id, partner_id, partner_values, tx_values, context=None):
98 """ Pre process values before giving them to the acquirer-specific render
99 methods. Those methods will receive:
101 - partner_values: will contain name, lang, email, zip, address, city,
102 country_id (int or False), country (browse or False), phone, reference
103 - tx_values: will contain refernece, amount, currency_id (int or False),
104 currency (browse or False), partner (browse or False)
106 acquirer = self.browse(cr, uid, id, context=context)
109 tx = self.browse(cr, uid, id, context=context)
111 'reference': tx.reference,
113 'currency_id': tx.currency_id.id,
114 'currency': tx.currency_id,
115 'partner': tx.partner_id,
118 'name': tx.partner_name,
119 'lang': tx.partner_lang,
120 'email': tx.partner_email,
121 'zip': tx.partner_zip,
122 'address': tx.partner_address,
123 'city': tx.partner_city,
124 'country_id': tx.partner_country_id.id,
125 'country': tx.partner_country_id,
126 'phone': tx.partner_phone,
127 'reference': tx.partner_reference,
131 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
133 'name': partner.name,
134 'lang': partner.lang,
135 'email': partner.email,
137 'city': partner.city,
138 'address': _partner_format_address(partner.street, partner.street2),
139 'country_id': partner.country_id.id,
140 'country': partner.country_id,
141 'phone': partner.phone,
144 partner, partner_data = False, {}
145 partner_data.update(partner_values)
148 currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
150 currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
152 'reference': reference,
154 'currency_id': currency.id,
155 'currency': currency,
160 tx_data.update(tx_values)
162 # update partner values
163 if not partner_data.get('address'):
164 partner_data['address'] = _partner_format_address(partner_data.get('street', ''), partner_data.get('street2', ''))
165 if not partner_data.get('country') and partner_data.get('country_id'):
166 partner_data['country'] = self.pool['res.country'].browse(cr, uid, partner_data.get('country_id'), context=context)
167 partner_data.update({
168 'first_name': _partner_split_name(partner_data['name'])[0],
169 'last_name': _partner_split_name(partner_data['name'])[1],
173 fees_method_name = '%s_compute_fees' % acquirer.name
174 if hasattr(self, fees_method_name):
175 tx_data['fees'] = getattr(self, fees_method_name)(
176 cr, uid, id, tx_data['amount'], tx_data['currency_id'],
177 partner_data['country_id'], context=None)
179 return (partner_data, tx_data)
181 def render(self, cr, uid, id, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, context=None):
182 """ Renders the form template of the given acquirer as a qWeb template.
183 All templates will receive:
185 - acquirer: the payment.acquirer browse record
186 - user: the current user browse record
187 - currency: currency browse record
188 - amount: amount of the transaction
189 - reference: reference of the transaction
190 - partner: the current partner browse record, if any (not necessarily set)
191 - partner_values: a dictionary of partner-related values
192 - tx_values: a dictionary of transaction related values that depends on
193 the acquirer. Some specific keys should be managed in each
194 provider, depending on the features it offers:
196 - 'feedback_url': feedback URL, controler that manage answer of the acquirer
197 (without base url) -> FIXME
198 - 'return_url': URL for coming back after payment validation (wihout
200 - 'cancel_url': URL if the client cancels the payment -> FIXME
201 - 'error_url': URL if there is an issue with the payment -> FIXME
203 - context: OpenERP context dictionary
205 :param string reference: the transaction reference
206 :param float amount: the amount the buyer has to pay
207 :param res.currency browse record currency: currency
208 :param int tx_id: id of a transaction; if set, bypasses all other given
209 values and only render the already-stored transaction
210 :param res.partner browse record partner_id: the buyer
211 :param dict partner_values: a dictionary of values for the buyer (see above)
212 :param dict tx_custom_values: a dictionary of values for the transction
213 that is given to the acquirer-specific method
214 generating the form values
215 :param dict context: OpenERP context
219 if tx_values is None:
221 if partner_values is None:
223 acquirer = self.browse(cr, uid, id, context=context)
226 partner_values, tx_values = self.form_preprocess_values(
227 cr, uid, id, reference, amount, currency_id, tx_id, partner_id,
228 partner_values, tx_values, context=context)
230 # call <name>_form_generate_values to update the tx dict with acqurier specific values
231 cust_method_name = '%s_form_generate_values' % (acquirer.name)
232 if hasattr(self, cust_method_name):
233 method = getattr(self, cust_method_name)
234 partner_values, tx_values = method(cr, uid, id, partner_values, tx_values, context=context)
237 'tx_url': context.get('tx_url', self.get_form_action_url(cr, uid, id, context=context)),
238 'submit_class': context.get('submit_class', 'btn btn-link'),
239 'submit_txt': context.get('submit_txt'),
240 'acquirer': acquirer,
241 'user': self.pool.get("res.users").browse(cr, uid, uid, context=context),
242 'reference': tx_values['reference'],
243 'amount': tx_values['amount'],
244 'currency': tx_values['currency'],
245 'partner': tx_values.get('partner'),
246 'partner_values': partner_values,
247 'tx_values': tx_values,
251 # because render accepts view ids but not qweb -> need to use the xml_id
252 return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context)
255 class PaymentTransaction(osv.Model):
256 """ Transaction Model. Each specific acquirer can extend the model by adding
259 Methods that can be added in an acquirer-specific implementation:
261 - ``<name>_create``: method receiving values used when creating a new
262 transaction and that returns a dictionary that will update those values.
263 This method can be used to tweak some transaction values.
265 Methods defined for convention, depending on your controllers:
267 - ``<name>_form_feedback(self, cr, uid, data, context=None)``: method that
268 handles the data coming from the acquirer after the transaction. It will
269 generally receives data posted by the acquirer after the transaction.
271 _name = 'payment.transaction'
272 _description = 'Payment Transaction'
273 _inherit = ['mail.thread']
275 _rec_name = 'reference'
278 'date_create': fields.datetime('Creation Date', readonly=True, required=True),
279 'date_validate': fields.datetime('Validation Date'),
280 'acquirer_id': fields.many2one(
281 'payment.acquirer', 'Acquirer',
284 'type': fields.selection(
285 [('server2server', 'Server To Server'), ('form', 'Form')],
286 string='Type', required=True),
287 'state': fields.selection(
288 [('draft', 'Draft'), ('pending', 'Pending'),
289 ('done', 'Done'), ('error', 'Error'),
290 ('cancel', 'Canceled')
291 ], 'Status', required=True,
292 track_visiblity='onchange'),
293 'state_message': fields.text('Message',
294 help='Field used to store error and/or validation messages for information'),
296 'amount': fields.float('Amount', required=True,
297 help='Amount in cents',
298 track_visibility='always'),
299 'fees': fields.float('Fees', help='Fees amount; set by the system because depends on the acquirer'),
300 'currency_id': fields.many2one('res.currency', 'Currency', required=True),
301 'reference': fields.char('Order Reference', required=True),
302 'acquirer_reference': fields.char('Acquirer Order Reference',
303 help='Reference of the TX as stored in the acquirer database'),
304 # duplicate partner / transaction data to store the values at transaction time
305 'partner_id': fields.many2one('res.partner', 'Partner'),
306 'partner_name': fields.char('Partner Name'),
307 'partner_lang': fields.char('Lang'),
308 'partner_email': fields.char('Email'),
309 'partner_zip': fields.char('Zip'),
310 'partner_address': fields.char('Address'),
311 'partner_city': fields.char('City'),
312 'partner_country_id': fields.many2one('res.country', 'Country', required=True),
313 'partner_phone': fields.char('Phone'),
314 'partner_reference': fields.char('Buyer Reference'),
318 ('reference_uniq', 'UNIQUE(reference)', 'The payment transaction reference must be unique!'),
322 'date_create': fields.datetime.now,
325 'partner_lang': 'en_US',
328 def create(self, cr, uid, values, context=None):
329 Acquirer = self.pool['payment.acquirer']
331 if values.get('partner_id'): # @TDENOTE: not sure
332 values.update(self.on_change_partner_id(cr, uid, None, values.get('partner_id'), context=context)['values'])
334 # call custom create method if defined (i.e. ogone_create for ogone)
335 if values.get('acquirer_id'):
336 acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
339 custom_method_name = '%s_compute_fees' % acquirer.name
340 if hasattr(Acquirer, custom_method_name):
341 values['fees'] = getattr(Acquirer, custom_method_name)(
342 cr, uid, acquirer.id, values.get('amount', 0.0), values.get('currency_id'), values.get('country_id'), context=None)
345 custom_method_name = '%s_create' % acquirer.name
346 if hasattr(self, custom_method_name):
347 values.update(getattr(self, custom_method_name)(cr, uid, values, context=context))
349 return super(PaymentTransaction, self).create(cr, uid, values, context=context)
351 def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
354 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
356 'partner_name': partner and partner.name or False,
357 'partner_lang': partner and partner.lang or 'en_US',
358 'partner_email': partner and partner.email or False,
359 'partner_zip': partner and partner.zip or False,
360 'partner_address': _partner_format_address(partner and partner.street or '', partner and partner.street2 or ''),
361 'partner_city': partner and partner.city or False,
362 'partner_country_id': partner and partner.country_id.id or False,
363 'partner_phone': partner and partner.phone or False,
366 # --------------------------------------------------
367 # FORM RELATED METHODS
368 # --------------------------------------------------
370 def form_feedback(self, cr, uid, data, acquirer_name, context=None):
371 invalid_parameters, tx = None, None
373 tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
374 if hasattr(self, tx_find_method_name):
375 tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context)
377 invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name
378 if hasattr(self, invalid_param_method_name):
379 invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
381 if invalid_parameters:
382 _error_message = '%s: incorrect tx data:\n' % (acquirer_name)
383 for item in invalid_parameters:
384 _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
385 _logger.error(_error_message)
388 feedback_method_name = '_%s_form_validate' % acquirer_name
389 if hasattr(self, feedback_method_name):
390 return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
394 # --------------------------------------------------
395 # SERVER2SERVER RELATED METHODS
396 # --------------------------------------------------
398 def s2s_create(self, cr, uid, values, cc_values, context=None):
399 tx_id, tx_result = self.s2s_send(cr, uid, values, cc_values, context=context)
400 self.s2s_feedback(cr, uid, tx_id, tx_result, context=context)
403 def s2s_send(self, cr, uid, values, cc_values, context=None):
404 """ Create and send server-to-server transaction.
406 :param dict values: transaction values
407 :param dict cc_values: credit card values that are not stored into the
408 payment.transaction object. Acquirers should
409 handle receiving void or incorrect cc values.
420 tx_id, result = None, None
422 if values.get('acquirer_id'):
423 acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
424 custom_method_name = '_%s_s2s_send' % acquirer.name
425 if hasattr(self, custom_method_name):
426 tx_id, result = getattr(self, custom_method_name)(cr, uid, values, cc_values, context=context)
428 if tx_id is None and result is None:
429 tx_id = super(PaymentTransaction, self).create(cr, uid, values, context=context)
430 return (tx_id, result)
432 def s2s_feedback(self, cr, uid, tx_id, data, context=None):
433 """ Handle the feedback of a server-to-server transaction. """
434 tx = self.browse(cr, uid, tx_id, context=context)
435 invalid_parameters = None
437 invalid_param_method_name = '_%s_s2s_get_invalid_parameters' % tx.acquirer_id.name
438 if hasattr(self, invalid_param_method_name):
439 invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
441 if invalid_parameters:
442 _error_message = '%s: incorrect tx data:\n' % (tx.acquirer_id.name)
443 for item in invalid_parameters:
444 _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
445 _logger.error(_error_message)
448 feedback_method_name = '_%s_s2s_validate' % tx.acquirer_id.name
449 if hasattr(self, feedback_method_name):
450 return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
454 def s2s_get_tx_status(self, cr, uid, tx_id, context=None):
455 """ Get the tx status. """
456 tx = self.browse(cr, uid, tx_id, context=context)
458 invalid_param_method_name = '_%s_s2s_get_tx_status' % tx.acquirer_id.name
459 if hasattr(self, invalid_param_method_name):
460 return getattr(self, invalid_param_method_name)(cr, uid, tx, context=context)