[REF] payment_ogone: migrate Ogone to the new payment API

In this commit, we take the opportunity to replace the previous unsecure
API with the new FlexCheckout API that implements payment methods
validation in a hosted page. Payment processing is still done with the
DirectLink API.

This commit also renames the module `payment_ingenico` to
`payment_ogone` as well as the referring strings ("Ogone" instead
of "Ingenico", ...).
A dedicated [MOV] commit is not used because neither the [MOV] commit
nor the adapted [REF] would be valid on its own.

See the merge commit for more details.

task-2333029
task-2313907
task-2334015

Co-authored-by: Antoine Vandevenne <anv@odoo.com>
This commit is contained in:
Arnaud Joset
2021-03-30 09:25:51 +02:00
committed by Antoine Vandevenne (anv)
co-authored by Antoine Vandevenne
parent 471a1f39f5
commit 61a02a7330
56 changed files with 844 additions and 1551 deletions
-20
View File
@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': 'Ingenico Payment Acquirer',
'category': 'Accounting/Payment Acquirers',
'sequence': 360,
'summary': 'Payment Acquirer: Ingenico Implementation',
'version': '1.0',
'description': """Ingenico Payment Acquirer""",
'depends': ['payment'],
'data': [
'views/payment_views.xml',
'views/payment_ingenico_templates.xml',
'data/payment_acquirer_data.xml',
],
'installable': True,
'application': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
'uninstall_hook': 'uninstall_hook',
}
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import main
-126
View File
@@ -1,126 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import pprint
import werkzeug
from werkzeug.urls import url_unquote_plus
from odoo import http
from odoo.http import request
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment.controllers.portal import PaymentProcessing
_logger = logging.getLogger(__name__)
class OgoneController(http.Controller):
_accept_url = '/payment/ogone/test/accept'
_decline_url = '/payment/ogone/test/decline'
_exception_url = '/payment/ogone/test/exception'
_cancel_url = '/payment/ogone/test/cancel'
@http.route([
'/payment/ogone/accept', '/payment/ogone/test/accept',
'/payment/ogone/decline', '/payment/ogone/test/decline',
'/payment/ogone/exception', '/payment/ogone/test/exception',
'/payment/ogone/cancel', '/payment/ogone/test/cancel',
], type='http', auth='public', csrf=False, method=['GET', 'POST'])
def ogone_form_feedback(self, **post):
""" Handle both redirection from Ingenico (GET) and s2s notification (POST/GET) """
_logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
return werkzeug.utils.redirect("/payment/process")
@http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json(self, **kwargs):
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
new_id = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
return new_id.id
@http.route(['/payment/ogone/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = False
error = None
try:
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
except Exception as e:
error = str(e)
if not token:
res = {
'result': False,
'error': error,
}
return res
res = {
'result': True,
'id': token.id,
'short_name': token.short_name,
'3d_secure': False,
'verified': False,
}
if verify_validity != False:
baseurl = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
params = {
'accept_url': baseurl + '/payment/ogone/validate/accept',
'decline_url': baseurl + '/payment/ogone/validate/decline',
'exception_url': baseurl + '/payment/ogone/validate/exception',
'return_url': kwargs.get('return_url', baseurl)
}
tx = token.validate(**params)
res['verified'] = token.verified
if tx and tx.html_3ds:
res['3d_secure'] = tx.html_3ds
return res
@http.route(['/payment/ogone/s2s/create'], type='http', auth='public', methods=["POST"], csrf=False)
def ogone_s2s_create(self, **post):
error = ''
acq = request.env['payment.acquirer'].browse(int(post.get('acquirer_id')))
try:
token = acq.s2s_process(post)
except Exception as e:
# synthax error: 'CHECK ERROR: |Not a valid date\n\n50001111: None'
token = False
error = str(e).splitlines()[0].split('|')[-1] or ''
if token and post.get('verify_validity'):
baseurl = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
params = {
'accept_url': baseurl + '/payment/ogone/validate/accept',
'decline_url': baseurl + '/payment/ogone/validate/decline',
'exception_url': baseurl + '/payment/ogone/validate/exception',
'return_url': post.get('return_url', baseurl)
}
tx = token.validate(**params)
if tx and tx.html_3ds:
return tx.html_3ds
# add the payment transaction into the session to let the page /payment/process to handle it
PaymentProcessing.add_payment_transaction(tx)
return werkzeug.utils.redirect("/payment/process")
@http.route([
'/payment/ogone/validate/accept',
'/payment/ogone/validate/decline',
'/payment/ogone/validate/exception',
], type='http', auth='public')
def ogone_validation_form_feedback(self, **post):
""" Feedback from 3d secure for a bank card validation """
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
return werkzeug.utils.redirect("/payment/process")
@http.route(['/payment/ogone/s2s/feedback'], auth='public', csrf=False)
def feedback(self, **kwargs):
try:
tx = request.env['payment.transaction'].sudo()._ogone_form_get_tx_from_data(kwargs)
tx._ogone_s2s_validate_tree(kwargs)
except ValidationError:
return 'ko'
return 'ok'
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import ogone
-497
View File
@@ -1,497 +0,0 @@
# -*- coding: utf-8 -*-
OGONE_ERROR_MAP = {
'0020001001': "Authorization failed, please retry",
'0020001002': "Authorization failed, please retry",
'0020001003': "Authorization failed, please retry",
'0020001004': "Authorization failed, please retry",
'0020001005': "Authorization failed, please retry",
'0020001006': "Authorization failed, please retry",
'0020001007': "Authorization failed, please retry",
'0020001008': "Authorization failed, please retry",
'0020001009': "Authorization failed, please retry",
'0020001010': "Authorization failed, please retry",
'0030001999': "Our payment system is currently under maintenance, please try later",
'0050001005': "Expiration Date error",
'0050001007': "Requested Operation code not allowed",
'0050001008': "Invalid delay value",
'0050001010': "Input date in invalid format",
'0050001013': "Unable to parse socket input stream",
'0050001014': "Error in parsing stream content",
'0050001015': "Currency error",
'0050001016': "Transaction still posted at end of wait",
'0050001017': "Sync value not compatible with delay value",
'0050001019': "Transaction duplicate of a pre-existing transaction",
'0050001020': "Acceptation code empty while required for the transaction",
'0050001024': "Maintenance acquirer differs from original transaction acquirer",
'0050001025': "Maintenance merchant differs from original transaction merchant",
'0050001028': "Maintenance operation not accurate for the original transaction",
'0050001031': "Host application unknown for the transaction",
'0050001032': "Unable to perform requested operation with requested currency",
'0050001033': "Maintenance card number differs from original transaction card number",
'0050001034': "Operation code not allowed",
'0050001035': "Exception occurred in socket input stream treatment",
'0050001036': "Card length does not correspond to an acceptable value for the brand",
'0050001036': "Card length does not correspond to an acceptable value for the brand",
'0050001068': "A technical problem occurred, please contact helpdesk",
'0050001069': "Invalid check for CardID and Brand",
'0050001070': "A technical problem occurred, please contact helpdesk",
'0050001116': "Unknown origin IP",
'0050001117': "No origin IP detected",
'0050001118': "Merchant configuration problem, please contact support",
'10001001': "Communication failure",
'10001002': "Communication failure",
'10001003': "Communication failure",
'10001004': "Communication failure",
'10001005': "Communication failure",
'20001001': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001002': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001003': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001004': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001005': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001006': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001007': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001008': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001009': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001010': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001101': "A technical problem occurred, please contact helpdesk",
'20001105': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
'20001111': "A technical problem occurred, please contact helpdesk",
'20002001': "Origin for the response of the bank can not be checked",
'20002002': "Beneficiary account number has been modified during processing",
'20002003': "Amount has been modified during processing",
'20002004': "Currency has been modified during processing",
'20002005': "No feedback from the bank server has been detected",
'30001001': "Payment refused by the acquirer",
'30001002': "Duplicate request",
'30001010': "A technical problem occurred, please contact helpdesk",
'30001011': "A technical problem occurred, please contact helpdesk",
'30001012': "Card black listed - Contact acquirer",
'30001015': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
'30001051': "A technical problem occurred, please contact helpdesk",
'30001054': "A technical problem occurred, please contact helpdesk",
'30001057': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
'30001058': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
'30001060': "Aquirer indicates that a failure occured during payment processing",
'30001070': "RATEPAY Invalid Response Type (Failure)",
'30001071': "RATEPAY Missing Mandatory status code field (failure)",
'30001072': "RATEPAY Missing Mandatory Result code field (failure)",
'30001073': "RATEPAY Response parsing Failed",
'30001090': "CVC check required by front end and returned invalid by acquirer",
'30001091': "ZIP check required by front end and returned invalid by acquirer",
'30001092': "Address check required by front end and returned as invalid by acquirer.",
'30001100': "Unauthorized buyer's country",
'30001101': "IP country <> card country",
'30001102': "Number of different countries too high",
'30001103': "unauthorized card country",
'30001104': "unauthorized ip address country",
'30001105': "Anonymous proxy",
'30001110': "If the problem persists, please contact Support, or go to paysafecard's card balance page (https://customer.cc.at.paysafecard.com/psccustomer/GetWelcomePanelServlet?language=en) to see when the amount reserved on your card will be available again.",
'30001120': "IP address in merchant's black list",
'30001130': "BIN in merchant's black list",
'30001131': "Wrong BIN for 3xCB",
'30001140': "Card in merchant's card blacklist",
'30001141': "Email in blacklist",
'30001142': "Passenger name in blacklist",
'30001143': "Card holder name in blacklist",
'30001144': "Passenger name different from owner name",
'30001145': "Time to departure too short",
'30001149': "Card Configured in Card Supplier Limit for another relation (CSL)",
'30001150': "Card not configured in the system for this customer (CSL)",
'30001151': "REF1 not allowed for this relationship (Contract number",
'30001152': "Card/Supplier Amount limit reached (CSL)",
'30001153': "Card not allowed for this supplier (Date out of contract bounds)",
'30001154': "You have reached the usage limit allowed",
'30001155': "You have reached the usage limit allowed",
'30001156': "You have reached the usage limit allowed",
'30001157': "Unauthorized IP country for itinerary",
'30001158': "email usage limit reached",
'30001159': "Unauthorized card country/IP country combination",
'30001160': "Postcode in highrisk group",
'30001161': "generic blacklist match",
'30001162': "Billing Address is a PO Box",
'30001180': "maximum scoring reached",
'30001997': "Authorization canceled by simulation",
'30001998': "A technical problem occurred, please try again.",
'30001999': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
'30002001': "Payment refused by the financial institution",
'30002001': "Payment refused by the financial institution",
'30021001': "Call acquirer support call number.",
'30022001': "Payment must be approved by the acquirer before execution.",
'30031001': "Invalid merchant number.",
'30041001': "Retain card.",
'30051001': "Authorization declined",
'30071001': "Retain card - special conditions.",
'30121001': "Invalid transaction",
'30131001': "Invalid amount",
'30131002': "You have reached the total amount allowed",
'30141001': "Invalid card number",
'30151001': "Unknown acquiring institution.",
'30171001': "Payment method cancelled by the buyer",
'30171002': "The maximum time allowed is elapsed.",
'30191001': "Try again later.",
'30201001': "A technical problem occurred, please contact helpdesk",
'30301001': "Invalid format",
'30311001': "Unknown acquirer ID.",
'30331001': "Card expired.",
'30341001': "Suspicion of fraud.",
'30341002': "Suspicion of fraud (3rdMan)",
'30341003': "Suspicion of fraud (Perseuss)",
'30341004': "Suspicion of fraud (ETHOCA)",
'30381001': "A technical problem occurred, please contact helpdesk",
'30401001': "Invalid function.",
'30411001': "Lost card.",
'30431001': "Stolen card, pick up",
'30511001': "Insufficient funds.",
'30521001': "No Authorization. Contact the issuer of your card.",
'30541001': "Card expired.",
'30551001': "Invalid PIN.",
'30561001': "Card not in authorizer's database.",
'30571001': "Transaction not permitted on card.",
'30581001': "Transaction not allowed on this terminal",
'30591001': "Suspicion of fraud.",
'30601001': "The merchant must contact the acquirer.",
'30611001': "Amount exceeds card ceiling.",
'30621001': "Restricted card.",
'30631001': "Security policy not respected.",
'30641001': "Amount changed from ref. trn.",
'30681001': "Tardy response.",
'30751001': "PIN entered incorrectly too often",
'30761001': "Card holder already contesting.",
'30771001': "PIN entry required.",
'30811001': "Message flow error.",
'30821001': "Authorization center unavailable",
'30831001': "Authorization center unavailable",
'30901001': "Temporary system shutdown.",
'30911001': "Acquirer unavailable.",
'30921001': "Invalid card type for acquirer.",
'30941001': "Duplicate transaction",
'30961001': "Processing temporarily not possible",
'30971001': "A technical problem occurred, please contact helpdesk",
'30981001': "A technical problem occurred, please contact helpdesk",
'31011001': "Unknown acceptance code",
'31021001': "Invalid currency",
'31031001': "Acceptance code missing",
'31041001': "Inactive card",
'31051001': "Merchant not active",
'31061001': "Invalid expiration date",
'31071001': "Interrupted host communication",
'31081001': "Card refused",
'31091001': "Invalid password",
'31101001': "Plafond transaction (majoré du bonus) dépassé",
'31111001': "Plafond mensuel (majoré du bonus) dépassé",
'31121001': "Plafond centre de facturation dépassé",
'31131001': "Plafond entreprise dépassé",
'31141001': "Code MCC du fournisseur non autorisé pour la carte",
'31151001': "Numéro SIRET du fournisseur non autorisé pour la carte",
'31161001': "This is not a valid online banking account",
'32001004': "A technical problem occurred, please try again.",
'34011001': "Bezahlung mit RatePAY nicht möglich.",
'39991001': "A technical problem occurred, please contact the helpdesk of your acquirer",
'40001001': "A technical problem occurred, please try again.",
'40001002': "A technical problem occurred, please try again.",
'40001003': "A technical problem occurred, please try again.",
'40001004': "A technical problem occurred, please try again.",
'40001005': "A technical problem occurred, please try again.",
'40001006': "A technical problem occurred, please try again.",
'40001007': "A technical problem occurred, please try again.",
'40001008': "A technical problem occurred, please try again.",
'40001009': "A technical problem occurred, please try again.",
'40001010': "A technical problem occurred, please try again.",
'40001011': "A technical problem occurred, please contact helpdesk",
'40001012': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
'40001013': "A technical problem occurred, please contact helpdesk",
'40001016': "A technical problem occurred, please contact helpdesk",
'40001018': "A technical problem occurred, please try again.",
'40001019': "Sorry, an error occurred during processing. Please retry the operation (use back button of the browser). If problem persists, contact your merchant's helpdesk.",
'40001020': "Sorry, an error occurred during processing. Please retry the operation (use back button of the browser). If problem persists, contact your merchant's helpdesk.",
'40001050': "A technical problem occurred, please contact helpdesk",
'40001133': "Authentication failed, the signature of your bank access control server is incorrect",
'40001134': "Authentication failed, please retry or cancel.",
'40001135': "Authentication temporary unavailable, please retry or cancel.",
'40001136': "Technical problem with your browser, please retry or cancel",
'40001137': "Your bank access control server is temporary unavailable, please retry or cancel",
'40001998': "Temporary technical problem. Please retry a little bit later.",
'50001001': "Unknown card type",
'50001002': "Card number format check failed for given card number.",
'50001003': "Merchant data error",
'50001004': "Merchant identification missing",
'50001005': "Expiration Date error",
'50001006': "Amount is not a number",
'50001007': "A technical problem occurred, please contact helpdesk",
'50001008': "A technical problem occurred, please contact helpdesk",
'50001009': "A technical problem occurred, please contact helpdesk",
'50001010': "A technical problem occurred, please contact helpdesk",
'50001011': "Brand not supported for that merchant",
'50001012': "A technical problem occurred, please contact helpdesk",
'50001013': "A technical problem occurred, please contact helpdesk",
'50001014': "A technical problem occurred, please contact helpdesk",
'50001015': "Invalid currency code",
'50001016': "A technical problem occurred, please contact helpdesk",
'50001017': "A technical problem occurred, please contact helpdesk",
'50001018': "A technical problem occurred, please contact helpdesk",
'50001019': "A technical problem occurred, please contact helpdesk",
'50001020': "A technical problem occurred, please contact helpdesk",
'50001021': "A technical problem occurred, please contact helpdesk",
'50001022': "A technical problem occurred, please contact helpdesk",
'50001023': "A technical problem occurred, please contact helpdesk",
'50001024': "A technical problem occurred, please contact helpdesk",
'50001025': "A technical problem occurred, please contact helpdesk",
'50001026': "A technical problem occurred, please contact helpdesk",
'50001027': "A technical problem occurred, please contact helpdesk",
'50001028': "A technical problem occurred, please contact helpdesk",
'50001029': "A technical problem occurred, please contact helpdesk",
'50001030': "A technical problem occurred, please contact helpdesk",
'50001031': "A technical problem occurred, please contact helpdesk",
'50001032': "A technical problem occurred, please contact helpdesk",
'50001033': "A technical problem occurred, please contact helpdesk",
'50001034': "A technical problem occurred, please contact helpdesk",
'50001035': "A technical problem occurred, please contact helpdesk",
'50001036': "Card length does not correspond to an acceptable value for the brand",
'50001037': "Purchasing card number for a regular merchant",
'50001038': "Non Purchasing card for a Purchasing card merchant",
'50001039': "Details sent for a non-Purchasing card merchant, please contact helpdesk",
'50001040': "Details not sent for a Purchasing card transaction, please contact helpdesk",
'50001041': "Payment detail validation failed",
'50001042': "Given transactions amounts (tax,discount,shipping,net,etc…) do not compute correctly together",
'50001043': "A technical problem occurred, please contact helpdesk",
'50001044': "No acquirer configured for this operation",
'50001045': "No UID configured for this operation",
'50001046': "Operation not allowed for the merchant",
'50001047': "A technical problem occurred, please contact helpdesk",
'50001048': "A technical problem occurred, please contact helpdesk",
'50001049': "A technical problem occurred, please contact helpdesk",
'50001050': "A technical problem occurred, please contact helpdesk",
'50001051': "A technical problem occurred, please contact helpdesk",
'50001052': "A technical problem occurred, please contact helpdesk",
'50001053': "A technical problem occurred, please contact helpdesk",
'50001054': "Card number incorrect or incompatible",
'50001055': "A technical problem occurred, please contact helpdesk",
'50001056': "A technical problem occurred, please contact helpdesk",
'50001057': "A technical problem occurred, please contact helpdesk",
'50001058': "A technical problem occurred, please contact helpdesk",
'50001059': "A technical problem occurred, please contact helpdesk",
'50001060': "A technical problem occurred, please contact helpdesk",
'50001061': "A technical problem occurred, please contact helpdesk",
'50001062': "A technical problem occurred, please contact helpdesk",
'50001063': "Card Issue Number does not correspond to range or not present",
'50001064': "Start Date not valid or not present",
'50001066': "Format of CVC code invalid",
'50001067': "The merchant is not enrolled for 3D-Secure",
'50001068': "The card number or account number (PAN) is invalid",
'50001069': "Invalid check for CardID and Brand",
'50001070': "The ECI value given is either not supported, or in conflict with other data in the transaction",
'50001071': "Incomplete TRN demat",
'50001072': "Incomplete PAY demat",
'50001073': "No demat APP",
'50001074': "Authorisation too old",
'50001075': "VERRes was an error message",
'50001076': "DCP amount greater than authorisation amount",
'50001077': "Details negative amount",
'50001078': "Details negative quantity",
'50001079': "Could not decode/decompress received PARes (3D-Secure)",
'50001080': "Received PARes was an erereor message from ACS (3D-Secure)",
'50001081': "Received PARes format was invalid according to the 3DS specifications (3D-Secure)",
'50001082': "PAReq/PARes reconciliation failure (3D-Secure)",
'50001084': "Maximum amount reached",
'50001087': "The transaction type requires authentication, please check with your bank.",
'50001090': "CVC missing at input, but CVC check asked",
'50001091': "ZIP missing at input, but ZIP check asked",
'50001092': "Address missing at input, but Address check asked",
'50001095': "Invalid date of birth",
'50001096': "Invalid commodity code",
'50001097': "The requested currency and brand are incompatible.",
'50001111': "Data validation error",
'50001113': "This order has already been processed",
'50001114': "Error pre-payment check page access",
'50001115': "Request not received in secure mode",
'50001116': "Unknown IP address origin",
'50001117': "NO IP address origin",
'50001118': "Pspid not found or not correct",
'50001119': "Password incorrect or disabled due to numbers of errors",
'50001120': "Invalid currency",
'50001121': "Invalid number of decimals for the currency",
'50001122': "Currency not accepted by the merchant",
'50001123': "Card type not active",
'50001124': "Number of lines don't match with number of payments",
'50001125': "Format validation error",
'50001126': "Overflow in data capture requests for the original order",
'50001127': "The original order is not in a correct status",
'50001128': "missing authorization code for unauthorized order",
'50001129': "Overflow in refunds requests",
'50001130': "Error access to original order",
'50001131': "Error access to original history item",
'50001132': "The Selected Catalog is empty",
'50001133': "Duplicate request",
'50001134': "Authentication failed, please retry or cancel.",
'50001135': "Authentication temporary unavailable, please retry or cancel.",
'50001136': "Technical problem with your browser, please retry or cancel",
'50001137': "Your bank access control server is temporary unavailable, please retry or cancel",
'50001150': "Fraud Detection, Technical error (IP not valid)",
'50001151': "Fraud detection : technical error (IPCTY unknown or error)",
'50001152': "Fraud detection : technical error (CCCTY unknown or error)",
'50001153': "Overflow in redo-authorisation requests",
'50001170': "Dynamic BIN check failed",
'50001171': "Dynamic country check failed",
'50001172': "Error in Amadeus signature",
'50001174': "Card Holder Name is too long",
'50001175': "Name contains invalid characters",
'50001176': "Card number is too long",
'50001177': "Card number contains non-numeric info",
'50001178': "Card Number Empty",
'50001179': "CVC too long",
'50001180': "CVC contains non-numeric info",
'50001181': "Expiration date contains non-numeric info",
'50001182': "Invalid expiration month",
'50001183': "Expiration date must be in the future",
'50001184': "SHA Mismatch",
'50001205': "Missing mandatory fields for billing address.",
'50001206': "Missing mandatory field date of birth.",
'50001207': "Missing required shopping basket details.",
'50001208': "Missing social security number",
'50001209': "Invalid country code",
'50001210': "Missing yearly salary",
'50001211': "Missing gender",
'50001212': "Missing email",
'50001213': "Missing IP address",
'50001214': "Missing part payment campaign ID",
'50001215': "Missing invoice number",
'50001216': "The alias must be different than the card number",
'60000001': "account number unknown",
'60000003': "not credited dd-mm-yy",
'60000005': "name/number do not correspond",
'60000007': "account number blocked",
'60000008': "specific direct debit block",
'60000009': "account number WKA",
'60000010': "administrative reason",
'60000011': "account number expired",
'60000012': "no direct debit authorisation given",
'60000013': "debit not approved",
'60000014': "double payment",
'60000018': "name/address/city not entered",
'60001001': "no original direct debit for revocation",
'60001002': "payer’s account number format error",
'60001004': "payer’s account at different bank",
'60001005': "payee’s account at different bank",
'60001006': "payee’s account number format error",
'60001007': "payer’s account number blocked",
'60001008': "payer’s account number expired",
'60001009': "payee’s account number expired",
'60001010': "direct debit not possible",
'60001011': "creditor payment not possible",
'60001012': "payer’s account number unknown WKA-number",
'60001013': "payee’s account number unknown WKA-number",
'60001014': "impermissible WKA transaction",
'60001015': "period for revocation expired",
'60001017': "reason for revocation not correct",
'60001018': "original run number not numeric",
'60001019': "payment ID incorrect",
'60001020': "amount not numeric",
'60001021': "amount zero not permitted",
'60001022': "negative amount not permitted",
'60001023': "payer and payee giro account number",
'60001025': "processing code (verwerkingscode) incorrect",
'60001028': "revocation not permitted",
'60001029': "guaranteed direct debit on giro account number",
'60001030': "NBC transaction type incorrect",
'60001031': "description too large",
'60001032': "book account number not issued",
'60001034': "book account number incorrect",
'60001035': "payer’s account number not numeric",
'60001036': "payer’s account number not eleven-proof",
'60001037': "payer’s account number not issued",
'60001039': "payer’s account number of DNB/BGC/BLA",
'60001040': "payee’s account number not numeric",
'60001041': "payee’s account number not eleven-proof",
'60001042': "payee’s account number not issued",
'60001044': "payee’s account number unknown",
'60001050': "payee’s name missing",
'60001051': "indicate payee’s bank account number instead of 3102",
'60001052': "no direct debit contract",
'60001053': "amount beyond bounds",
'60001054': "selective direct debit block",
'60001055': "original run number unknown",
'60001057': "payer’s name missing",
'60001058': "payee’s account number missing",
'60001059': "restore not permitted",
'60001060': "bank’s reference (navraaggegeven) missing",
'60001061': "BEC/GBK number incorrect",
'60001062': "BEC/GBK code incorrect",
'60001087': "book account number not numeric",
'60001090': "cancelled on request",
'60001091': "cancellation order executed",
'60001092': "cancelled instead of bended",
'60001093': "book account number is a shortened account number",
'60001094': "instructing party account number not identical with payer",
'60001095': "payee unknown GBK acceptor",
'60001097': "instructing party account number not identical with payee",
'60001099': "clearing not permitted",
'60001101': "payer’s account number not spaces",
'60001102': "PAN length not numeric",
'60001103': "PAN length outside limits",
'60001104': "track number not numeric",
'60001105': "track number not valid",
'60001106': "PAN sequence number not numeric",
'60001107': "domestic PAN not numeric",
'60001108': "domestic PAN not eleven-proof",
'60001109': "domestic PAN not issued",
'60001110': "foreign PAN not numeric",
'60001111': "card valid date not numeric",
'60001112': "book period number (boekperiodenr) not numeric",
'60001113': "transaction number not numeric",
'60001114': "transaction time not numeric",
'60001115': "transaction no valid time",
'60001116': "transaction date not numeric",
'60001117': "transaction no valid date",
'60001118': "STAN not numeric",
'60001119': "instructing party’s name missing",
'60001120': "foreign amount (bedrag-vv) not numeric",
'60001122': "rate (verrekenkoers) not numeric",
'60001125': "number of decimals (aantaldecimalen) incorrect",
'60001126': "tariff (tarifering) not B/O/S",
'60001127': "domestic costs (kostenbinnenland) not numeric",
'60001128': "domestic costs (kostenbinnenland) not higher than zero",
'60001129': "foreign costs (kostenbuitenland) not numeric",
'60001130': "foreign costs (kostenbuitenland) not higher than zero",
'60001131': "domestic costs (kostenbinnenland) not zero",
'60001132': "foreign costs (kostenbuitenland) not zero",
'60001134': "Euro record not fully filled in",
'60001135': "Client currency incorrect",
'60001136': "Amount NLG not numeric",
'60001137': "Amount NLG not higher than zero",
'60001138': "Amount NLG not equal to Amount",
'60001139': "Amount NLG incorrectly converted",
'60001140': "Amount EUR not numeric",
'60001141': "Amount EUR not greater than zero",
'60001142': "Amount EUR not equal to Amount",
'60001143': "Amount EUR incorrectly converted",
'60001144': "Client currency not NLG",
'60001145': "rate euro-vv (Koerseuro-vv) not numeric",
'60001146': "comma rate euro-vv (Kommakoerseuro-vv) incorrect",
'60001147': "acceptgiro distributor not valid",
'60001148': "Original run number and/or BRN are missing",
'60001149': "Amount/Account number/ BRN different",
'60001150': "Direct debit already revoked/restored",
'60001151': "Direct debit already reversed/revoked/restored",
'60001153': "Payer’s account number not known",
}
DATA_VALIDATION_ERROR = '50001111'
def retryable(error):
return error in [
'0020001001', '0020001002', '0020001003', '0020001004', '0020001005',
'0020001006', '0020001007', '0020001008', '0020001009', '0020001010',
'30001010', '30001011', '30001015',
'30001057', '30001058',
'30001998', '30001999',
#'30611001', # amount exceeds card limit
'30961001',
'40001001', '40001002', '40001003', '40001004', '40001005',
'40001006', '40001007', '40001008', '40001009', '40001010',
'40001012',
'40001018', '40001019', '40001020',
'40001134', '40001135', '40001136', '40001137',
#'50001174', # cardholder name too long
]
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="payment.payment_acquirer_ingenico" model="payment.acquirer">
<field name="name">Ingenico</field>
<field name="image_128" type="base64" file="payment_ingenico/static/src/img/ingenico_icon.png"/>
<field name="provider">ogone</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="ogone_form"/>
<field name="registration_view_template_id" ref="ogone_s2s_form"/>
</record>
</data>
</odoo>
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import payment
-597
View File
@@ -1,597 +0,0 @@
# coding: utf-8
import base64
import datetime
import logging
import time
from hashlib import sha1
from pprint import pformat
from unicodedata import normalize
import requests
from lxml import etree, objectify
from werkzeug import urls
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_ingenico.controllers.main import OgoneController
from odoo.addons.payment_ingenico.data import ogone
from odoo.http import request
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, ustr
from odoo.tools.float_utils import float_compare, float_repr, float_round
_logger = logging.getLogger(__name__)
class PaymentAcquirerOgone(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(selection_add=[
('ogone', 'Ingenico')
], ondelete={'ogone': 'set default'})
ogone_pspid = fields.Char('PSPID', required_if_provider='ogone', groups='base.group_user')
ogone_userid = fields.Char('API User ID', required_if_provider='ogone', groups='base.group_user')
ogone_password = fields.Char('API User Password', required_if_provider='ogone', groups='base.group_user')
ogone_shakey_in = fields.Char('SHA Key IN', size=32, required_if_provider='ogone', groups='base.group_user')
ogone_shakey_out = fields.Char('SHA Key OUT', size=32, required_if_provider='ogone', groups='base.group_user')
ogone_alias_usage = fields.Char('Alias Usage', default="Allow saving my payment data",
help="If you want to use Ogone Aliases, this default "
"Alias Usage will be presented to the customer as the "
"reason you want to keep his payment data")
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
res = super(PaymentAcquirerOgone, self)._get_feature_support()
res['tokenize'].append('ogone')
return res
def _get_ogone_urls(self, environment):
""" Ogone URLS:
- standard order: POST address for form-based """
return {
'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (environment,),
'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (environment,),
'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (environment,),
'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (environment,),
}
def _ogone_generate_shasign(self, inout, values):
""" Generate the shasign for incoming or outgoing communications.
:param string inout: 'in' (odoo contacting ogone) or 'out' (ogone
contacting odoo). In this last case only some
fields should be contained (see e-Commerce basic)
:param dict values: transaction values
:return string: shasign
"""
assert inout in ('in', 'out')
assert self.provider == 'ogone'
key = getattr(self, 'ogone_shakey_' + inout)
def filter_key(key):
if inout == 'in':
return True
else:
# SHA-OUT keys
# source https://payment-services.ingenico.com/int/en/ogone/support/guides/integration guides/e-commerce/transaction-feedback
keys = [
'AAVADDRESS',
'AAVCHECK',
'AAVMAIL',
'AAVNAME',
'AAVPHONE',
'AAVZIP',
'ACCEPTANCE',
'ALIAS',
'AMOUNT',
'BIC',
'BIN',
'BRAND',
'CARDNO',
'CCCTY',
'CN',
'COLLECTOR_BIC',
'COLLECTOR_IBAN',
'COMPLUS',
'CREATION_STATUS',
'CREDITDEBIT',
'CURRENCY',
'CVCCHECK',
'DCC_COMMPERCENTAGE',
'DCC_CONVAMOUNT',
'DCC_CONVCCY',
'DCC_EXCHRATE',
'DCC_EXCHRATESOURCE',
'DCC_EXCHRATETS',
'DCC_INDICATOR',
'DCC_MARGINPERCENTAGE',
'DCC_VALIDHOURS',
'DEVICEID',
'DIGESTCARDNO',
'ECI',
'ED',
'EMAIL',
'ENCCARDNO',
'FXAMOUNT',
'FXCURRENCY',
'IP',
'IPCTY',
'MANDATEID',
'MOBILEMODE',
'NBREMAILUSAGE',
'NBRIPUSAGE',
'NBRIPUSAGE_ALLTX',
'NBRUSAGE',
'NCERROR',
'ORDERID',
'PAYID',
'PAYIDSUB',
'PAYMENT_REFERENCE',
'PM',
'SCO_CATEGORY',
'SCORING',
'SEQUENCETYPE',
'SIGNDATE',
'STATUS',
'SUBBRAND',
'SUBSCRIPTION_ID',
'TICKET',
'TRXDATE',
'VC',
]
return key.upper() in keys
items = sorted((k.upper(), v) for k, v in values.items())
sign = ''.join('%s=%s%s' % (k, v, key) for k, v in items if v and filter_key(k))
sign = sign.encode("utf-8")
shasign = sha1(sign).hexdigest()
return shasign
def ogone_form_generate_values(self, values):
base_url = self.get_base_url()
ogone_tx_values = dict(values)
param_plus = {
'return_url': ogone_tx_values.pop('return_url', False)
}
temp_ogone_tx_values = {
'PSPID': self.ogone_pspid,
'ORDERID': values['reference'],
'AMOUNT': float_repr(float_round(values['amount'], 2) * 100, 0),
'CURRENCY': values['currency'] and values['currency'].name or '',
'LANGUAGE': values.get('partner_lang'),
'CN': values.get('partner_name'),
'EMAIL': values.get('partner_email'),
'OWNERZIP': values.get('partner_zip'),
'OWNERADDRESS': values.get('partner_address'),
'OWNERTOWN': values.get('partner_city'),
'OWNERCTY': values.get('partner_country') and values.get('partner_country').code or '',
'OWNERTELNO': values.get('partner_phone'),
'ACCEPTURL': urls.url_join(base_url, OgoneController._accept_url),
'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url),
'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url),
'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url),
'PARAMPLUS': urls.url_encode(param_plus),
}
if self.save_token in ['ask', 'always']:
temp_ogone_tx_values.update({
'ALIAS': 'ODOO-NEW-ALIAS-%s' % time.time(), # something unique,
'ALIASUSAGE': values.get('alias_usage') or self.ogone_alias_usage,
})
shasign = self._ogone_generate_shasign('in', temp_ogone_tx_values)
temp_ogone_tx_values['SHASIGN'] = shasign
ogone_tx_values.update(temp_ogone_tx_values)
return ogone_tx_values
def ogone_get_form_action_url(self):
self.ensure_one()
environment = 'prod' if self.state == 'enabled' else 'test'
return self._get_ogone_urls(environment)['ogone_standard_order_url']
def ogone_s2s_form_validate(self, data):
error = dict()
mandatory_fields = ["cc_number", "cc_cvc", "cc_holder_name", "cc_expiry", "cc_brand"]
# Validation
for field_name in mandatory_fields:
if not data.get(field_name):
error[field_name] = 'missing'
return False if error else True
def ogone_s2s_form_process(self, data):
values = {
'cc_number': data.get('cc_number'),
'cc_cvc': int(data.get('cc_cvc')),
'cc_holder_name': data.get('cc_holder_name'),
'cc_expiry': data.get('cc_expiry'),
'cc_brand': data.get('cc_brand'),
'acquirer_id': int(data.get('acquirer_id')),
'partner_id': int(data.get('partner_id'))
}
pm_id = self.env['payment.token'].sudo().create(values)
return pm_id
class PaymentTxOgone(models.Model):
_inherit = 'payment.transaction'
# ogone status
_ogone_valid_tx_status = [5, 9, 8]
_ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
_ogone_pending_tx_status = [46, 81, 82] # 46 = 3DS HTML response
_ogone_cancel_tx_status = [1]
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
@api.model
def _ogone_form_get_tx_from_data(self, data):
""" Given a data dict coming from ogone, verify it and find the related
transaction record. Create a payment token if an alias is returned."""
reference, pay_id, shasign, alias = data.get('orderID'), data.get('PAYID'), data.get('SHASIGN'), data.get('ALIAS')
if not reference or not pay_id or not shasign:
error_msg = _('Ogone: received data with missing reference (%s) or pay_id (%s) or shasign (%s)') % (reference, pay_id, shasign)
_logger.info(error_msg)
raise ValidationError(error_msg)
# find tx -> @TDENOTE use paytid ?
tx = self.search([('reference', '=', reference)])
if not tx or len(tx) > 1:
error_msg = _('Ogone: received data for reference %s') % (reference)
if not tx:
error_msg += _('; no order found')
else:
error_msg += _('; multiple order found')
_logger.info(error_msg)
raise ValidationError(error_msg)
# verify shasign
shasign_check = tx.acquirer_id._ogone_generate_shasign('out', data)
if shasign_check.upper() != shasign.upper():
error_msg = _('Ogone: invalid shasign, received %s, computed %s, for data %s') % (shasign, shasign_check, data)
_logger.info(error_msg)
raise ValidationError(error_msg)
if not tx.acquirer_reference:
tx.acquirer_reference = pay_id
# alias was created on ogone server, store it
if alias and tx.type == 'form_save':
Token = self.env['payment.token']
domain = [('acquirer_ref', '=', alias)]
cardholder = data.get('CN')
if not Token.search_count(domain):
_logger.info('Ogone: saving alias %s for partner %s' % (data.get('CARDNO'), tx.partner_id))
ref = Token.create({'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''),
'partner_id': tx.partner_id.id,
'acquirer_id': tx.acquirer_id.id,
'acquirer_ref': alias})
tx.write({'payment_token_id': ref.id})
return tx
def _ogone_form_get_invalid_parameters(self, data):
invalid_parameters = []
# TODO: txn_id: should be false at draft, set afterwards, and verified with txn details
if self.acquirer_reference and data.get('PAYID') != self.acquirer_reference:
invalid_parameters.append(('PAYID', data.get('PAYID'), self.acquirer_reference))
# check what is bought
if float_compare(float(data.get('amount', '0.0')), self.amount, 2) != 0:
invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount))
if data.get('currency') != self.currency_id.name:
invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name))
return invalid_parameters
def _ogone_form_validate(self, data):
if self.state not in ['draft', 'pending']:
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference)
return True
status = int(data.get('STATUS', '0'))
if status in self._ogone_valid_tx_status:
vals = {
'date': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': data['PAYID'],
}
if data.get('ALIAS') and self.partner_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always')\
and not self.payment_token_id:
pm = self.env['payment.token'].create({
'partner_id': self.partner_id.id,
'acquirer_id': self.acquirer_id.id,
'acquirer_ref': data.get('ALIAS'),
'name': '%s - %s' % (data.get('CARDNO'), data.get('CN'))
})
vals.update(payment_token_id=pm.id)
self.write(vals)
if self.payment_token_id:
self.payment_token_id.verified = True
self._set_transaction_done()
self.execute_callback()
# if this transaction is a validation one, then we refund the money we just withdrawn
if self.type == 'validation':
self.s2s_do_refund()
return True
elif status in self._ogone_cancel_tx_status:
self.write({'acquirer_reference': data.get('PAYID')})
self._set_transaction_cancel()
elif status in self._ogone_pending_tx_status or status in self._ogone_wait_tx_status:
self.write({'acquirer_reference': data.get('PAYID')})
self._set_transaction_pending()
else:
error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % {
'error_str': data.get('NCERRORPLUS'),
'error_code': data.get('NCERROR'),
'error_msg': ogone.OGONE_ERROR_MAP.get(data.get('NCERROR')),
}
_logger.info(error)
self.write({
'state_message': error,
'acquirer_reference': data.get('PAYID'),
})
self._set_transaction_cancel()
return False
# --------------------------------------------------
# S2S RELATED METHODS
# --------------------------------------------------
def ogone_s2s_do_transaction(self, **kwargs):
# TODO: create tx with s2s type
account = self.acquirer_id
reference = self.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), self.partner_id.id)
param_plus = {
'return_url': kwargs.get('return_url', False)
}
data = {
'PSPID': account.ogone_pspid,
'USERID': account.ogone_userid,
'PSWD': account.ogone_password,
'ORDERID': reference,
'AMOUNT': int(self.amount * 100),
'CURRENCY': self.currency_id.name,
'OPERATION': 'SAL',
'ECI': 9, # Recurring (from eCommerce)
'ALIAS': self.payment_token_id.acquirer_ref,
'RTIMEOUT': 30,
'PARAMPLUS': urls.url_encode(param_plus),
'EMAIL': self.partner_id.email or '',
'CN': self.partner_id.name or '',
}
if request:
data['REMOTE_ADDR'] = request.httprequest.remote_addr
if kwargs.get('3d_secure'):
data.update({
'FLAG3D': 'Y',
'LANGUAGE': self.partner_id.lang or 'en_US',
})
for url in 'accept decline exception'.split():
key = '{0}_url'.format(url)
val = kwargs.pop(key, None)
if val:
key = '{0}URL'.format(url).upper()
data[key] = val
data['SHASIGN'] = self.acquirer_id._ogone_generate_shasign('in', data)
direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % ('prod' if self.acquirer_id.state == 'enabled' else 'test')
logged_data = data.copy()
logged_data.pop('PSWD')
_logger.info("ogone_s2s_do_transaction: Sending values to URL %s, values:\n%s", direct_order_url, pformat(logged_data))
result = requests.post(direct_order_url, data=data).content
try:
tree = objectify.fromstring(result)
_logger.info('ogone_s2s_do_transaction: Values received:\n%s', etree.tostring(tree, pretty_print=True, encoding='utf-8'))
except etree.XMLSyntaxError:
# invalid response from ogone
_logger.exception('Invalid xml response from ogone')
_logger.info('ogone_s2s_do_transaction: Values received:\n%s', result)
raise
return self._ogone_s2s_validate_tree(tree)
def ogone_s2s_do_refund(self, **kwargs):
account = self.acquirer_id
reference = self.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), self.partner_id.id)
data = {
'PSPID': account.ogone_pspid,
'USERID': account.ogone_userid,
'PSWD': account.ogone_password,
'ORDERID': reference,
'AMOUNT': int(self.amount * 100),
'CURRENCY': self.currency_id.name,
'OPERATION': 'RFS',
'PAYID': self.acquirer_reference,
}
data['SHASIGN'] = self.acquirer_id._ogone_generate_shasign('in', data)
direct_order_url = 'https://secure.ogone.com/ncol/%s/maintenancedirect.asp' % ('prod' if self.acquirer_id.state == 'enabled' else 'test')
logged_data = data.copy()
logged_data.pop('PSWD')
_logger.info("ogone_s2s_do_refund: Sending values to URL %s, values:\n%s", direct_order_url, pformat(logged_data))
result = requests.post(direct_order_url, data=data).content
try:
tree = objectify.fromstring(result)
_logger.info('ogone_s2s_do_refund: Values received:\n%s', etree.tostring(tree, pretty_print=True, encoding='utf-8'))
except etree.XMLSyntaxError:
# invalid response from ogone
_logger.exception('Invalid xml response from ogone')
_logger.info('ogone_s2s_do_refund: Values received:\n%s', result)
raise
return self._ogone_s2s_validate_tree(tree)
def _ogone_s2s_validate(self):
tree = self._ogone_s2s_get_tx_status()
return self._ogone_s2s_validate_tree(tree)
def _ogone_s2s_validate_tree(self, tree, tries=2):
if self.state not in ['draft', 'pending']:
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference)
return True
status = int(tree.get('STATUS') or 0)
if status in self._ogone_valid_tx_status:
self.write({
'date': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': tree.get('PAYID'),
})
if tree.get('ALIAS') and self.partner_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always')\
and not self.payment_token_id:
pm = self.env['payment.token'].create({
'partner_id': self.partner_id.id,
'acquirer_id': self.acquirer_id.id,
'acquirer_ref': tree.get('ALIAS'),
'name': tree.get('CARDNO'),
})
self.write({'payment_token_id': pm.id})
if self.payment_token_id:
self.payment_token_id.verified = True
self._set_transaction_done()
self.execute_callback()
# if this transaction is a validation one, then we refund the money we just withdrawn
if self.type == 'validation':
self.s2s_do_refund()
return True
elif status in self._ogone_cancel_tx_status:
self.write({'acquirer_reference': tree.get('PAYID')})
self._set_transaction_cancel()
elif status in self._ogone_pending_tx_status:
vals = {
'acquirer_reference': tree.get('PAYID'),
}
if status == 46: # HTML 3DS
vals['html_3ds'] = ustr(base64.b64decode(tree.HTML_ANSWER.text))
self.write(vals)
self._set_transaction_pending()
elif status in self._ogone_wait_tx_status and tries > 0:
time.sleep(0.5)
self.write({'acquirer_reference': tree.get('PAYID')})
tree = self._ogone_s2s_get_tx_status()
return self._ogone_s2s_validate_tree(tree, tries - 1)
else:
error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % {
'error_str': tree.get('NCERRORPLUS'),
'error_code': tree.get('NCERROR'),
'error_msg': ogone.OGONE_ERROR_MAP.get(tree.get('NCERROR')),
}
_logger.info(error)
self.write({
'state_message': error,
'acquirer_reference': tree.get('PAYID'),
})
self._set_transaction_cancel()
return False
def _ogone_s2s_get_tx_status(self):
account = self.acquirer_id
#reference = tx.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%Y%m%d_%H%M%S'), tx.partner_id.id)
data = {
'PAYID': self.acquirer_reference,
'PSPID': account.ogone_pspid,
'USERID': account.ogone_userid,
'PSWD': account.ogone_password,
}
query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % ('prod' if self.acquirer_id.state == 'enabled' else 'test')
logged_data = data.copy()
logged_data.pop('PSWD')
_logger.info("_ogone_s2s_get_tx_status: Sending values to URL %s, values:\n%s", query_direct_url, pformat(logged_data))
result = requests.post(query_direct_url, data=data).content
try:
tree = objectify.fromstring(result)
_logger.info('_ogone_s2s_get_tx_status: Values received:\n%s', etree.tostring(tree, pretty_print=True, encoding='utf-8'))
except etree.XMLSyntaxError:
# invalid response from ogone
_logger.exception('Invalid xml response from ogone')
_logger.info('_ogone_s2s_get_tx_status: Values received:\n%s', result)
raise
return tree
class PaymentToken(models.Model):
_inherit = 'payment.token'
def ogone_create(self, values):
if values.get('cc_number'):
# create a alias via batch
values['cc_number'] = values['cc_number'].replace(' ', '')
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
alias = 'ODOO-NEW-ALIAS-%s' % time.time()
expiry = str(values['cc_expiry'][:2]) + str(values['cc_expiry'][-2:])
line = 'ADDALIAS;%(alias)s;%(cc_holder_name)s;%(cc_number)s;%(expiry)s;%(cc_brand)s;%(pspid)s'
line = line % dict(values, alias=alias, expiry=expiry, pspid=acquirer.ogone_pspid)
data = {
'FILE_REFERENCE': alias,
'TRANSACTION_CODE': 'MTR',
'OPERATION': 'SAL',
'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0
'FILE': normalize('NFKD', line).encode('ascii','ignore'), # Ogone Batch must be ASCII only
'REPLY_TYPE': 'XML',
'PSPID': acquirer.ogone_pspid,
'USERID': acquirer.ogone_userid,
'PSWD': acquirer.ogone_password,
'PROCESS_MODE': 'CHECKANDPROCESS',
}
url = 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % ('prod' if acquirer.state == 'enabled' else 'test')
_logger.info("ogone_create: Creating new alias %s via url %s", alias, url)
result = requests.post(url, data=data).content
try:
tree = objectify.fromstring(result)
except etree.XMLSyntaxError:
_logger.exception('Invalid xml response from ogone')
return None
error_code = error_str = None
if hasattr(tree, 'PARAMS_ERROR'):
error_code = tree.NCERROR.text
error_str = 'PARAMS ERROR: %s' % (tree.PARAMS_ERROR.text or '',)
else:
node = tree.FORMAT_CHECK
error_node = getattr(node, 'FORMAT_CHECK_ERROR', None)
if error_node is not None:
error_code = error_node.NCERROR.text
error_str = 'CHECK ERROR: %s' % (error_node.ERROR.text or '',)
if error_code:
error_msg = tree.get(error_code)
error = '%s\n\n%s: %s' % (error_str, error_code, error_msg)
_logger.error(error)
raise Exception(error)
return {
'acquirer_ref': alias,
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
}
return {}
@@ -1 +0,0 @@
# -*- coding: utf-8 -*-
-195
View File
@@ -1,195 +0,0 @@
# -*- coding: utf-8 -*-
from lxml import objectify
import time
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.addons.payment_ingenico.controllers.main import OgoneController
from werkzeug import urls
from odoo.tools import mute_logger
from odoo.tests import tagged
@tagged('post_install', '-at_install', 'external', '-standard')
class OgonePayment(PaymentAcquirerCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.ogone = cls.env.ref('payment.payment_acquirer_ogone')
cls.ogone.write({
'ogone_pspid': 'dummy',
'ogone_userid': 'dummy',
'ogone_password': 'dummy',
'ogone_shakey_in': 'dummy',
'ogone_shakey_out': 'dummy',
'state': 'test',
})
def test_10_ogone_form_render(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
# be sure not to do stupid thing
self.assertEqual(self.ogone.state, 'test', 'test without test environment')
# ----------------------------------------
# Test: button direct rendering + shasign
# ----------------------------------------
form_values = {
'PSPID': 'dummy',
'ORDERID': 'test_ref0',
'AMOUNT': '1',
'CURRENCY': 'EUR',
'LANGUAGE': 'en_US',
'CN': 'Norbert Buyer',
'EMAIL': 'norbert.buyer@example.com',
'OWNERZIP': '1000',
'OWNERADDRESS': 'Huge Street 2/543',
'OWNERCTY': 'Belgium',
'OWNERTOWN': 'Sin City',
'OWNERTELNO': '0032 12 34 56 78',
'SHASIGN': '815f67b8ff70d234ffcf437c13a9fa7f807044cc',
'ACCEPTURL': urls.url_join(base_url, OgoneController._accept_url),
'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url),
'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url),
'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url),
}
# render the button
res = self.ogone.render(
'test_ref0', 0.01, self.currency_euro.id,
partner_id=None,
partner_values=self.buyer_values)
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://secure.ogone.com/ncol/test/orderstandard.asp', 'ogone: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit']:
continue
self.assertEqual(
form_input.get('value'),
form_values[form_input.get('name')],
'ogone: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
)
# ----------------------------------------
# Test2: button using tx + validation
# ----------------------------------------
# create a new draft tx
tx = self.env['payment.transaction'].create({
'amount': 0.01,
'acquirer_id': self.ogone.id,
'currency_id': self.currency_euro.id,
'reference': 'test_ref0',
'partner_id': self.buyer_id})
# render the button
res = self.ogone.render(
'should_be_erased', 0.01, self.currency_euro,
tx_id=tx.id,
partner_id=None,
partner_values=self.buyer_values)
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://secure.ogone.com/ncol/test/orderstandard.asp', 'ogone: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit']:
continue
self.assertEqual(
form_input.get('value'),
form_values[form_input.get('name')],
'ingenico: wrong value for form input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
)
@mute_logger('odoo.addons.payment_ingenico.models.payment', 'ValidationError')
def test_20_ogone_form_management(self):
# be sure not to do stupid thing
self.assertEqual(self.ogone.state, 'test', 'test without test environment')
# typical data posted by ogone after client has successfully paid
ogone_post_data = {
'orderID': u'test_ref_2',
'STATUS': u'9',
'CARDNO': u'XXXXXXXXXXXX0002',
'PAYID': u'25381582',
'CN': u'Norbert Buyer',
'NCERROR': u'0',
'TRXDATE': u'11/15/13',
'IP': u'85.201.233.72',
'BRAND': u'VISA',
'ACCEPTANCE': u'test123',
'currency': u'EUR',
'amount': u'1.95',
'SHASIGN': u'7B7B0ED9CBC4A85543A9073374589033A62A05A5',
'ED': u'0315',
'PM': u'CreditCard'
}
# should raise error about unknown tx
with self.assertRaises(ValidationError):
self.env['payment.transaction'].form_feedback(ogone_post_data)
# create tx
tx = self.env['payment.transaction'].create({
'amount': 1.95,
'acquirer_id': self.ogone.id,
'currency_id': self.currency_euro.id,
'reference': 'test_ref_2-1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# validate it
tx.form_feedback(ogone_post_data)
# check state
self.assertEqual(tx.state, 'done', 'ogone: validation did not put tx into done state')
self.assertEqual(tx.ogone_payid, ogone_post_data.get('PAYID'), 'ogone: validation did not update tx payid')
# reset tx
tx = self.env['payment.transaction'].create({
'amount': 1.95,
'acquirer_id': self.ogone.id,
'currency_id': self.currency_euro.id,
'reference': 'test_ref_2-2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# now ogone post is ok: try to modify the SHASIGN
ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691'
with self.assertRaises(ValidationError):
tx.form_feedback(ogone_post_data)
# simulate an error
ogone_post_data['STATUS'] = 2
ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691'
tx.form_feedback(ogone_post_data)
# check state
self.assertEqual(tx.state, 'cancel', 'ogone: erroneous validation did not put tx into error state')
def test_30_ogone_s2s(self):
test_ref = 'test_ref_%.15f' % time.time()
# be sure not to do stupid thing
self.assertEqual(self.ogone.state, 'test', 'test without test environment')
# create a new draft tx
tx = self.env['payment.transaction'].create({
'amount': 0.01,
'acquirer_id': self.ogone.id,
'currency_id': self.currency_euro.id,
'reference': test_ref,
'partner_id': self.buyer_id,
'type': 'server2server',
})
# create an alias
res = tx.ogone_s2s_create_alias({
'expiry_date_mm': '01',
'expiry_date_yy': '2015',
'holder_name': 'Norbert Poilu',
'number': '4000000000000002',
'brand': 'VISA'})
res = tx.ogone_s2s_execute({})
@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="ogone_form">
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
<!-- seller -->
<input type="hidden" name='PSPID' t-att-value='PSPID'/>
<input type="hidden" name='ORDERID' t-att-value='ORDERID'/>
<!-- cart -->
<input type="hidden" name='AMOUNT' t-att-value='AMOUNT or "0.0"'/>
<input type="hidden" name='CURRENCY' t-att-value='CURRENCY'/>
<!-- buyer -->
<input type="hidden" name='LANGUAGE' t-att-value='LANGUAGE'/>
<input type="hidden" name='CN' t-att-value='CN'/>
<input type="hidden" name='EMAIL' t-att-value='EMAIL'/>
<input type="hidden" name='OWNERZIP' t-att-value='OWNERZIP'/>
<input type="hidden" name='OWNERADDRESS' t-att-value='OWNERADDRESS'/>
<input type="hidden" name='OWNERCTY' t-att-value='OWNERCTY'/>
<input type="hidden" name='OWNERTOWN' t-att-value='OWNERTOWN'/>
<input type="hidden" name='OWNERTELNO' t-att-value='OWNERTELNO'/>
<t t-if="acquirer.save_token in ['ask', 'always']">
<input type="hidden" name='ALIAS' t-att-value='ALIAS'/>
<input type="hidden" name='ALIASUSAGE' t-att-value='ALIASUSAGE'/>
</t>
<!-- before payment verification -->
<input type="hidden" name='SHASIGN' t-att-value='SHASIGN'/>
<!-- after payment parameters -->
<t t-if='PARAMPLUS'>
<input type="hidden" name="PARAMPLUS" t-att-value='PARAMPLUS'/>
</t>
<!-- redirection -->
<input type="hidden" name='ACCEPTURL' t-att-value='ACCEPTURL'/>
<input type="hidden" name='DECLINEURL' t-att-value='DECLINEURL'/>
<input type="hidden" name='EXCEPTIONURL' t-att-value='EXCEPTIONURL'/>
<input type="hidden" name='CANCELURL' t-att-value='CANCELURL'/>
</template>
<template id="ogone_s2s_form">
<input type="hidden" name="data_set" data-create-route="/payment/ogone/s2s/create_json_3ds"/>
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
<div t-attf-class="row mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-lg-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
<div class="visa"></div>
<input type="hidden" name="cc_brand" value=""/>
</div>
<div t-att-class="'form-group col-lg-5' if bootstrap_formatting else 'form-group'">
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name" data-is-required="true"/>
</div>
<div t-att-class="'form-group col-lg-3' if bootstrap_formatting else 'form-group'">
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)" data-is-required="true"/>
</div>
<div t-att-class="'form-group col-lg-4' if bootstrap_formatting else 'form-group'">
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC" data-is-required="true"/>
</div>
</div>
</template>
</data>
</odoo>
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="acquirer_form_ogone" model="ir.ui.view">
<field name="name">acquirer.form.ogone</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'ogone')]}">
<field name="ogone_pspid" attrs="{'required':[ ('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_userid" attrs="{'required':[ ('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_password" attrs="{'required':[ ('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_shakey_in" attrs="{'required':[ ('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_shakey_out" attrs="{'required':[ ('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_alias_usage"/>
</group>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
from odoo.addons.payment import reset_payment_provider
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment.models.payment_acquirer import create_missing_journals # post-init hook
def uninstall_hook(cr, registry):
reset_payment_provider(cr, registry, 'ogone')
reset_payment_acquirer(cr, registry, 'ogone')
+19
View File
@@ -0,0 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Ogone Payment Acquirer',
'version': '2.0',
'category': 'Accounting/Payment Acquirers',
'sequence': 360,
'summary': 'Payment Acquirer: Ogone Implementation',
'description': """Ogone Payment Acquirer""",
'depends': ['payment'],
'data': [
'views/payment_views.xml',
'views/payment_ogone_templates.xml',
'data/payment_acquirer_data.xml',
],
'application': True,
'post_init_hook': 'create_missing_journals',
'uninstall_hook': 'uninstall_hook',
}
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
+129
View File
@@ -0,0 +1,129 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import logging
import pprint
import re
import werkzeug
from odoo import _, http
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.tools import ustr
_logger = logging.getLogger(__name__)
class OgoneController(http.Controller):
_flexcheckout_return_url = '/payment/ogone/flexchekout'
_directlink_return_url = '/payment/ogone/directlink'
@http.route(
_flexcheckout_return_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False
) # 'GET' or 'POST' depending on the configuration in Ogone backend
def ogone_return_from_flexcheckout(self, **feedback_data):
""" Process the data returned by Ogone after redirection to Flexcheckout.
:param dict feedback_data: The feedback data
"""
# Check the source and integrity of the data
data = self._homogenize_data(feedback_data)
self._verify_signature(feedback_data, data)
# Create a token from the feedback data
data['FEEDBACK_TYPE'] = 'flexcheckout'
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data))
tx_sudo = request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
# Process the payment through the token
tree = tx_sudo._ogone_send_order_request(request_3ds_authentication=True)
# Handle the response
redirect_html_element = tree.find('HTML_ANSWER')
if redirect_html_element:
# Ogone has inserted an HTML_ANSWER element in its response XML tree. This means that a
# redirection to DirectLink's authentication page is required, as FlexCheckout is not
# capable of handling authentications...
# As per the documentation, the HTML must be inserted as-is into the current page, and
# consists of a <form> bundled with a script to auto-submit the form once its inserted.
# The content of the HTML is not sanitized to preserve the redirection in the script.
# After redirection, the customer comes back to the directlink return URL and we proceed
# with the (now authenticated) payment request.
redirect_html = ustr(base64.b64decode(redirect_html_element.text))
return request.render(
'payment_ogone.directlink_feedback', {'redirect_html': redirect_html}
)
else:
feedback_data = {
'FEEDBACK_TYPE': 'directlink',
'ORDERID': tree.get('orderID'),
'tree': tree,
}
_logger.info(
"entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data)
)
request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', feedback_data)
if tx_sudo.state in ('cancel', 'error'):
tx_sudo.token_id.active = False # The initial payment failed, archive the token
return werkzeug.utils.redirect('/payment/status')
@http.route(
_directlink_return_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False
) # 'GET' or 'POST' depending on the configuration in Ogone backend
def ogone_return_from_directlink(self, **feedback_data):
""" Process the data returned by Ogone after redirection to Directlink authentication page.
A redirection to Directlink can happen if a 3DS1 authentication is requested when sending
the request for a new order. This should normally only happen for the first payment of a
token as this is the only case where we specifically request the authentication if necessary
and handle the redirection request if one is returned.
:param dict feedback_data: The feedback data
"""
# Check the source and integrity of the data
data = self._homogenize_data(feedback_data)
self._verify_signature(feedback_data, data)
# Handle the feedback data
data['FEEDBACK_TYPE'] = 'directlink'
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data))
tx_sudo = request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
if tx_sudo.state in ('cancel', 'error'):
tx_sudo.token_id.active = False # The initial payment failed, archive the token
return werkzeug.utils.redirect('/payment/status')
def _homogenize_data(self, data):
""" Format keys to follow an homogenized convention inspired by Ogone Directlink API.
The keys received from Ogone APIs have inconsistent formatting and must be homogenized to
allow re-using the same methods. We reformat them to follow a unified nomenclature inspired
by DirectLink's order direct endpoint.
Formatting steps:
1) Uppercase key strings: 'Something' -> 'SOMETHING', 'something' -> 'SOMETHING'
2) Remove the prefix: 'CARD.SOMETHING' -> 'SOMETHING', 'ALIAS.SOMETHING' -> 'SOMETHING'
"""
return {re.sub(r'.*\.', '', k.upper()): v for k, v in data.items()}
def _verify_signature(self, sign_data, data):
""" Check that the signature computed from the feedback matches the received one.
:param dict sign_data: The original feedback data used to compute the signature
:param dict sign_data: The formatted feedback data used to find the tx and received sig
:return: None
:raise: ValidationError if the signatures don't match
"""
acquirer_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
'ogone', data
).acquirer_id # Find the acquirer based on the transaction
received_signature = data.get('SHASIGN')
expected_signature = acquirer_sudo._ogone_generate_signature(sign_data)
if received_signature != expected_signature.upper():
raise ValidationError(
"Ogone: " + _(
"Received data with invalid signature. expected: %(exp)s ; received: %(rec)s ; "
"data:\n%(data)s",
exp=expected_signature, rec=received_signature, data=pprint.pformat(sign_data)
)
)
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="payment.payment_acquirer_ogone" model="payment.acquirer">
<field name="provider">ogone</field>
<field name="redirect_form_view_id" ref="redirect_form"/>
<field name="support_authorization">False</field>
<field name="support_fees_computation">False</field>
<field name="support_tokenization">True</field>
<field name="allow_tokenization">True</field>
</record>
</odoo>
+5
View File
@@ -0,0 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer
from . import payment_token
from . import payment_transaction
+97
View File
@@ -0,0 +1,97 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# See https://epayments-support.ingenico.com/en/integration/all-sales-channels/flexcheckout/guide#flexcheckout_integration_guides_sha_out
FLEXCHECKOUT_KEYS = [
'ALIAS.ALIASID',
'ALIAS.NCERROR',
'ALIAS.NCERRORCARDNO',
'ALIAS.NCERRORCN',
'ALIAS.NCERRORCVC',
'ALIAS.NCERRORED',
'ALIAS.ORDERID',
'ALIAS.STATUS',
'ALIAS.STOREPERMANENTLY',
'CARD.BIC',
'CARD.BIN',
'CARD.BRAND',
'CARD.CARDHOLDERNAME',
'CARD.CARDNUMBER',
'CARD.CVC',
'CARD.EXPIRYDATE'
]
# See https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_request_a_new_order
# See https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_order_response
DIRECTLINK_KEYS = [
'AAVADDRESS',
'AAVCHECK',
'AAVMAIL',
'AAVNAME',
'AAVPHONE',
'AAVZIP',
'ACCEPTANCE',
'ALIAS',
'AMOUNT',
'BIC',
'BIN',
'BRAND',
'CARDNO',
'CCCTY',
'CN',
'COLLECTOR_BIC',
'COLLECTOR_IBAN',
'COMPLUS',
'CREATION_STATUS',
'CREDITDEBIT',
'CURRENCY',
'CVCCHECK',
'DCC_COMMPERCENTAGE',
'DCC_CONVAMOUNT',
'DCC_CONVCCY',
'DCC_EXCHRATE',
'DCC_EXCHRATESOURCE',
'DCC_EXCHRATETS',
'DCC_INDICATOR',
'DCC_MARGINPERCENTAGE',
'DCC_VALIDHOURS',
'DEVICEID',
'DIGESTCARDNO',
'ECI',
'ED',
'EMAIL',
'ENCCARDNO',
'FXAMOUNT',
'FXCURRENCY',
'IP',
'IPCTY',
'MANDATEID',
'MOBILEMODE',
'NBREMAILUSAGE',
'NBRIPUSAGE',
'NBRIPUSAGE_ALLTX',
'NBRUSAGE',
'NCERROR',
'ORDERID',
'PAYID',
'PAYIDSUB',
'PAYMENT_REFERENCE',
'PM',
'SCO_CATEGORY',
'SCORING',
'SEQUENCETYPE',
'SIGNDATE',
'STATUS',
'SUBBRAND',
'SUBSCRIPTION_ID',
'TICKET',
'TRXDATE',
'VC',
]
VALID_KEYS = DIRECTLINK_KEYS + FLEXCHECKOUT_KEYS
# See https://epayments-support.ingenico.com/en/get-started/transaction-status-full/
PAYMENT_STATUS_MAPPING = {
'pending': (41, 46, 50, 51, 52, 55, 56, 81, 82, 91, 92, 99), # 46 = 3DS
'done': (5, 8, 9),
'cancel': (1,),
}
@@ -0,0 +1,122 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from hashlib import sha256
import requests
from odoo import _, fields, models
from odoo.exceptions import ValidationError
from .const import VALID_KEYS
_logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(
selection_add=[('ogone', "Ogone")], ondelete={'ogone': 'set default'})
ogone_pspid = fields.Char(
string="PSPID", help="The ID solely used to identify the account with Ogone",
required_if_provider='ogone')
ogone_userid = fields.Char(
string="API User ID", help="The ID solely used to identify the API user with Ogone",
required_if_provider='ogone')
ogone_password = fields.Char(
string="API User Password", required_if_provider='ogone', groups='base.group_system')
ogone_shakey_in = fields.Char(
string="SHA Key IN", size=32, required_if_provider='ogone', groups='base.group_system')
ogone_shakey_out = fields.Char(
string="SHA Key OUT", size=32, required_if_provider='ogone', groups='base.group_system')
def _get_validation_amount(self):
""" Override of payment to return the amount for Ogone validation operations.
:return: The validation amount
:rtype: float
"""
res = super()._get_validation_amount()
if self.provider != 'ogone':
return res
return 1.0
def _ogone_get_api_url(self, api_key):
""" Return the appropriate URL of the requested API for the acquirer state.
Note: self.ensure_one()
:param str api_key: The API whose URL to get: 'flexcheckout' or 'directlink'
:return: The API URL
:rtype: str
"""
self.ensure_one()
if self.state == 'enabled':
api_urls = {
'flexcheckout': 'https://secure.ogone.com/Tokenization/HostedPage',
'directlink': 'https://secure.ogone.com/ncol/prod/orderdirect.asp',
'maintenancedirect': 'https://secure.ogone.com/ncol/prod/maintenancedirect.asp',
}
else: # 'test'
api_urls = {
'flexcheckout': 'https://ogone.test.v-psp.com/Tokenization/HostedPage',
'directlink': 'https://ogone.test.v-psp.com/ncol/test/orderdirect.asp',
'maintenancedirect': 'https://ogone.test.v-psp.com/ncol/test/maintenancedirect.asp',
}
return api_urls.get(api_key)
def _ogone_generate_signature(self, values, incoming=True, format_keys=False):
""" Generate the signature for incoming or outgoing communications.
:param dict values: The values used to generate the signature
:param bool incoming: Whether the signature must be generated for an incoming (Ogone to
Odoo) or outgoing (Odoo to Ogone) communication.
:param bool format_keys: Whether the keys must be formatted as uppercase, dot-separated
strings to comply with Ogone APIs. This must be used when the keys
are formatted as underscore-separated strings to be compliant with
QWeb's `t-att-value`.
:return: The signature
:rtype: str
"""
def _filter_key(_key):
return not incoming or _key in VALID_KEYS
key = self.ogone_shakey_in if incoming else self.ogone_shakey_out
if format_keys:
formatted_items = [(k.upper().replace('_', '.'), v) for k, v in values.items()]
else:
formatted_items = [(k.upper(), v) for k, v in values.items()]
sorted_items = sorted(formatted_items)
signing_string = ''.join(f'{k}={v}{key}' for k, v in sorted_items if _filter_key(k) and v)
shasign = sha256(signing_string.encode("utf-8")).hexdigest()
return shasign
def _ogone_make_request(self, api_key, payload=None, method='POST'):
""" Make a request to one of Ogone APIs.
Note: self.ensure_one()
:param str api_key: The API to which the request is made: 'flexcheckout' or 'directlink'
:param dict payload: The payload of the request
:param str method: The HTTP method of the request
:return The content of the response
:rtype: bytes
:raise: ValidationError if an HTTP error occurs
"""
self.ensure_one()
url = self._ogone_get_api_url(api_key)
try:
response = requests.request(method, url, data=payload, timeout=60)
response.raise_for_status()
except requests.exceptions.ConnectionError:
_logger.exception("unable to reach endpoint at %s", url)
raise ValidationError("Ogone: " + _("Could not establish the connection to the API."))
except requests.exceptions.HTTPError:
_logger.exception("invalid API request at %s with data %s", url, payload)
raise ValidationError("Ogone: " + _("The communication with the API failed."))
return response.content
@@ -0,0 +1,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, models
from odoo.exceptions import UserError
class PaymentToken(models.Model):
_inherit = 'payment.token'
def _handle_reactivation_request(self):
""" Override of payment to raise an error informing that Ogone tokens cannot be restored.
More specifically, permanents tokens are never deleted in Ogone's backend but we don't
distinguish them from temporary tokens which are archived at creation time. So we simply
block the reactivation of every token.
Note: self.ensure_one()
:return: None
:raise: UserError if the token is managed by Ogone
"""
super()._handle_reactivation_request()
if self.provider != 'ogone':
return
raise UserError(_("Saved payment methods cannot be restored once they have been archived."))
@@ -0,0 +1,284 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from lxml import etree, objectify
from werkzeug import urls
from odoo import _, api, models
from odoo.exceptions import UserError, ValidationError
from . import const
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_ogone.controllers.main import OgoneController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
@api.model
def _compute_reference(self, provider, prefix=None, **kwargs):
""" Override of payment to ensure that Ogone requirements for references are satisfied.
Ogone requirements for references are as follows:
- References must be unique at provider level for a given merchant account.
This is satisfied by singularizing the prefix with the current datetime. If two
transactions are created simultaneously, `_compute_reference` ensures the uniqueness of
references by suffixing a sequence number.
:param str provider: The provider of the acquirer handling the transaction
:param str prefix: The custom prefix used to compute the full reference
:return: The unique reference for the transaction
:rtype: str
"""
if provider != 'ogone':
return super()._compute_reference(provider, prefix=prefix, **kwargs)
prefix = payment_utils.singularize_reference_prefix(prefix=prefix, max_length=40)
return super()._compute_reference(provider, prefix=prefix, **kwargs)
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Ogone-specific rendering values.
Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic and specific processing values of the transaction
:return: The dict of acquirer-specific processing values
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.acquirer_id.provider != 'ogone':
return res
base_url = self.acquirer_id._get_base_url()
return_url = urls.url_join(base_url, OgoneController._flexcheckout_return_url)
rendering_values = {
'ACCOUNT_PSPID': self.acquirer_id.ogone_pspid,
'ALIAS_ALIASID': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'),
'ALIAS_ORDERID': self.reference,
'ALIAS_STOREPERMANENTLY': 'Y' if self.tokenize else 'N',
'CARD_PAYMENTMETHOD': 'CreditCard',
'LAYOUT_LANGUAGE': self.partner_lang,
'PARAMETERS_ACCEPTURL': return_url,
'PARAMETERS_EXCEPTIONURL': return_url,
}
rendering_values.update({
'SHASIGNATURE_SHASIGN': self.acquirer_id._ogone_generate_signature(
rendering_values, incoming=False, format_keys=True
).upper(),
'api_url': self.acquirer_id._ogone_get_api_url('flexcheckout'),
})
return rendering_values
def _send_payment_request(self):
""" Override of payment to send a payment request to Ogone.
Note: self.ensure_one()
:return: None
:raise: UserError if the transaction is not linked to a token
"""
super()._send_payment_request()
if self.provider != 'ogone':
return
if not self.token_id:
raise UserError("Ogone: " + _("The transaction is not linked to a token."))
tree = self._ogone_send_order_request()
feedback_data = {
'FEEDBACK_TYPE': 'directlink',
'ORDERID': tree.get('orderID'),
'tree': tree,
}
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data))
self._handle_feedback_data('ogone', feedback_data)
def _ogone_send_order_request(self, request_3ds_authentication=False):
""" Make a new order request to Ogone and return the lxml etree parsed from the response.
:param bool request_3ds_authentication: Whether a 3DS authentication should be requested if
necessary to process the payment
:return: The lxml etree
:raise: ValidationError if the response can not be parsed to an lxml etree
"""
base_url = self.acquirer_id.get_base_url()
return_url = urls.url_join(base_url, OgoneController._directlink_return_url)
data = {
# DirectLink parameters
'PSPID': self.acquirer_id.ogone_pspid,
'ORDERID': self.reference,
'USERID': self.acquirer_id.ogone_userid,
'PSWD': self.acquirer_id.ogone_password,
'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
'CURRENCY': self.currency_id.name,
'CN': self.partner_name or '', # Cardholder Name
'EMAIL': self.partner_email or '',
'OWNERADDRESS': self.partner_address or '',
'OWNERZIP': self.partner_zip or '',
'OWNERTOWN': self.partner_city or '',
'OWNERCTY': self.partner_country_id.code or '',
'OWNERTELNO': self.partner_phone or '',
'OPERATION': 'SAL', # direct sale
# Alias Manager parameters
'ALIAS': self.token_id.acquirer_ref,
'ALIASPERSISTEDAFTERUSE': 'Y' if self.token_id.active else 'N',
'ECI': 9, # Recurring (from eCommerce)
# 3DS parameters
'ACCEPTURL': return_url,
'DECLINEURL': return_url,
'EXCEPTIONURL': return_url,
'LANGUAGE': self.partner_lang or 'en_US',
'FLAG3D': 'Y' if request_3ds_authentication else 'N',
}
data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(data, incoming=False)
_logger.info(
"making payment request:\n%s",
pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'})
) # Log the payment request data without the password
response_content = self.acquirer_id._ogone_make_request('directlink', data)
try:
tree = objectify.fromstring(response_content)
except etree.XMLSyntaxError:
raise ValidationError("Ogone: " + "Received badly structured response from the API.")
_logger.info(
"received payment request response as an etree:\n%s",
etree.tostring(tree, pretty_print=True, encoding='utf-8')
)
return tree
@api.model
def _get_tx_from_feedback_data(self, provider, data):
""" Override of payment to find the transaction based on Ogone data.
:param str provider: The provider of the acquirer that handled the transaction
:param dict data: The feedback data sent by the provider
:return: The transaction if found
:rtype: recordset of `payment.transaction`
:raise: ValidationError if the data match no transaction
"""
tx = super()._get_tx_from_feedback_data(provider, data)
if provider != 'ogone':
return tx
reference = data.get('ORDERID')
tx = self.search([('reference', '=', reference), ('provider', '=', 'ogone')])
if not tx:
raise ValidationError(
"Ogone: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _process_feedback_data(self, data):
""" Override of payment to process the transaction based on Ogone data.
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
:raise: ValidationError if inconsistent data were received
"""
super()._process_feedback_data(data)
if self.provider != 'ogone':
return
feedback_type = data.get('FEEDBACK_TYPE')
if feedback_type == 'flexcheckout':
self._process_flexcheckout_data(data)
elif feedback_type == 'directlink':
self._process_directlink_data(data)
else:
raise ValidationError(
"Ogone: " + _("Received feedback data with unknown type: %s", feedback_type)
)
def _process_flexcheckout_data(self, data):
""" Create a token from Flexcheckout feedback data.
:param dict data: The feedback data from Flexcheckout
:return: None
"""
token = self.env['payment.token'].create({
'acquirer_id': self.acquirer_id.id,
'name': data.get('CARDNUMBER'), # Already padded with 'X's
'partner_id': self.partner_id.id,
'acquirer_ref': data['ALIASID'],
'verified': False, # No payment has been processed through this token yet
'active': self.tokenize, # Immediately archive the token if it was not requested
})
self.write({
'token_id': token.id,
'tokenize': False,
})
def _process_directlink_data(self, data):
""" Update the transaction state and the acquirer reference based on the feedback data.
:param dict data: The feedback data from DirectLink
:return: None
"""
if 'tree' in data:
data = data['tree']
self.acquirer_reference = data.get('PAYID')
payment_status = int(data.get('STATUS', '0'))
if payment_status in const.PAYMENT_STATUS_MAPPING['pending']:
self._set_pending()
elif payment_status in const.PAYMENT_STATUS_MAPPING['done']:
self.token_id.verified = True # The payment has been authorized, the token is valid
self._set_done()
elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']:
self._set_canceled()
else: # Classify unknown payment statuses as `error` tx state
_logger.info("received data with invalid payment status: %s", payment_status)
self._set_error(
"Ogone: " + _("Received data with invalid payment status: %s", payment_status)
)
def _send_refund_request(self):
""" Override of payment to send a refund request to Authorize.
Note: self.ensure_one()
:return: None
:raise: ValidationError if a badly structured response is received
"""
super()._send_refund_request()
if self.provider != 'ogone':
return
data = {
'PSPID': self.acquirer_id.ogone_pspid,
'ORDERID': self.reference,
'PAYID': self.acquirer_reference,
'USERID': self.acquirer_id.ogone_userid,
'PSWD': self.acquirer_id.ogone_password,
'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2),
'CURRENCY': self.currency_id.name,
'OPERATION': 'RFS', # refund
}
data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(data, incoming=False)
_logger.info(
"making refund request:\n%s",
pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'})
) # Log the refund request data without the password
response_content = self.acquirer_id._ogone_make_request('maintenancedirect', data)
try:
tree = objectify.fromstring(response_content)
except etree.XMLSyntaxError:
raise ValidationError("Ogone: " + "Received badly structured response from the API.")
_logger.info(
"received refund request response as an etree:\n%s",
etree.tostring(tree, pretty_print=True, encoding='utf-8')
)
feedback_data = {
'FEEDBACK_TYPE': 'directlink',
'ORDERID': tree.get('orderID'),
'tree': tree,
}
_logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data))
self._handle_feedback_data('ogone', feedback_data)

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

+4
View File
@@ -0,0 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_ogone
+20
View File
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.payment.tests.common import PaymentCommon
class OgoneCommon(PaymentCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ogone = cls._prepare_acquirer('ogone', update_values={
'ogone_pspid': 'dummy',
'ogone_userid': 'dummy',
'ogone_password': 'dummy',
'ogone_shakey_in': 'dummy',
'ogone_shakey_out': 'dummy',
})
cls.acquirer = cls.ogone
cls.currency = cls.currency_euro
+70
View File
@@ -0,0 +1,70 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment import utils as payment_utils
from .common import OgoneCommon
from ..controllers.main import OgoneController
@tagged('post_install', '-at_install')
class OgoneTest(OgoneCommon):
def test_validation_amount(self):
self.assertEqual(self.ogone._get_validation_amount(), 1.0)
# freeze time for consistent singularize_prefix behavior during the test
@freeze_time("2011-11-02 12:00:21")
def test_reference(self):
tx = self.create_transaction(flow="redirect", reference="")
self.assertEqual(tx.reference, "tx-20111102120021",
"Ogone: transaction reference wasn't correctly singularized.")
# Test prefixes of length > 40 chars
reference = self.env['payment.transaction']._compute_reference(
provider=self.ogone.provider,
prefix="This is a reference of more than 40 characters to annoy ogone",
)
self.assertEqual(reference, "This is a reference of mo-20111102120021")
self.assertEqual(len(reference), 40)
# freeze time for consistent singularize_prefix behavior during the test
@freeze_time("2011-11-02 12:00:21")
def test_redirect_form_values(self):
return_url = self._build_url(OgoneController._flexcheckout_return_url)
expected_values = {
'ACCOUNT_PSPID': self.ogone.ogone_pspid,
'ALIAS_ALIASID': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'),
'ALIAS_ORDERID': self.reference,
'ALIAS_STOREPERMANENTLY': 'N', # 'Y' if self.tokenize
'CARD_PAYMENTMETHOD': 'CreditCard',
'LAYOUT_LANGUAGE': self.partner.lang,
'PARAMETERS_ACCEPTURL': return_url,
'PARAMETERS_EXCEPTIONURL': return_url,
}
expected_values['SHASIGNATURE_SHASIGN'] = self.ogone._ogone_generate_signature(
expected_values, incoming=False, format_keys=True
).upper()
tx = self.create_transaction(flow="redirect")
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = tx._get_processing_values()
form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
self.assertEqual(form_info['action'], 'https://ogone.test.v-psp.com/Tokenization/HostedPage')
inputs = form_info['inputs']
self.assertEqual(len(expected_values), len(inputs))
for rendering_key, value in expected_values.items():
form_key = rendering_key.replace('_', '.')
self.assertEqual(
inputs[form_key],
value,
"Ogone: received value %s for input %s (expected %s)" % (
inputs[form_key], form_key, value,
)
)
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="redirect_form">
<form t-att-action="api_url" method="post">
<input type="hidden" name="ACCOUNT.PSPID" t-att-value="ACCOUNT_PSPID"/>
<input type="hidden" name="ALIAS.ALIASID" t-att-value="ALIAS_ALIASID"/>
<input type="hidden" name="ALIAS.ORDERID" t-att-value="ALIAS_ORDERID"/>
<input type="hidden" name="ALIAS.STOREPERMANENTLY" t-att-value="ALIAS_STOREPERMANENTLY"/>
<input type="hidden" name="CARD.PAYMENTMETHOD" t-att-value="CARD_PAYMENTMETHOD"/>
<input type="hidden" name="LAYOUT.LANGUAGE" t-att-value="LAYOUT_LANGUAGE"/>
<input type="hidden" name="PARAMETERS.ACCEPTURL" t-att-value="PARAMETERS_ACCEPTURL"/>
<input type="hidden" name="PARAMETERS.EXCEPTIONURL" t-att-value="PARAMETERS_EXCEPTIONURL"/>
<input type="hidden" name="SHASIGNATURE.SHASIGN" t-att-value="SHASIGNATURE_SHASIGN"/>
</form>
</template>
<template id="directlink_feedback" name="Payment processing page">
<t t-call="web.layout">
<t t-call-assets="web.assets_common"/>
<t t-raw="redirect_html"/>
</t>
</template>
</odoo>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_acquirer_form" model="ir.ui.view">
<field name="name">Ogone Acquirer Form</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'ogone')]}">
<field name="ogone_pspid" attrs="{'required':[('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_userid" attrs="{'required':[('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_password" attrs="{'required':[('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_shakey_in" attrs="{'required':[('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
<field name="ogone_shakey_out" attrs="{'required':[('provider', '=', 'ogone'), ('state', '!=', 'disabled')]}"/>
</group>
</xpath>
</field>
</record>
</odoo>