[MERGE] [IMP] payment_acquirer: added fees computation possibility + paypal: added fees computation

- acquirer model: added fees_active and fees_* fields: fix and variable
fees, for domestic and international transactions.
- updated views accordingly + cleaned paypal form view.
- updated tests
- updated form, adding handling input in the paypal button that is the input controlling
the fees in the tx process.

bzr revid: tde@openerp.com-20131202162630-lj0hu16qm0zj2y1f
This commit is contained in:
Thibault Delavallée
2013-12-02 17:26:30 +01:00
7 changed files with 192 additions and 59 deletions
@@ -48,6 +48,7 @@ class PaymentAcquirer(osv.Model):
_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(
@@ -55,11 +56,18 @@ class PaymentAcquirer(osv.Model):
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 = {
'portal_published': True,
'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):
@@ -74,9 +82,36 @@ class PaymentAcquirer(osv.Model):
(_check_required_if_provider, 'Required fields not filled', ['required for this provider']),
]
def render(self, cr, uid, id, reference, amount, currency, tx_id=None, partner_id=False, partner_values=None, tx_custom_values=None, context=None):
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_custom_values, context=None):
acquirer = self.browse(cr, uid, id, context=context)
tx_values = tx_custom_values
# compute country
if partner_id:
partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
tx_values['country_id'] = partner.country_id.id
elif partner_values and partner_values.get('country_name'):
country_ids = self.pool['res.country'].search(cr, uid, [('name', '=', partner_values.get('country_name'))], context=context)
tx_values['country_id'] = country_ids and country_ids[0] or None
else:
tx_values['country_id'] = False
# compute fees
if hasattr(self, '%s_compute_fees' % acquirer.name):
tx_values['fees'] = getattr(self, '%s_compute_fees' % acquirer.name)(cr, uid, id, amount, currency_id, tx_values.get('country_id'), context=None)
return tx_values
def render(self, cr, uid, id, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_custom_values=None, context=None):
""" Renders the form template of the given acquirer as a qWeb template.
All templates should handle:
All templates will receive:
- acquirer: the payment.acquirer browse record
- user: the current user browse record
@@ -90,11 +125,11 @@ class PaymentAcquirer(osv.Model):
in each provider, depending on the features it offers:
- 'feedback_url': feedback URL, controler that manage answer of the acquirer
(without base url)
(without base url) -> FIXME
- 'return_url': URL for coming back after payment validation (wihout
base url)
- 'cancel_url': URL if the client cancels the payment
- 'error_url': URL if there is an issue with the payment
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
@@ -112,20 +147,25 @@ class PaymentAcquirer(osv.Model):
"""
if context is None:
context = {}
if tx_custom_values is None:
tx_custom_values = {}
partner = None
if partner_id:
partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
acquirer = self.browse(cr, uid, id, context=context)
method_name = '%s_form_generate_values' % (acquirer.name)
currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
if tx_id and hasattr(self.pool['payment.transaction'], method_name):
method = getattr(self.pool['payment.transaction'], method_name)
tx_values = method(cr, uid, tx_id, tx_custom_values, context=context)
elif hasattr(self, method_name):
method = getattr(self, method_name)
tx_values = method(cr, uid, id, reference, amount, currency, partner_id, partner_values, tx_custom_values, context=context)
else:
tx_values = tx_custom_values
# pre-process values
tx_values = self.form_preprocess_values(cr, uid, id, reference, amount, currency, tx_id, partner_id, partner_values, tx_custom_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 tx_id and hasattr(self.pool['payment.transaction'], cust_method_name):
method = getattr(self.pool['payment.transaction'], cust_method_name)
tx_values = method(cr, uid, tx_id, tx_values, context=context)
elif hasattr(self, cust_method_name):
method = getattr(self, cust_method_name)
tx_values = method(cr, uid, id, reference, amount, currency, partner_id, partner_values, tx_values, context=context)
qweb_context = {
'acquirer': acquirer,
@@ -141,12 +181,6 @@ class PaymentAcquirer(osv.Model):
# because render accepts view ids but not qweb -> need to find 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)
def get_form_action_url(self, cr, uid, id, context=None):
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
class PaymentTransaction(osv.Model):
""" Transaction Model. Each specific acquirer can extend the model by adding
@@ -198,6 +232,7 @@ class PaymentTransaction(osv.Model):
'amount': fields.float('Amount', required=True,
help='Amount in cents',
track_visibility='always'),
'fees': fields.float('Fees', 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),
# duplicate partner / transaction data to store the values at transaction time
@@ -225,12 +260,22 @@ class PaymentTransaction(osv.Model):
}
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):
values['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)
# 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))
@@ -12,32 +12,50 @@
<field name="model">payment.acquirer</field>
<field name="arch" type="xml">
<form string="Payment Acquirer" version="7.0">
<group name="acquirer_base">
<field name="name"/>
<field name="portal_published"/>
<field name="env"/>
<field name="message"/>
<label for="view_template_id"/>
<div>
<field name="view_template_id" nolabel="1"/>
<sheet>
<group name="acquirer_base">
<group>
<field name="name"/>
<field name="company_id"/>
<field name="portal_published"/>
<field name="env"/>
</group>
<group>
<field name="fees_active"/>
<field name="fees_dom_fixed"
attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_dom_var"
attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_int_fixed"
attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_int_var"
attrs="{'invisible': [('fees_active', '=', False)]}"/>
</group>
</group>
<group name="acquirer_display">
<field name="message"/>
<label for="view_template_id"/>
<div>
This is an HTML form template to submit a payment through this acquirer.
The template will be rendered with qWeb, so it may use qWeb expressions.
The qWeb evaluation context provides:
<ul>
<li>acquirer: payment.acquirer browse record</li>
<li>user: current user browse record</li>
<li>reference: the transaction reference number</li>
<li>currency: the transaction currency browse record</li>
<li>amount: the transaction amount, a float</li>
<li>partner: the buyer partner browse record, not necessarily set</li>
<li>partner_values: specific values about the buyer, for example coming from a shipping form</li>
<li>tx_values: specific transaction values</li>
<li>context: the current context dictionary</li>
</ul>
<field name="view_template_id" nolabel="1"/>
<div>
This is an HTML form template to submit a payment through this acquirer.
The template will be rendered with qWeb, so it may use qWeb expressions.
The qWeb evaluation context provides:
<ul>
<li>acquirer: payment.acquirer browse record</li>
<li>user: current user browse record</li>
<li>reference: the transaction reference number</li>
<li>currency: the transaction currency browse record</li>
<li>amount: the transaction amount, a float</li>
<li>partner: the buyer partner browse record, not necessarily set</li>
<li>partner_values: specific values about the buyer, for example coming from a shipping form</li>
<li>tx_values: specific transaction values</li>
<li>context: the current context dictionary</li>
</ul>
</div>
</div>
</div>
</group>
</group>
</sheet>
</form>
</field>
</record>
@@ -84,6 +102,7 @@
<group>
<field name="reference"/>
<field name="amount"/>
<field name="fees"/>
<field name="currency_id"/>
<field name="partner_id"/>
<field name="partner_name"/>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<data noupdate="1">
<record id="payment_acquirer_paypal" model="payment.acquirer">
<field name="name">paypal</field>
@@ -10,7 +10,7 @@
<field name="paypal_email_id">dummy</field>
<field name="paypal_username">dummy</field>
<field name="paypal_api_username">dummy</field>
<field name="paypal_api_password">dummy-Ud_oozfiDqqN3mUVLMmzPK71IZA3TM4taicUY2uaJYU1</field>
<field name="paypal_api_password">dummy</field>
</record>
</data>
@@ -27,6 +27,7 @@ class AcquirerPaypal(osv.Model):
'paypal_username': fields.char('Username', required_if_provider='paypal'),
'paypal_tx_url': fields.char('Transaction URL', required_if_provider='paypal'),
'paypal_use_ipn': fields.boolean('Use IPN'),
# Server 2 server
'paypal_api_enabled': fields.boolean('Use Rest API'),
'paypal_api_username': fields.char('Rest API Username'),
'paypal_api_password': fields.char('Rest API Password'),
@@ -35,11 +36,35 @@ class AcquirerPaypal(osv.Model):
}
_defaults = {
'paypal_use_ipn': True,
'paypal_api_enabled': False,
'paypal_tx_url': 'https://www.sandbox.paypal.com/cgi-bin/webscr',
'paypal_use_ipn': True,
'fees_active': False,
'fees_dom_fixed': 0.35,
'fees_dom_var': 3.4,
'fees_int_fixed': 0.35,
'fees_int_var': 3.9,
'paypal_api_enabled': False,
}
def paypal_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None):
""" Compute paypal fees.
:param float amount: the amount to pay
:param integer country_id: an ID of a res.country, or None. This is
the customer's country, to be compared to
the acquirer company country.
:return float fees: computed fees
"""
acquirer = self.browse(cr, uid, id, context=context)
if not acquirer.fees_active:
return 0.0
country = self.pool['res.country'].browse(cr, uid, country_id, context=context)
if country and acquirer.company_id.country_id.id == country.id:
fees = amount * (1 + acquirer.fees_dom_var / 100.0) + acquirer.fees_dom_fixed - amount
else:
fees = amount * (1 + acquirer.fees_int_var / 100.0) + acquirer.fees_int_fixed - amount
return fees
def paypal_form_generate_values(self, cr, uid, id, reference, amount, currency, partner_id=False, partner_values=None, tx_custom_values=None, context=None):
if partner_values is None:
partner_values = {}
@@ -48,6 +73,7 @@ class AcquirerPaypal(osv.Model):
partner = None
if partner_id:
partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
tx_values = {
'cmd': '_xclick',
'business': acquirer.paypal_email_id,
@@ -66,6 +92,8 @@ class AcquirerPaypal(osv.Model):
'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
}
if acquirer.fees_active:
tx_values['handling'] = '%.2f' % tx_custom_values.pop('fees', 0.0)
if tx_custom_values and tx_custom_values.get('return_url'):
tx_values['custom'] = json.dumps({'return_url': '%s' % tx_custom_values.pop('return_url')})
if tx_custom_values:
@@ -95,6 +95,9 @@ class PaypalForm(PaypalCommon):
def test_10_paypal_form_render(self):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
paypal = self.payment_acquirer.browse(cr, uid, self.paypal_id, context)
self.assertEqual(paypal.env, 'test', 'test without test env')
# ----------------------------------------
# Test: button direct rendering
@@ -139,9 +142,40 @@ class PaypalForm(PaypalCommon):
'paypal: wrong value for form: received %s instead of %s' % (form_input.get('value'), form_values[form_input.get('name')])
)
def test_11_paypal_form_with_fees(self):
cr, uid, context = self.cr, self.uid, {}
self.payment_acquirer.write(cr, uid, self.paypal_id, {
'fees_active': True,
}, context)
# be sure not to do stupid things
paypal = self.payment_acquirer.browse(self.cr, self.uid, self.paypal_id, None)
self.assertEqual(paypal.env, 'test', 'test without test env')
# render the button
res = self.payment_acquirer.render(
cr, uid, self.paypal_id,
'test_ref0', 12.50, self.currency_euro,
partner_id=None,
partner_values=self.buyer_values,
context=context)
# check form result
handling_found = False
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://www.sandbox.paypal.com/cgi-bin/webscr', 'paypal: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['handling']:
handling_found = True
self.assertEqual(form_input.get('value'), '0.84', 'paypal: wrong computed fees')
self.assertTrue(handling_found, 'paypal: fees_active did not add handling input in rendered form')
@mute_logger('openerp.addons.payment_acquirer_paypal.models.paypal', 'ValidationError')
def test_20_paypal_form_management(self):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
paypal = self.payment_acquirer.browse(cr, uid, self.paypal_id, context)
self.assertEqual(paypal.env, 'test', 'test without test env')
# typical data posted by paypal after client has successfully paid
paypal_post_data = {
@@ -7,15 +7,18 @@
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment_acquirer.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer_base"]' position='after'>
<group string="Paypal Details"
attrs="{'invisible': [('name', '!=', 'paypal')]}">
<field name="paypal_email_id"/>
<field name="paypal_username"/>
<field name="paypal_use_ipn"/>
<field name="paypal_api_enabled"/>
<field name="paypal_api_username"/>
<field name="paypal_api_password"/>
<xpath expr='//group[@name="acquirer_display"]' position='after'>
<group attrs="{'invisible': [('name', '!=', 'paypal')]}">
<group>
<field name="paypal_email_id"/>
<field name="paypal_username"/>
<field name="paypal_use_ipn"/>
<field name="paypal_api_enabled"/>
<field name="paypal_api_username"
attrs="{'invisible': [('paypal_api_enabled', '=', False)]}"/>
<field name="paypal_api_password"
attrs="{'invisible': [('paypal_api_enabled', '=', False)]}"/>
</group>
</group>
</xpath>
</field>
@@ -9,6 +9,10 @@
<input type="hidden" name="item_name" t-att-value="tx_values['item_name']"/>
<input type="hidden" name="item_number" t-att-value="tx_values['item_number']"/>
<input type="hidden" name="amount" t-att-value="tx_values['amount']"/>
<t t-if="'handling' in tx_values">
<input type="hidden" name="handling" t-att-value="tx_values['handling']"/>
</t>
<input type="hidden" name="amount" t-att-value="tx_values['amount']"/>
<input type="hidden" name="currency_code" t-att-value="tx_values['currency_code']"/>
<!-- partner / address data -->
<input type="hidden" name="address1" t-att-value="tx_values['address1']"/>