summaryrefslogtreecommitdiff
path: root/addons/payment/models
diff options
context:
space:
mode:
authorThibault Delavallée <tde@openerp.com>2014-01-22 18:57:02 +0100
committerThibault Delavallée <tde@openerp.com>2014-01-22 18:57:02 +0100
commit0b69bad9968ce2650f6dffd7df703cd33e7f38af (patch)
tree947ae3933f8b4f382bf090031d1ea15e53e88155 /addons/payment/models
parentf70e54255641f4d3d61777501b20b0f8c7a1fb77 (diff)
[RENAME] payment_acquirer_* -> payment_ *
bzr revid: tde@openerp.com-20140122175702-1h1e51z4njt4s70w
Diffstat (limited to 'addons/payment/models')
-rw-r--r--addons/payment/models/__init__.py3
-rw-r--r--addons/payment/models/payment_acquirer.py470
2 files changed, 473 insertions, 0 deletions
diff --git a/addons/payment/models/__init__.py b/addons/payment/models/__init__.py
new file mode 100644
index 00000000000..da51c9c1854
--- /dev/null
+++ b/addons/payment/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import payment_acquirer
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
new file mode 100644
index 00000000000..1a7afc3031d
--- /dev/null
+++ b/addons/payment/models/payment_acquirer.py
@@ -0,0 +1,470 @@
+# -*- coding: utf-'8' "-*-"
+
+import logging
+
+from openerp.osv import osv, fields
+from openerp.tools import float_round
+
+_logger = logging.getLogger(__name__)
+
+
+def _partner_format_address(address1=False, address2=False):
+ return ' '.join((address1 or '', address2 or '')).strip()
+
+
+def _partner_split_name(partner_name):
+ return [' '.join(partner_name.split()[-1:]), ' '.join(partner_name.split()[:-1])]
+
+
+class ValidationError(ValueError):
+ """ Used for value error when validating transaction data coming from acquirers. """
+ pass
+
+
+class PaymentAcquirer(osv.Model):
+ """ Acquirer Model. Each specific acquirer can extend the model by adding
+ its own fields, using the acquirer_name as a prefix for the new fields.
+ Using the required_if_provider='<name>' attribute on fields it is possible
+ to have required fields that depend on a specific acquirer.
+
+ Each acquirer has a link to an ir.ui.view record that is a template of
+ a button used to display the payment form. See examples in ``payment_ogone``
+ and ``payment_paypal`` modules.
+
+ Methods that should be added in an acquirer-specific implementation:
+
+ - ``<name>_form_generate_values(self, cr, uid, id, reference, amount, currency,
+ partner_id=False, partner_values=None, tx_custom_values=None, context=None)``:
+ method that generates the values used to render the form button template.
+ - ``<name>_get_form_action_url(self, cr, uid, id, context=None):``: method
+ that returns the url of the button form. It is used for example in
+ ecommerce application, if you want to post some data to the acquirer.
+ - ``<name>_compute_fees(self, cr, uid, id, amount, currency_id, country_id,
+ context=None)``: computed the fees of the acquirer, using generic fields
+ defined on the acquirer model (see fields definition).
+
+ Each acquirer should also define controllers to handle communication between
+ OpenERP and the acquirer. It generally consists in return urls given to the
+ button form and that the acquirer uses to send the customer back after the
+ transaction, with transaction details given as a POST request.
+ """
+ _name = 'payment.acquirer'
+ _description = 'Payment Acquirer'
+
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ 'company_id': fields.many2one('res.company', 'Company', required=True),
+ 'message': fields.html('Message', help='Message displayed to help payment and validation'),
+ 'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True),
+ 'env': fields.selection(
+ [('test', 'Test'), ('prod', 'Production')],
+ string='Environment'),
+ 'portal_published': fields.boolean(
+ 'Visible in Portal',
+ help="Make this payment acquirer available (Customer invoices, etc.)"),
+ # Fees
+ 'fees_active': fields.boolean('Compute fees'),
+ 'fees_dom_fixed': fields.float('Fixed domestic fees'),
+ 'fees_dom_var': fields.float('Variable domestic fees (in percents)'),
+ 'fees_int_fixed': fields.float('Fixed international fees'),
+ 'fees_int_var': fields.float('Variable international fees (in percents)'),
+ }
+
+ _defaults = {
+ 'company_id': lambda self, cr, uid, obj, ctx=None: self.pool['res.users'].browse(cr, uid, uid).company_id.id,
+ 'env': 'test',
+ 'portal_published': True,
+ }
+
+ def _check_required_if_provider(self, cr, uid, ids, context=None):
+ """ If the field has 'required_if_provider="<name>"' attribute, then it
+ required if record.name is <name>. """
+ for acquirer in self.browse(cr, uid, ids, context=context):
+ 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]):
+ return False
+ return True
+
+ _constraints = [
+ (_check_required_if_provider, 'Required fields not filled', ['required for this provider']),
+ ]
+
+ def get_form_action_url(self, cr, uid, id, context=None):
+ """ Returns the form action URL, for form-based acquirer implementations. """
+ acquirer = self.browse(cr, uid, id, context=context)
+ if hasattr(self, '%s_get_form_action_url' % acquirer.name):
+ return getattr(self, '%s_get_form_action_url' % acquirer.name)(cr, uid, id, context=context)
+ return False
+
+ def form_preprocess_values(self, cr, uid, id, reference, amount, currency_id, tx_id, partner_id, partner_values, tx_values, context=None):
+ """ Pre process values before giving them to the acquirer-specific render
+ methods. Those methods will receive:
+
+ - partner_values: will contain name, lang, email, zip, address, city,
+ country_id (int or False), country (browse or False), phone, reference
+ - tx_values: will contain refernece, amount, currency_id (int or False),
+ currency (browse or False), partner (browse or False)
+ """
+ acquirer = self.browse(cr, uid, id, context=context)
+
+ if tx_id:
+ tx = self.pool.get('payment.transaction').browse(cr, uid, tx_id, context=context)
+ tx_data = {
+ 'reference': tx.reference,
+ 'amount': tx.amount,
+ 'currency_id': tx.currency_id.id,
+ 'currency': tx.currency_id,
+ 'partner': tx.partner_id,
+ }
+ partner_data = {
+ 'name': tx.partner_name,
+ 'lang': tx.partner_lang,
+ 'email': tx.partner_email,
+ 'zip': tx.partner_zip,
+ 'address': tx.partner_address,
+ 'city': tx.partner_city,
+ 'country_id': tx.partner_country_id.id,
+ 'country': tx.partner_country_id,
+ 'phone': tx.partner_phone,
+ 'reference': tx.partner_reference,
+ }
+ else:
+ if partner_id:
+ partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
+ partner_data = {
+ 'name': partner.name,
+ 'lang': partner.lang,
+ 'email': partner.email,
+ 'zip': partner.zip,
+ 'city': partner.city,
+ 'address': _partner_format_address(partner.street, partner.street2),
+ 'country_id': partner.country_id.id,
+ 'country': partner.country_id,
+ 'phone': partner.phone,
+ }
+ else:
+ partner, partner_data = False, {}
+ partner_data.update(partner_values)
+
+ if currency_id:
+ currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
+ else:
+ currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
+ tx_data = {
+ 'reference': reference,
+ 'amount': amount,
+ 'currency_id': currency.id,
+ 'currency': currency,
+ 'partner': partner,
+ }
+
+ # update tx values
+ tx_data.update(tx_values)
+
+ # update partner values
+ if not partner_data.get('address'):
+ partner_data['address'] = _partner_format_address(partner_data.get('street', ''), partner_data.get('street2', ''))
+ if not partner_data.get('country') and partner_data.get('country_id'):
+ partner_data['country'] = self.pool['res.country'].browse(cr, uid, partner_data.get('country_id'), context=context)
+ partner_data.update({
+ 'first_name': _partner_split_name(partner_data['name'])[0],
+ 'last_name': _partner_split_name(partner_data['name'])[1],
+ })
+
+ # compute fees
+ fees_method_name = '%s_compute_fees' % acquirer.name
+ if hasattr(self, fees_method_name):
+ fees = getattr(self, fees_method_name)(
+ cr, uid, id, tx_data['amount'], tx_data['currency_id'], partner_data['country_id'], context=None)
+ tx_data['fees'] = float_round(fees, 2)
+
+ return (partner_data, tx_data)
+
+ def render(self, cr, uid, id, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, context=None):
+ """ Renders the form template of the given acquirer as a qWeb template.
+ All templates will receive:
+
+ - acquirer: the payment.acquirer browse record
+ - user: the current user browse record
+ - currency: currency browse record
+ - amount: amount of the transaction
+ - reference: reference of the transaction
+ - partner: the current partner browse record, if any (not necessarily set)
+ - partner_values: a dictionary of partner-related values
+ - tx_values: a dictionary of transaction related values that depends on
+ the acquirer. Some specific keys should be managed in each
+ provider, depending on the features it offers:
+
+ - 'feedback_url': feedback URL, controler that manage answer of the acquirer
+ (without base url) -> FIXME
+ - 'return_url': URL for coming back after payment validation (wihout
+ base url) -> FIXME
+ - 'cancel_url': URL if the client cancels the payment -> FIXME
+ - 'error_url': URL if there is an issue with the payment -> FIXME
+
+ - context: OpenERP context dictionary
+
+ :param string reference: the transaction reference
+ :param float amount: the amount the buyer has to pay
+ :param res.currency browse record currency: currency
+ :param int tx_id: id of a transaction; if set, bypasses all other given
+ values and only render the already-stored transaction
+ :param res.partner browse record partner_id: the buyer
+ :param dict partner_values: a dictionary of values for the buyer (see above)
+ :param dict tx_custom_values: a dictionary of values for the transction
+ that is given to the acquirer-specific method
+ generating the form values
+ :param dict context: OpenERP context
+ """
+ if context is None:
+ context = {}
+ if tx_values is None:
+ tx_values = {}
+ if partner_values is None:
+ partner_values = {}
+ acquirer = self.browse(cr, uid, id, context=context)
+
+ # pre-process values
+ amount = float_round(amount, 2)
+ partner_values, tx_values = self.form_preprocess_values(
+ cr, uid, id, reference, amount, currency_id, tx_id, partner_id,
+ partner_values, tx_values, context=context)
+
+ # call <name>_form_generate_values to update the tx dict with acqurier specific values
+ cust_method_name = '%s_form_generate_values' % (acquirer.name)
+ if hasattr(self, cust_method_name):
+ method = getattr(self, cust_method_name)
+ partner_values, tx_values = method(cr, uid, id, partner_values, tx_values, context=context)
+
+ qweb_context = {
+ 'tx_url': context.get('tx_url', self.get_form_action_url(cr, uid, id, context=context)),
+ 'submit_class': context.get('submit_class', 'btn btn-link'),
+ 'submit_txt': context.get('submit_txt'),
+ 'acquirer': acquirer,
+ 'user': self.pool.get("res.users").browse(cr, uid, uid, context=context),
+ 'reference': tx_values['reference'],
+ 'amount': tx_values['amount'],
+ 'currency': tx_values['currency'],
+ 'partner': tx_values.get('partner'),
+ 'partner_values': partner_values,
+ 'tx_values': tx_values,
+ 'context': context,
+ }
+
+ # because render accepts view ids but not qweb -> need to use the xml_id
+ return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context)
+
+
+class PaymentTransaction(osv.Model):
+ """ Transaction Model. Each specific acquirer can extend the model by adding
+ its own fields.
+
+ Methods that can be added in an acquirer-specific implementation:
+
+ - ``<name>_create``: method receiving values used when creating a new
+ transaction and that returns a dictionary that will update those values.
+ This method can be used to tweak some transaction values.
+
+ Methods defined for convention, depending on your controllers:
+
+ - ``<name>_form_feedback(self, cr, uid, data, context=None)``: method that
+ handles the data coming from the acquirer after the transaction. It will
+ generally receives data posted by the acquirer after the transaction.
+ """
+ _name = 'payment.transaction'
+ _description = 'Payment Transaction'
+ _inherit = ['mail.thread']
+ _order = 'id desc'
+ _rec_name = 'reference'
+
+ _columns = {
+ 'date_create': fields.datetime('Creation Date', readonly=True, required=True),
+ 'date_validate': fields.datetime('Validation Date'),
+ 'acquirer_id': fields.many2one(
+ 'payment.acquirer', 'Acquirer',
+ required=True,
+ ),
+ 'type': fields.selection(
+ [('server2server', 'Server To Server'), ('form', 'Form')],
+ string='Type', required=True),
+ 'state': fields.selection(
+ [('draft', 'Draft'), ('pending', 'Pending'),
+ ('done', 'Done'), ('error', 'Error'),
+ ('cancel', 'Canceled')
+ ], 'Status', required=True,
+ track_visiblity='onchange'),
+ 'state_message': fields.text('Message',
+ help='Field used to store error and/or validation messages for information'),
+ # payment
+ 'amount': fields.float('Amount', required=True,
+ digits=(16, 2),
+ track_visibility='always',
+ help='Amount in cents'),
+ 'fees': fields.float('Fees',
+ digits=(16, 2),
+ track_visibility='always',
+ help='Fees amount; set by the system because depends on the acquirer'),
+ 'currency_id': fields.many2one('res.currency', 'Currency', required=True),
+ 'reference': fields.char('Order Reference', required=True),
+ 'acquirer_reference': fields.char('Acquirer Order Reference',
+ help='Reference of the TX as stored in the acquirer database'),
+ # duplicate partner / transaction data to store the values at transaction time
+ 'partner_id': fields.many2one('res.partner', 'Partner', track_visibility='onchange',),
+ 'partner_name': fields.char('Partner Name'),
+ 'partner_lang': fields.char('Lang'),
+ 'partner_email': fields.char('Email'),
+ 'partner_zip': fields.char('Zip'),
+ 'partner_address': fields.char('Address'),
+ 'partner_city': fields.char('City'),
+ 'partner_country_id': fields.many2one('res.country', 'Country', required=True),
+ 'partner_phone': fields.char('Phone'),
+ 'partner_reference': fields.char('Partner Reference',
+ help='Reference of the customer in the acquirer database'),
+ }
+
+ _sql_constraints = [
+ ('reference_uniq', 'UNIQUE(reference)', 'The payment transaction reference must be unique!'),
+ ]
+
+ _defaults = {
+ 'date_create': fields.datetime.now,
+ 'type': 'form',
+ 'state': 'draft',
+ 'partner_lang': 'en_US',
+ }
+
+ def create(self, cr, uid, values, context=None):
+ Acquirer = self.pool['payment.acquirer']
+
+ if values.get('partner_id'): # @TDENOTE: not sure
+ values.update(self.on_change_partner_id(cr, uid, None, values.get('partner_id'), context=context)['values'])
+
+ # call custom create method if defined (i.e. ogone_create for ogone)
+ if values.get('acquirer_id'):
+ acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
+
+ # compute fees
+ custom_method_name = '%s_compute_fees' % acquirer.name
+ if hasattr(Acquirer, custom_method_name):
+ fees = getattr(Acquirer, custom_method_name)(
+ cr, uid, acquirer.id, values.get('amount', 0.0), values.get('currency_id'), values.get('country_id'), context=None)
+ values['fees'] = float_round(fees, 2)
+
+ # custom create
+ custom_method_name = '%s_create' % acquirer.name
+ if hasattr(self, custom_method_name):
+ values.update(getattr(self, custom_method_name)(cr, uid, values, context=context))
+
+ return super(PaymentTransaction, self).create(cr, uid, values, context=context)
+
+ def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
+ partner = None
+ if partner_id:
+ partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
+ return {'values': {
+ 'partner_name': partner and partner.name or False,
+ 'partner_lang': partner and partner.lang or 'en_US',
+ 'partner_email': partner and partner.email or False,
+ 'partner_zip': partner and partner.zip or False,
+ 'partner_address': _partner_format_address(partner and partner.street or '', partner and partner.street2 or ''),
+ 'partner_city': partner and partner.city or False,
+ 'partner_country_id': partner and partner.country_id.id or False,
+ 'partner_phone': partner and partner.phone or False,
+ }}
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ def form_feedback(self, cr, uid, data, acquirer_name, context=None):
+ invalid_parameters, tx = None, None
+
+ tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
+ if hasattr(self, tx_find_method_name):
+ tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context)
+
+ invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name
+ if hasattr(self, invalid_param_method_name):
+ invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
+
+ if invalid_parameters:
+ _error_message = '%s: incorrect tx data:\n' % (acquirer_name)
+ for item in invalid_parameters:
+ _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
+ _logger.error(_error_message)
+ return False
+
+ feedback_method_name = '_%s_form_validate' % acquirer_name
+ if hasattr(self, feedback_method_name):
+ return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
+
+ return True
+
+ # --------------------------------------------------
+ # SERVER2SERVER RELATED METHODS
+ # --------------------------------------------------
+
+ def s2s_create(self, cr, uid, values, cc_values, context=None):
+ tx_id, tx_result = self.s2s_send(cr, uid, values, cc_values, context=context)
+ self.s2s_feedback(cr, uid, tx_id, tx_result, context=context)
+ return tx_id
+
+ def s2s_send(self, cr, uid, values, cc_values, context=None):
+ """ Create and send server-to-server transaction.
+
+ :param dict values: transaction values
+ :param dict cc_values: credit card values that are not stored into the
+ payment.transaction object. Acquirers should
+ handle receiving void or incorrect cc values.
+ Should contain :
+
+ - holder_name
+ - number
+ - cvc
+ - expiry_date
+ - brand
+ - expiry_date_yy
+ - expiry_date_mm
+ """
+ tx_id, result = None, None
+
+ if values.get('acquirer_id'):
+ acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
+ custom_method_name = '_%s_s2s_send' % acquirer.name
+ if hasattr(self, custom_method_name):
+ tx_id, result = getattr(self, custom_method_name)(cr, uid, values, cc_values, context=context)
+
+ if tx_id is None and result is None:
+ tx_id = super(PaymentTransaction, self).create(cr, uid, values, context=context)
+ return (tx_id, result)
+
+ def s2s_feedback(self, cr, uid, tx_id, data, context=None):
+ """ Handle the feedback of a server-to-server transaction. """
+ tx = self.browse(cr, uid, tx_id, context=context)
+ invalid_parameters = None
+
+ invalid_param_method_name = '_%s_s2s_get_invalid_parameters' % tx.acquirer_id.name
+ if hasattr(self, invalid_param_method_name):
+ invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
+
+ if invalid_parameters:
+ _error_message = '%s: incorrect tx data:\n' % (tx.acquirer_id.name)
+ for item in invalid_parameters:
+ _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
+ _logger.error(_error_message)
+ return False
+
+ feedback_method_name = '_%s_s2s_validate' % tx.acquirer_id.name
+ if hasattr(self, feedback_method_name):
+ return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
+
+ return True
+
+ def s2s_get_tx_status(self, cr, uid, tx_id, context=None):
+ """ Get the tx status. """
+ tx = self.browse(cr, uid, tx_id, context=context)
+
+ invalid_param_method_name = '_%s_s2s_get_tx_status' % tx.acquirer_id.name
+ if hasattr(self, invalid_param_method_name):
+ return getattr(self, invalid_param_method_name)(cr, uid, tx, context=context)
+
+ return True