[REF] account,payment(_*),sale*: downgrade transactions into debug items

It was very confusing for the user to distinct account.payment and payment.transaction. From now on, the transactions are
technical objects and, in the backend, we only refer to it in log messages (Front end will be adapted in the same fashion
later on). They are hidden in debug mode in accounting\configuration\payments as their purpose is now purely technical/log

This commit also aims to reduce the gap between the accounting app and the transactions: account.payment objects are
created/validated upon completion of transaction.

To ease the capture/voiding of pending transactions, the related buttons are now displayed directly on the SO/invoice
instead of the transactions.

Was task: https://www.odoo.com/web#id=35857&view_type=form&model=project.task&action=333&active_id=967&menu_id=4720
Was PR #24043

[FIX] add domain based on journal to payment tokens

Was opw: https://www.odoo.com/web?debug#id=1828206&view_type=form&model=project.task&menu_id=5200
This commit is contained in:
qdp-odoo
2018-05-23 15:44:55 +02:00
parent 7a31a92afa
commit 01216345e2
82 changed files with 1303 additions and 1367 deletions
@@ -368,7 +368,9 @@
<button name="invoice_print" string="Print Invoice" type="object" attrs="{'invisible':['|',('sent','=',True), ('state', 'not in', ('open','paid'))]}" class="oe_highlight" groups="base.group_user"/>
<button name="action_invoice_sent" type="object" string="Send by Email" attrs="{'invisible':['|',('sent','=',False), ('state', 'not in', ('open','paid'))]}" groups="base.group_user"/>
<button name="invoice_print" string="Print Invoice" type="object" attrs="{'invisible':['|',('sent','=',False), ('state', 'not in', ('open','paid'))]}" groups="base.group_user"/>
<button name="%(action_account_invoice_payment)d" id="account_invoice_payment_btn" type="action" states="open" string="Register Payment" groups="account.group_account_invoice" class="oe_highlight"/>
<button name="%(action_account_invoice_payment)d" id="account_invoice_payment_btn" type="action"
attrs="{'invisible': [('state', '!=', 'open')]}"
string="Register Payment" groups="account.group_account_invoice" class="oe_highlight"/>
<button name="action_invoice_open" type="object" states="draft" string="Validate" class="oe_highlight o_invoice_validate" groups="account.group_account_invoice"/>
<button name="%(action_account_invoice_refund)d" type='action' string='Add Credit Note' groups="account.group_account_invoice" attrs="{'invisible': ['|',('type', '=', 'out_refund'), ('state', 'not in', ('open','paid'))]}"/>
<button name="action_invoice_draft" states="cancel" string="Reset to Draft" type="object" groups="account.group_account_invoice"/>
+1 -3
View File
@@ -12,10 +12,8 @@ enable payment.
* UPDATE ME
""",
'depends': ['payment', 'account'],
'depends': ['payment'],
'data': [
'views/account_invoice_views.xml',
'views/payment_views.xml',
'views/account_portal_templates.xml',
],
'installable': True,
+23 -38
View File
@@ -24,26 +24,23 @@ class PaymentPortal(http.Controller):
return False
try:
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
acquirer_id = int(acquirer_id)
except:
return False
token = request.env['payment.token'].sudo() # currently no support of payment tokens
tx = request.env['payment.transaction'].sudo()._check_or_create_invoice_tx(
invoice_sudo,
acquirer,
payment_token=token,
tx_type='form_save' if save_token else 'form',
add_tx_values={
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id,
'callback_res_id': invoice_sudo.id,
'callback_method': callback_method,
})
vals = {
'acquirer_id': acquirer_id,
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id,
'callback_res_id': invoice_sudo.id,
'callback_method': callback_method,
}
# set the transaction id into the session
request.session['portal_invoice_%s_transaction_id' % invoice_sudo.id] = tx.id
if save_token:
vals['type'] = 'form_save'
return tx.render_invoice_button(
transaction = invoice_sudo._create_payment_transaction(vals)
return transaction.render_invoice_button(
invoice_sudo,
success_url,
submit_txt=_('Pay & Confirm'),
@@ -70,33 +67,21 @@ class PaymentPortal(http.Controller):
return request.redirect(_build_url_w_params(error_url, params))
try:
token = request.env['payment.token'].sudo().browse(int(pm_id))
pm_id = int(pm_id)
except (ValueError, TypeError):
token = False
if not token:
params['error'] = 'pay_invoice_invalid_token'
return request.redirect(_build_url_w_params(error_url, params))
# find an existing tx or create a new one
tx = request.env['payment.transaction'].sudo()._check_or_create_invoice_tx(
invoice_sudo,
token.acquirer_id,
payment_token=token,
tx_type='server2server',
add_tx_values={
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id,
'callback_res_id': invoice_sudo.id,
'callback_method': callback_method,
})
vals = {
'payment_token_id': pm_id,
'type': 'server2server',
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)],
limit=1).id,
'callback_res_id': invoice_sudo.id,
'callback_method': callback_method,
}
# set the transaction id into the session
request.session['portal_invoice_%s_transaction_id' % invoice_sudo.id] = tx.id
invoice_sudo._create_payment_transaction(vals)
# proceed to the payment
res = tx.confirm_invoice_token()
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_invoice'
params['success'] = 'pay_invoice'
return request.redirect(_build_url_w_params(success_url, params))
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_invoice
from . import payment
@@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
payment_tx_ids = fields.One2many('payment.transaction', 'account_invoice_id', string='Transactions')
payment_tx_id = fields.Many2one('payment.transaction', string='Last Transaction', copy=False)
payment_acquirer_id = fields.Many2one(
'payment.acquirer', string='Payment Acquirer',
related='payment_tx_id.acquirer_id', store=True)
payment_tx_count = fields.Integer(string="Number of payment transactions", compute='_compute_payment_tx_count')
def _compute_payment_tx_count(self):
tx_data = self.env['payment.transaction'].read_group(
[('account_invoice_id', 'in', self.ids)],
['account_invoice_id'], ['account_invoice_id']
)
mapped_data = dict([(m['account_invoice_id'][0], m['account_invoice_id_count']) for m in tx_data])
for invoice in self:
invoice.payment_tx_count = mapped_data.get(invoice.id, 0)
def action_view_transactions(self):
action = {
'name': _('Payment Transactions'),
'type': 'ir.actions.act_window',
'res_model': 'payment.transaction',
'target': 'current',
}
tx = self.env['payment.transaction'].search([('account_invoice_id', 'in', self.ids)])
if len(tx) == 1:
action['res_id'] = tx.ids[0]
action['view_mode'] = 'form'
else:
action['view_mode'] = 'tree,form'
action['domain'] = [('account_invoice_id', 'in', self.ids)]
return action
-157
View File
@@ -12,115 +12,6 @@ _logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
account_invoice_id = fields.Many2one('account.invoice', string='Invoice')
def form_feedback(self, data, acquirer_name):
""" Override to confirm the invoice, if defined, and if the transaction is done. """
tx = None
res = super(PaymentTransaction, self).form_feedback(data, acquirer_name)
# fetch the tx
tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
if hasattr(self, tx_find_method_name):
tx = getattr(self, tx_find_method_name)(data)
if tx and tx.account_invoice_id:
_logger.info(
'<%s> transaction <%s> processing form feedback for invoice <%s>: tx ref:%s, tx amount: %s',
acquirer_name, tx.id, tx.account_invoice_id.id, tx.reference, tx.amount)
tx._confirm_invoice()
return res
def confirm_invoice_token(self):
""" Confirm a transaction token and call SO confirmation if it is a success.
:return: True if success; error string otherwise """
self.ensure_one()
if self.payment_token_id and self.partner_id == self.account_invoice_id.partner_id:
try:
s2s_result = self.s2s_do_transaction()
except Exception as e:
_logger.warning(
_("<%s> transaction (%s) failed : <%s>") %
(self.acquirer_id.provider, self.id, str(e)))
return 'pay_invoice_tx_fail'
valid_state = 'authorized' if self.acquirer_id.capture_manually else 'done'
if not s2s_result or self.state != valid_state:
_logger.warning(
_("<%s> transaction (%s) invalid state : %s") %
(self.acquirer_id.provider, self.id, self.state_mesage))
return 'pay_invoice_tx_state'
try:
# Auto-confirm SO if necessary
return self._confirm_invoice()
except Exception as e:
_logger.warning(
_("<%s> transaction (%s) invoice confirmation failed : <%s>") %
(self.acquirer_id.provider, self.id, str(e)))
return 'pay_invoice_tx_confirm'
return 'pay_invoice_tx_token'
def _confirm_invoice(self):
""" Check tx state, confirm and pay potential invoice """
self.ensure_one()
# check tx state, confirm the potential SO
if self.account_invoice_id.state != 'open':
_logger.warning('<%s> transaction STATE INCORRECT for invoice %s (ID %s, state %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id, self.account_invoice_id.state)
return 'pay_invoice_invalid_doc_state'
if not float_compare(self.amount, self.account_invoice_id.amount_total, 2) == 0:
_logger.warning(
'<%s> transaction AMOUNT MISMATCH for invoice %s (ID %s): expected %r, got %r',
self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id,
self.account_invoice_id.amount_total, self.amount,
)
self.account_invoice_id.message_post(
subject=_("Amount Mismatch (%s)") % self.acquirer_id.provider,
body=_("The invoice was not confirmed despite response from the acquirer (%s): invoice amount is %r but acquirer replied with %r.") % (
self.acquirer_id.provider,
self.account_invoice_id.amount_total,
self.amount,
)
)
return 'pay_invoice_tx_amount'
if self.state == 'authorized' and self.acquirer_id.capture_manually:
_logger.info('<%s> transaction authorized, nothing to do with invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id)
elif self.state == 'done':
_logger.info('<%s> transaction completed, paying invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id)
self._pay_invoice()
else:
_logger.warning('<%s> transaction MISMATCH for invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id)
return 'pay_invoice_tx_state'
return True
def _pay_invoice(self):
self.ensure_one()
# force company to ensure journals/accounts etc. are correct
# company_id needed for default_get on account.journal
# force_company needed for company_dependent fields
ctx_company = {'company_id': self.account_invoice_id.company_id.id,
'force_company': self.account_invoice_id.company_id.id}
invoice = self.account_invoice_id.with_context(**ctx_company)
if not self.acquirer_id.journal_id:
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if not default_journal:
_logger.warning('<%s> transaction completed, could not auto-generate payment for invoice %s (ID %s) (no journal set on acquirer)',
self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id)
return False
self.acquirer_id.journal_id = default_journal
invoice.pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=invoice.amount_total)
invoice.payment_ids.write({'payment_transaction_id': self.ids[0]})
_logger.info('<%s> transaction <%s> completed, reconciled invoice %s (ID %s))',
self.acquirer_id.provider, self.ids[0], invoice.number, invoice.id)
return True
def render_invoice_button(self, invoice, return_url, submit_txt=None, render_values=None):
values = {
'return_url': return_url,
@@ -134,51 +25,3 @@ class PaymentTransaction(models.Model):
invoice.currency_id.id,
values=values,
)
def _check_or_create_invoice_tx(self, invoice, acquirer, payment_token=None, tx_type='form', add_tx_values=None):
tx = self
if not tx:
tx = self.search([('reference', '=', invoice.number)], limit=1)
if tx and tx.state in ['error', 'cancel']: # filter incorrect states
tx = False
if (tx and acquirer and tx.acquirer_id != acquirer) or (tx and tx.account_invoice_id != invoice): # filter unmatching
tx = False
if tx and tx.payment_token_id and payment_token and payment_token != tx.payment_token_id: # new or distinct token
tx = False
# still draft tx, no more info -> create a new one
if tx and tx.state == 'draft':
tx = False
if not tx:
tx_values = {
'acquirer_id': acquirer.id,
'type': tx_type,
'amount': invoice.amount_total,
'currency_id': invoice.currency_id.id,
'partner_id': invoice.partner_id.id,
'partner_country_id': invoice.partner_id.country_id.id,
'reference': self.get_next_reference(invoice.number),
'account_invoice_id': invoice.id,
}
if add_tx_values:
tx_values.update(add_tx_values)
if payment_token and payment_token.sudo().partner_id == invoice.partner_id:
tx_values['payment_token_id'] = payment_token.id
tx = self.create(tx_values)
# update invoice
invoice.write({
'payment_tx_id': tx.id,
})
return tx
def _post_process_after_done(self, **kwargs):
# set invoice id in payment transaction when payment being done from sale order
res = super(PaymentTransaction, self)._post_process_after_done()
if kwargs.get('invoice_id'):
self.account_invoice_id = kwargs['invoice_id']
return res
@@ -1,17 +0,0 @@
<?xml version="1.0"?>
<odoo>
<record id="account_invoice_view_form_inherit_payment" model="ir.ui.view">
<field name="name">account.invoice.view.form.inherit.payment</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_transactions" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('payment_tx_count', '=', 0)]}">
<field name="payment_tx_count" widget="statinfo" string="Transactions"/>
</button>
</xpath>
</field>
</record>
</odoo>
@@ -12,39 +12,9 @@
</xpath>
</template>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<template id="portal_invoice_page_inherit_payment" name="Payment on My Invoices" inherit_id="account.portal_invoice_page">
<xpath expr="//div[@id='o_portal_account_actions']/a" position="before">
<t t-set="tx_ids" t-value="invoice.payment_tx_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
<a t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total"
class="btn btn-primary pull-right ml8" href="#portal_pay">
<i class="fa fa-arrow-circle-right"/> Pay Now
@@ -59,9 +29,9 @@
</a>
</xpath>
<xpath expr="//t[@t-call='account.portal_invoice_success']" position="after">
<div t-if="invoice.payment_tx_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
<t t-call="account_payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="invoice.payment_tx_id"/>
<div t-if="invoice.transaction_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
<t t-call="payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
</t>
</div>
</xpath>
@@ -88,7 +58,7 @@
<template id="portal_invoice_report" name="Invoice portal page inherit payment" inherit_id="account.portal_invoice_report">
<xpath expr="//div[@name='customer_address']" position="after">
<t t-set="tx_ids" t-value="invoice.payment_tx_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state != 'draft')"/>
<t t-if="tx_ids">
<div>
<strong>Transactions</strong>
@@ -118,34 +88,16 @@
<t t-if="error == 'pay_invoice_invalid_doc'">
There was an error processing your payment: invalid invoice.
</t>
<t t-if="error == 'pay_invoice_invalid_doc_state'">
There was an error processing your payment: invalid invoice state.
</t>
<t t-if="error == 'pay_invoice_invalid_token'">
There was en error processing your payment: invalid credit card ID.
</t>
<t t-if="error == 'pay_invoice_tx_amount'">
There was an error processing your payment: transaction amount issue.<br />
<t t-if="invoice.payment_tx_id">
<t t-esc="invoice.payment_tx_id.amount"/> / <t t-esc="invoice.amount_total"/>
</t>
</t>
<t t-if="error == 'pay_invoice_tx_fail'">
There was an error processing your payment: transaction failed.<br />
<t t-if="invoice.payment_tx_id and invoice.payment_tx_id.state_message">
<t t-esc="invoice.payment_tx_id.state_message"/>
<t t-set="tx_id" t-value="invoice.get_portal_last_transaction()"/>
<t t-if="tx_id and tx_id.state_message">
<t t-esc="tx_id.state_message"/>
</t>
</t>
<t t-if="error == 'pay_invoice_tx_state'">
There was an error processing your payment: transaction issue.<br />
<t t-if="invoice.payment_tx_id and invoice.payment_tx_id.state_message">
<t t-esc="invoice.payment_tx_id.state_message"/>
</t>
</t>
<t t-if="error == 'pay_invoice_tx_confirm'">
There was an error processing your payment: impossible to validate invoice.
</t>
<t t-if="error == 'pay_invoice_tx_token'">
There was an error processing your payment: issue with credit card ID validation.
</t>
@@ -156,8 +108,9 @@
inherit_id="account.portal_invoice_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_invoice'">
<span t-if='invoice.payment_acquirer_id.done_msg' t-raw="invoice.payment_acquirer_id.done_msg"/>
<span t-if='invoice.payment_acquirer_id.post_msg' t-raw="invoice.payment_acquirer_id.post_msg"/>
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</t>
</xpath>
</template>
@@ -1,13 +0,0 @@
<?xml version="1.0"?>
<odoo>
<record id="payment_transaction_view_form" model="ir.ui.view">
<field name="name">payment.transaction.view.form.inherit.account.payment</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment.transaction_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='acquirer_reference']" position="after">
<field name="account_invoice_id"/>
</xpath>
</field>
</record>
</odoo>
+5 -5
View File
@@ -8,12 +8,12 @@ class SaleOrder(models.Model):
@api.multi
def action_confirm(self):
self.ensure_one()
res = super(SaleOrder, self).action_confirm()
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
self.order_line._update_registrations(confirm=self.amount_total == 0, cancel_to_draft=False)
if any(self.order_line.filtered(lambda line: line.event_id)):
return self.env['ir.actions.act_window'].with_context(default_sale_order_id=self.id).for_xml_id('event_sale', 'action_sale_order_event_registration')
for so in self:
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
so.order_line._update_registrations(confirm=so.amount_total == 0, cancel_to_draft=False)
if any(so.order_line.filtered(lambda line: line.event_id)):
return self.env['ir.actions.act_window'].with_context(default_sale_order_id=so.id).for_xml_id('event_sale', 'action_sale_order_event_registration')
return res
+1
View File
@@ -13,6 +13,7 @@
'data/payment_acquirer_data.xml',
'views/payment_views.xml',
'views/account_payment_views.xml',
'views/account_invoice_views.xml',
'views/payment_templates.xml',
'views/payment_portal_templates.xml',
'views/assets.xml',
+60 -41
View File
@@ -24,32 +24,65 @@ class WebsitePayment(http.Controller):
return request.render("payment.pay_methods", values)
@http.route(['/website_payment/pay'], type='http', auth='public', website=True)
def pay(self, reference='', amount=False, currency_id=None, acquirer_id=None, **kw):
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw):
env = request.env
user = env.user.sudo()
currency_id = currency_id and int(currency_id) or user.company_id.currency_id.id
currency = env['res.currency'].browse(currency_id)
# Default values
values = {
'amount': 0.0,
'currency': user.company_id.currency_id,
}
# Check sale order
if order_id:
try:
order_id = int(order_id)
order = env['sale.order'].browse(order_id)
values.update({
'currency': order.currency_id,
'amount': order.amount_total,
'order_id': order_id
})
except:
order_id = None
# Check currency
if currency_id:
try:
currency_id = int(currency_id)
values['currency'] = env['res.currency'].browse(currency_id)
except:
pass
# Check amount
if amount:
try:
amount = float(amount)
values['amount'] = amount
except:
pass
# Check reference
reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {}
values['reference'] = env['payment.transaction']._compute_reference(values=reference_values, prefix=reference)
# Check acquirer
acquirers = None
if acquirer_id:
acquirers = env['payment.acquirer'].browse(int(acquirer_id))
if not acquirers:
acquirers = env['payment.acquirer'].search([('website_published', '=', True), ('company_id', '=', user.company_id.id)])
# auto-increment reference with a number suffix if the reference already exists
reference = request.env['payment.transaction'].get_next_reference(reference)
# Check partner
partner_id = user.partner_id.id if not user._is_public() else False
values = {
'reference': reference,
'currency': currency,
'amount': float(amount),
values.update({
'return_url': '/website_payment/confirm',
'partner_id': partner_id,
'bootstrap_formatting': True,
'error_msg': kw.get('error_msg')
}
})
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s']
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form']
@@ -57,43 +90,26 @@ class WebsitePayment(http.Controller):
return request.render('payment.pay', values)
def _get_existing_transaction(self, reference, amount, partner_id, currency_id, acquirer_id, tx_id):
PaymentTransaction = request.env['payment.transaction']
tx = None
if tx_id:
tx = PaymentTransaction.sudo().browse(tx_id)
if not tx.exists() or tx.reference != reference or tx.acquirer_id.id != acquirer_id:
tx = None
if not tx:
tx = PaymentTransaction.sudo().search([('reference', '=', reference), ('acquirer_id', '=', acquirer_id)])
if tx and (tx.state != 'draft' or tx.partner_id.id != partner_id or tx.amount != amount or tx.currency_id.id != currency_id):
tx = None
return tx
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',], type='json', auth='public')
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
order_id = kwargs.get('order_id')
tx = self._get_existing_transaction(reference, float(amount), partner_id,
int(currency_id), int(acquirer_id), request.session.get('website_payment_tx_id'))
values = {
'acquirer_id': int(acquirer_id),
'reference': reference,
'amount': float(amount),
'currency_id': int(currency_id),
'partner_id': partner_id,
'type': 'form_save' if acquirer.save_token != 'none' and partner_id else 'form',
}
if not tx:
values = {
'acquirer_id': int(acquirer_id),
'reference': reference,
'amount': float(amount),
'currency_id': currency_id,
'partner_id': partner_id,
'type': 'form_save' if acquirer.save_token != 'none' and partner_id else 'form',
}
if order_id:
values['sale_order_ids'] = [(6, 0, [order_id])]
tx = request.env['payment.transaction'].sudo().create(values)
request.session['website_payment_tx_id'] = tx.id
tx = request.env['payment.transaction'].sudo().create(values)
render_values = {
'return_url': '/website_payment/confirm?tx_id=%d' % tx.id,
@@ -106,6 +122,7 @@ class WebsitePayment(http.Controller):
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>'], type='http', auth='public', website=True)
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
token = request.env['payment.token'].browse(int(pm_id))
order_id = kwargs.get('order_id')
if not token:
return request.redirect('/website_payment/pay?error_msg=%s' % _('Cannot setup the payment.'))
@@ -122,8 +139,10 @@ class WebsitePayment(http.Controller):
'type': 'form_save' if token.acquirer_id.save_token != 'none' and partner_id else 'form',
}
if order_id:
values['sale_order_ids'] = [(6, 0, [order_id])]
tx = request.env['payment.transaction'].sudo().create(values)
request.session['website_payment_tx_id'] = tx.id
try:
res = tx.s2s_do_transaction()
@@ -138,7 +157,7 @@ class WebsitePayment(http.Controller):
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True)
def confirm(self, **kw):
tx_id = int(kw.get('tx_id', 0)) or request.session.pop('website_payment_tx_id', 0)
tx_id = int(kw.get('tx_id', 0))
if tx_id:
tx = request.env['payment.transaction'].browse(tx_id)
if tx.state == 'done':
+2
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from . import payment_acquirer
from . import account_invoice
from . import res_partner
from . import account_payment
from . import chart_template
+98
View File
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
transaction_ids = fields.Many2many('payment.transaction', 'account_invoice_transaction_rel', 'invoice_id', 'transaction_id',
string='Transactions', copy=False, readonly=True)
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
string='Authorized Transactions', copy=False, readonly=True)
@api.depends('transaction_ids')
def _compute_authorized_transaction_ids(self):
for trans in self:
trans.authorized_transaction_ids = trans.transaction_ids.filtered(lambda t: t.state == 'authorized')
@api.multi
def get_portal_last_transaction(self):
self.ensure_one()
return self.transaction_ids.get_last_transaction()
@api.multi
def _create_payment_transaction(self, vals):
'''Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
current invoices fields (e.g. the partner or the currency).
:param vals: The values to create a new payment.transaction.
:return: The newly created payment.transaction record.
'''
# Ensure the currencies are the same.
currency = self[0].currency_id
if any([inv.currency_id != currency for inv in self]):
raise ValidationError(_('A transaction can\'t be linked to invoices having different currencies.'))
# Ensure the partner are the same.
partner = self[0].partner_id
if any([inv.partner_id != partner for inv in self]):
raise ValidationError(_('A transaction can\'t be linked to invoices having different partners.'))
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
acquirer_id = vals.get('acquirer_id')
acquirer = None
payment_token_id = vals.get('payment_token_id')
if payment_token_id:
payment_token = self.env['payment.token'].sudo().browse(payment_token_id)
# Check payment_token/acquirer matching or take the acquirer from token
if acquirer_id:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
if payment_token and payment_token.acquirer_id != acquirer:
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
payment_token.acquirer_id.name, acquirer.name))
if payment_token and payment_token.partner_id != partner:
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
payment_token.partner.name, partner.name))
else:
acquirer = payment_token.acquirer_id
# Check an acquirer is there.
if not acquirer_id and not acquirer:
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
if not acquirer:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
# Check a journal is set on acquirer.
if not acquirer.journal_id:
raise ValidationError(_('A journal must be specified of the acquirer %s.' % acquirer.name))
if not acquirer_id and acquirer:
vals['acquirer_id'] = acquirer.id
vals.update({
'amount': sum(self.mapped('amount_total')),
'currency_id': currency.id,
'partner_id': partner.id,
'invoice_ids': [(6, 0, self.ids)],
})
transaction = self.env['payment.transaction'].create(vals)
# Process directly if payment_token
if transaction.payment_token_id:
transaction.s2s_do_transaction()
return transaction
@api.multi
def payment_action_capture(self):
self.authorized_transaction_ids.s2s_capture_transaction()
@api.multi
def payment_action_void(self):
self.authorized_transaction_ids.s2s_void_transaction()
+76 -37
View File
@@ -9,53 +9,92 @@ from odoo.exceptions import ValidationError
class AccountPayment(models.Model):
_inherit = 'account.payment'
payment_transaction_id = fields.Many2one('payment.transaction', string="Payment Transaction")
payment_transaction_id = fields.Many2one('payment.transaction', string='Payment Transaction', readonly=True)
payment_token_id = fields.Many2one('payment.token', string="Saved payment token", domain=[('acquirer_id.capture_manually', '=', False)],
help="Note that tokens from acquirers set to only authorize transactions (instead of capturing the amount) are not available.")
@api.onchange('partner_id')
def _onchange_partner_id(self):
res = {}
if self.partner_id:
partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids
res['domain'] = {'payment_token_id': [('partner_id', 'in', partners.ids), ('acquirer_id.capture_manually', '=', False)]}
@api.multi
def _get_payment_chatter_link(self):
self.ensure_one()
return '<a href=# data-oe-model=account.payment data-oe-id=%d>%s</a>' % (self.id, self.name)
@api.onchange('partner_id', 'payment_method_id', 'journal_id')
def _onchange_set_payment_token_id(self):
res = {}
if not self.payment_method_code == 'electronic' or not self.partner_id or not self.journal_id:
self.payment_token_id = False
return res
partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids
domain = [('partner_id', 'in', partners.ids), ('acquirer_id.journal_id', '=', self.journal_id.id)]
self.payment_token_id = self.env['payment.token'].search(domain, limit=1)
res['domain'] = {'payment_token_id': domain}
return res
@api.onchange('payment_method_id', 'journal_id')
def _onchange_payment_method(self):
if self.payment_method_code == 'electronic':
self.payment_token_id = self.env['payment.token'].search([('partner_id', '=', self.partner_id.id), ('acquirer_id.capture_manually', '=', False)], limit=1)
else:
self.payment_token_id = False
@api.model
def create(self, vals):
account_payment = super(AccountPayment, self).create(vals)
if account_payment.payment_token_id:
account_payment._do_payment()
return account_payment
def _do_payment(self):
if self.payment_token_id.acquirer_id.capture_manually:
raise ValidationError(_('This feature is not available for payment acquirers set to the "Authorize" mode.\n'
'Please use a token from another provider than %s.') % self.payment_token_id.acquirer_id.name)
reference = "P-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
tx = self.env['payment.transaction'].create({
@api.multi
def _prepare_payment_transaction_vals(self):
self.ensure_one()
return {
'amount': self.amount,
'acquirer_id': self.payment_token_id.acquirer_id.id,
'type': 'server2server',
'currency_id': self.currency_id.id,
'reference': reference,
'payment_token_id': self.payment_token_id.id,
'partner_id': self.partner_id.id,
'partner_country_id': self.partner_id.country_id.id,
})
'invoice_ids': [(6, 0, self.invoice_ids.ids)],
'payment_token_id': self.payment_token_id.id,
'acquirer_id': self.payment_token_id.acquirer_id.id,
'payment_id': self.id,
'type': 'server2server',
}
s2s_result = tx.s2s_do_transaction()
@api.multi
def _create_payment_transaction(self, vals=None):
for pay in self:
if pay.payment_transaction_id:
raise ValidationError(_('A payment transaction already exists.'))
elif not pay.payment_token_id:
raise ValidationError(_('A token is required to create a new payment transaction.'))
if not s2s_result or tx.state != 'done':
raise ValidationError(_("Payment transaction failed (%s)") % tx.state_message)
transactions = self.env['payment.transaction']
for pay in self:
transaction_vals = pay._prepare_payment_transaction_vals()
self.payment_transaction_id = tx
if vals:
transaction_vals.update(vals)
transaction = self.env['payment.transaction'].create(transaction_vals)
transactions += transaction
# Link the transaction to the payment.
pay.payment_transaction_id = transaction
return transactions
@api.multi
def post(self):
# Post the payments "normally" if no transactions are needed.
# If not, let the acquirer updates the state.
# __________ ______________
# | Payments | | Transactions |
# |__________| |______________|
# || | |
# || | |
# || | |
# __________ no s2s required __\/______ s2s required | | s2s_do_transaction()
# | Posted |<-----------------| post() |---------------- |
# |__________| |__________|<----- |
# | |
# OR---------------
# __________ __________ |
# | Cancelled|<-----------------| cancel() |<-----
# |__________| |__________|
payments_need_trans = self.filtered(lambda pay: pay.payment_token_id and not pay.payment_transaction_id)
transactions = payments_need_trans._create_payment_transaction()
res = super(AccountPayment, self - payments_need_trans).post()
transactions.s2s_do_transaction()
return res
+14
View File
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class WizardMultiChartsAccounts(models.TransientModel):
_inherit = 'wizard.multi.charts.accounts'
@api.multi
def _create_bank_journals_from_o2m(self, company, acc_template_ref):
res = super(WizardMultiChartsAccounts, self)._create_bank_journals_from_o2m(company, acc_template_ref)
# Try to generate the missing journals
return res + self.env['payment.acquirer']._create_missing_journal_for_acquirers()
+347 -130
View File
@@ -2,13 +2,16 @@
import hashlib
import hmac
import logging
import datetime
from datetime import datetime
import pprint
from odoo import api, exceptions, fields, models, _
from odoo.tools import consteq, float_round, image_resize_images, image_resize_image, ustr
from odoo.addons.base.models import ir_module
from odoo.exceptions import ValidationError
from odoo import api, SUPERUSER_ID
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.misc import formatLang
_logger = logging.getLogger(__name__)
@@ -21,6 +24,11 @@ def _partner_split_name(partner_name):
return [' '.join(partner_name.split()[:-1]), ' '.join(partner_name.split()[-1:])]
def create_missing_journal_for_acquirers(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
env['payment.acquirer']._create_missing_journal_for_acquirers()
class PaymentAcquirer(models.Model):
""" Acquirer Model. Each specific acquirer can extend the model by adding
its own fields, using the acquirer_name as a prefix for the new fields.
@@ -52,6 +60,9 @@ class PaymentAcquirer(models.Model):
_description = 'Payment Acquirer'
_order = 'website_published desc, sequence, name'
def _get_default_view_template_id(self):
return self.env.ref('payment.default_acquirer_button', raise_if_not_found=False)
name = fields.Char('Name', required=True, translate=True)
description = fields.Html('Description')
sequence = fields.Integer('Sequence', default=10, help="Determine the display order")
@@ -62,7 +73,7 @@ class PaymentAcquirer(models.Model):
'res.company', 'Company',
default=lambda self: self.env.user.company_id.id, required=True)
view_template_id = fields.Many2one(
'ir.ui.view', 'Form Button Template', required=True)
'ir.ui.view', 'Form Button Template', default=_get_default_view_template_id, required=True)
registration_view_template_id = fields.Many2one(
'ir.ui.view', 'S2S Form Template', domain=[('type', '=', 'qweb')],
help="Template for method registration")
@@ -79,7 +90,6 @@ class PaymentAcquirer(models.Model):
# Formerly associated to `generate_and_pay_invoice` option from auto_confirm
journal_id = fields.Many2one(
'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])],
default=lambda self: self.env['account.journal'].search([('type', 'in', ['bank', 'cash'])], limit=1),
help="""Payments will be registered into this journal. If you get paid straight on your bank account,
select your bank account. If you get paid in batch for several transactions, create a specific
payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
@@ -156,6 +166,16 @@ class PaymentAcquirer(models.Model):
('s2s','Payment from Odoo')],
default='form', required=True, string='Payment Flow',
help="""Note: Subscriptions does not take this field in account, it uses server to server by default.""")
inbound_payment_method_ids = fields.Many2many('account.payment.method', related='journal_id.inbound_payment_method_ids')
@api.onchange('payment_flow')
def _onchange_payment_flow(self):
electronic = self.env.ref('payment.account_payment_method_electronic_in')
if self.token_implemented and self.payment_flow == 's2s':
if electronic not in self.inbound_payment_method_ids:
self.inbound_payment_method_ids = [(4, electronic.id)]
elif electronic in self.inbound_payment_method_ids:
self.inbound_payment_method_ids = [(2, electronic.id)]
def _search_is_tokenized(self, operator, value):
tokenized = self._get_feature_support()['tokenize']
@@ -197,6 +217,58 @@ class PaymentAcquirer(models.Model):
"""
return dict(authorize=[], tokenize=[], fees=[])
@api.multi
def _prepare_account_journal_vals(self):
'''Prepare the values to create the acquirer's journal.
:return: a dictionary to create a account.journal record.
'''
self.ensure_one()
account_vals = self.env['wizard.multi.charts.accounts']._prepare_transfer_account(self.name, self.company_id)
account = self.env['account.account'].create(account_vals)
inbound_payment_method_ids = []
if self.token_implemented and self.payment_flow == 's2s':
inbound_payment_method_ids.append((4, self.env.ref('payment.account_payment_method_electronic_in').id))
return {
'name': self.name,
'code': self.name.upper(),
'sequence': 999,
'type': 'bank',
'company_id': self.company_id.id,
'default_debit_account_id': account.id,
'default_credit_account_id': account.id,
# Show the journal on dashboard if the acquirer is published on the website.
'show_on_dashboard': self.website_published,
# Don't show payment methods in the backend.
'inbound_payment_method_ids': inbound_payment_method_ids,
'outbound_payment_method_ids': [],
}
@api.model
def _create_missing_journal_for_acquirers(self):
'''Create the journal for active acquirers.
We want one journal per acquirer. However, we can't create them during the 'create' of the payment.acquirer
because every acquirers are defined on the 'payment' module but is active only when installing their own module
(e.g. payment_paypal for Paypal). We can't do that in such modules because we have no guarantee the chart template
is already installed.
'''
# Search for installed acquirers modules.
# If this method is triggered by a post_init_hook, the module is 'to install'.
# If the trigger comes from the chart template wizard, the modules are already installed.
acquirer_modules = self.env['ir.module.module'].search(
[('name', 'like', 'payment_%'), ('state', 'in', ('to install', 'installed'))])
acquirer_names = [a.name.split('_')[1] for a in acquirer_modules]
# Search for acquirers having no journal
company = self.env.user.company_id
acquirers = self.env['payment.acquirer'].search(
[('provider', 'in', acquirer_names), ('journal_id', '=', False), ('company_id', '=', company.id)])
journals = self.env['account.journal']
for acquirer in acquirers.filtered(lambda l: not l.journal_id and l.company_id.chart_template_id):
acquirer.journal_id = self.env['account.journal'].create(acquirer._prepare_account_journal_vals())
journals += acquirer.journal_id
return journals
@api.model
def create(self, vals):
image_resize_images(vals)
@@ -209,7 +281,13 @@ class PaymentAcquirer(models.Model):
@api.multi
def toggle_website_published(self):
self.write({'website_published': not self.website_published})
''' When clicking on the website publish toggle button, the website_published is reversed and
the acquirer journal is set or not in favorite on the dashboard.
'''
self.ensure_one()
self.website_published = not self.website_published
if self.journal_id:
self.journal_id.show_on_dashboard = self.website_published
return True
@api.multi
@@ -473,39 +551,31 @@ class PaymentTransaction(models.Model):
def _get_default_partner_country_id(self):
return self.env['res.company']._company_default_get('payment.transaction').country_id.id
create_date = fields.Datetime('Creation Date', readonly=True)
date_validate = fields.Datetime('Validation Date')
acquirer_id = fields.Many2one('payment.acquirer', 'Acquirer', required=True)
provider = fields.Selection(string='Provider', related='acquirer_id.provider')
date = fields.Datetime('Validation Date', readonly=True)
acquirer_id = fields.Many2one('payment.acquirer', string='Acquirer', readonly=True, required=True)
provider = fields.Selection(string='Provider', related='acquirer_id.provider', readonly=True)
type = fields.Selection([
('validation', 'Validation of the bank card'),
('server2server', 'Server To Server'),
('form', 'Form'),
('form_save', 'Form with tokenization')], 'Type',
default='form', required=True)
default='form', required=True, readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('pending', 'Pending'),
('authorized', 'Authorized'),
('done', 'Done'),
('refunding', 'Refunding'),
('refunded', 'Refunded'),
('error', 'Error'),
('cancel', 'Canceled')], 'Status',
copy=False, default='draft', required=True, track_visibility='onchange')
state_message = fields.Text('Message', help='Field used to store error and/or validation messages for information')
# payment
amount = fields.Float(
'Amount', digits=(16, 2), required=True, track_visibility='always',
help='Amount')
fees = fields.Float(
'Fees', digits=(16, 2), track_visibility='always',
help='Fees amount; set by the system because depends on the acquirer')
currency_id = fields.Many2one('res.currency', 'Currency', required=True)
reference = fields.Char(
'Reference', default=lambda self: self.env['ir.sequence'].next_by_code('payment.transaction'),
required=True, help='Internal reference of the TX')
acquirer_reference = fields.Char('Acquirer Reference', help='Reference of the TX as stored in the acquirer database')
('cancel', 'Canceled')],
string='Status', copy=False, default='draft', required=True, readonly=True)
state_message = fields.Text(string='Message', readonly=True,
help='Field used to store error and/or validation messages for information')
amount = fields.Monetary(string='Amount', currency_field='currency_id', required=True, readonly=True)
fees = fields.Monetary(string='Fees', currency_field='currency_id', readonly=True,
help='Fees amount; set by the system because depends on the acquirer')
currency_id = fields.Many2one('res.currency', 'Currency', required=True, readonly=True)
reference = fields.Char(string='Reference', required=True, readonly=True,
help='Internal reference of the TX')
acquirer_reference = fields.Char(string='Acquirer Reference', readonly=True, help='Reference of the TX as stored in the acquirer database')
# duplicate partner / transaction data to store the values at transaction time
partner_id = fields.Many2one('res.partner', 'Customer', track_visibility='onchange')
partner_name = fields.Char('Partner Name')
@@ -523,36 +593,225 @@ class PaymentTransaction(models.Model):
callback_method = fields.Char('Callback Method', groups="base.group_system")
callback_hash = fields.Char('Callback Hash', groups="base.group_system")
payment_token_id = fields.Many2one('payment.token', 'Payment Token', domain="[('acquirer_id', '=', acquirer_id)]")
# Fields used for payment.transaction traceability.
@api.onchange('partner_id')
def _onchange_partner_id(self):
onchange_vals = self.on_change_partner_id(self.partner_id.id).get('value', {})
self.update(onchange_vals)
payment_token_id = fields.Many2one('payment.token', 'Payment Token', readonly=True,
domain="[('acquirer_id', '=', acquirer_id)]")
payment_id = fields.Many2one('account.payment', string='Payment', readonly=True)
invoice_ids = fields.Many2many('account.invoice', 'account_invoice_transaction_rel', 'transaction_id', 'invoice_id',
string='Invoices', copy=False, readonly=True)
invoice_ids_nbr = fields.Integer(compute='_compute_invoice_ids_nbr', string='# of Invoices')
_sql_constraints = [
('reference_uniq', 'unique(reference)', 'Reference must be unique!'),
]
@api.depends('invoice_ids')
def _compute_invoice_ids_nbr(self):
for trans in self:
trans.invoice_ids_nbr = len(trans.invoice_ids)
@api.multi
def on_change_partner_id(self, partner_id):
partner = None
if partner_id:
partner = self.env['res.partner'].browse(partner_id)
return {'value': {
'partner_name': partner and partner.name or False,
'partner_lang': partner and partner.lang or 'en_US',
'partner_email': partner and partner.email or False,
'partner_zip': partner and partner.zip or False,
'partner_address': _partner_format_address(partner and partner.street or '', partner and partner.street2 or ''),
'partner_city': partner and partner.city or False,
'partner_country_id': partner and partner.country_id.id or self._get_default_partner_country_id(),
'partner_phone': partner and partner.phone or False,
}}
return {}
def _prepare_account_payment_vals(self):
self.ensure_one()
return {
'amount': self.amount,
'payment_type': 'inbound' if self.amount > 0 else 'outbound',
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'partner_type': 'customer',
'invoice_ids': [(6, 0, self.invoice_ids.ids)],
'journal_id': self.acquirer_id.journal_id.id,
'company_id': self.acquirer_id.company_id.id,
'payment_method_id': self.env.ref('payment.account_payment_method_electronic_in').id,
'payment_token_id': self.payment_token_id and self.payment_token_id.id or None,
'payment_transaction_id': self.id,
}
@api.constrains('reference', 'state')
def _check_reference(self):
for transaction in self.filtered(lambda tx: tx.state not in ('cancel', 'error')):
if self.search_count([('reference', '=', transaction.reference)]) != 1:
raise exceptions.ValidationError(_('The payment transaction reference must be unique!'))
return True
@api.multi
def get_last_transaction(self):
transactions = self.filtered(lambda t: t.state != 'draft')
return transactions and transactions[0] or transactions
@api.multi
def _get_payment_transaction_sent_message(self):
self.ensure_one()
if self.payment_token_id:
message = _('A transaction %s with %s has been initiated using %s credit card.')
message_vals = (self.reference, self.acquirer_id.name, self.payment_token_id.name)
elif self.provider in ('manual', 'transfer'):
message = _('The customer has selected %s to pay this document.')
message_vals = (self.acquirer_id.name)
else:
message = _('A transaction %s with %s has been initiated.')
message_vals = (self.reference, self.acquirer_id.name)
if self.provider not in ('manual', 'transfer'):
message += ' ' + _('Waiting for payment confirmation...')
return message % message_vals
@api.multi
def _get_payment_transaction_received_message(self):
self.ensure_one()
amount = formatLang(self.env, self.amount, currency_obj=self.currency_id)
message_vals = [self.reference, self.acquirer_id.name, amount]
if self.state == 'pending':
message = _('The transaction %s with %s for %s is pending.')
elif self.state == 'authorized':
message = _('The transaction %s with %s for %s has been authorized. Waiting for capture...')
elif self.state == 'done':
message = _('The transaction %s with %s for %s has been confirmed. The related payment is posted: %s')
message_vals.append(self.payment_id._get_payment_chatter_link())
elif self.state == 'cancel' and self.state_message:
message = _('The transaction %s with %s for %s has been cancelled with the following message: %s')
message_vals.append(self.state_message)
else:
message = _('The transaction %s with %s for %s has been cancelled.')
return message % tuple(message_vals)
@api.multi
def _log_payment_transaction_sent(self):
'''Log the message saying the transaction has been sent to the remote server to be
processed by the acquirer.
'''
for trans in self:
post_message = trans._get_payment_transaction_sent_message()
for inv in trans.invoice_ids:
inv.message_post(body=post_message)
@api.multi
def _log_payment_transaction_received(self):
'''Log the message saying a response has been received from the remote server and some
additional informations like the old/new state, the reference of the payment... etc.
:param old_state: The state of the transaction before the response.
:param add_messages: Optional additional messages to log like the capture status.
'''
for trans in self.filtered(lambda t: t.provider not in ('manual', 'transfer')):
post_message = trans._get_payment_transaction_received_message()
for inv in trans.invoice_ids:
inv.message_post(body=post_message)
@api.multi
def _set_transaction_pending(self):
'''Move the transaction to the pending state(e.g. Wire Transfer).'''
if any(trans.state != 'draft' for trans in self):
raise ValidationError(_('Only draft transaction can be processed.'))
self.write({'state': 'pending', 'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
self._log_payment_transaction_received()
@api.multi
def _set_transaction_authorized(self):
'''Move the transaction to the authorized state(e.g. Authorize).'''
if any(trans.state != 'draft' for trans in self):
raise ValidationError(_('Only draft transaction can be authorized.'))
self.write({'state': 'authorized', 'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
self._log_payment_transaction_received()
@api.multi
def _set_transaction_done(self):
'''Move the transaction's payment to the done state(e.g. Paypal).'''
if any(trans.state not in ('draft', 'authorized') for trans in self):
raise ValidationError(_('Only draft/authorized transaction can be posted.'))
# Validate invoices automatically upon the transaction is posted.
invoices = self.mapped('invoice_ids').filtered(lambda inv: inv.state == 'draft')
invoices.action_invoice_open()
# Create & Post the payments.
payments = self.env['account.payment']
for trans in self:
if trans.payment_id:
payments += trans.payment_id
continue
payment_vals = trans._prepare_account_payment_vals()
payment = payments.create(payment_vals)
payments += payment
# Track the payment to make a one2one.
trans.payment_id = payment
payments.post()
self.write({'state': 'done', 'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
self._log_payment_transaction_received()
@api.multi
def _set_transaction_cancel(self):
'''Move the transaction's payment to the cancel state(e.g. Paypal).'''
if any(trans.state not in ('draft', 'authorized') for trans in self):
raise ValidationError(_('Only draft/authorized transaction can be cancelled.'))
# Cancel the existing payments.
self.mapped('payment_id').cancel()
self.write({'state': 'cancel', 'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
self._log_payment_transaction_received()
@api.model
def _compute_reference_prefix(self, values):
if values and values.get('invoice_ids'):
many_list = self.resolve_2many_commands('invoice_ids', values['invoice_ids'], fields=['number'])
return ','.join(dic['number'] for dic in many_list)
return None
@api.model
def _compute_reference(self, values=None, prefix=None):
'''Compute a unique reference for the transaction.
If prefix:
prefix-\d+
If some invoices:
<inv_number_0>.number,<inv_number_1>,...,<inv_number_n>-x
If some sale orders:
<so_name_0>.number,<so_name_1>,...,<so_name_n>-x
Else:
tx-\d+
:param values: values used to create a new transaction.
:param prefix: custom transaction prefix.
:return: A unique reference for the transaction.
'''
if not prefix:
if values:
prefix = self._compute_reference_prefix(values)
else:
prefix = 'tx'
# Fetch the last reference
# E.g. If the last reference is SO42-5, this query will return '-5'
self._cr.execute('''
SELECT CAST(SUBSTRING(reference FROM '-\d+') AS INTEGER) AS suffix
FROM payment_transaction WHERE reference LIKE %s ORDER BY suffix
''', [prefix + '%'])
query_res = self._cr.fetchone()
if query_res:
# Increment the last reference by one
suffix = '%s' % (-query_res[0] + 1)
else:
# Start a new indexing from 1
suffix = '1'
return '%s-%s' % (prefix, suffix)
@api.multi
def action_view_invoices(self):
action = {
'name': _('Invoices'),
'type': 'ir.actions.act_window',
'res_model': 'account.invoice',
'target': 'current',
}
invoice_ids = self.invoice_ids.ids
if len(invoice_ids) == 1:
invoice = invoice_ids[0]
action['res_id'] = invoice.id
action['view_mode'] = 'form'
action['views'] = [(self.env.ref('account.invoice_form').id, 'form')]
else:
action['view_mode'] = 'tree,form'
action['domain'] = [('id', 'in', invoice_ids)]
return action
@api.constrains('state', 'acquirer_id')
def _check_authorize_state(self):
@@ -562,29 +821,38 @@ class PaymentTransaction(models.Model):
@api.model
def create(self, values):
if values.get('partner_id'): # @TDENOTE: not sure
values.update(self.on_change_partner_id(values['partner_id'])['value'])
# call custom create method if defined (i.e. ogone_create for ogone)
if values.get('acquirer_id'):
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
partner = self.env['res.partner'].browse(values['partner_id'])
# compute fees
custom_method_name = '%s_compute_fees' % acquirer.provider
if hasattr(acquirer, custom_method_name):
fees = getattr(acquirer, custom_method_name)(
values.get('amount', 0.0), values.get('currency_id'), values.get('partner_country_id'))
values['fees'] = float_round(fees, 2)
values.update({
'partner_name': partner.name,
'partner_lang': partner.lang or 'en_US',
'partner_email': partner.email,
'partner_zip': partner.zip,
'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''),
'partner_city': partner.city,
'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(),
'partner_phone': partner.phone,
})
# custom create
custom_method_name = '%s_create' % acquirer.provider
if hasattr(acquirer, custom_method_name):
values.update(getattr(self, custom_method_name)(values))
# compute fees
custom_method_name = '%s_compute_fees' % acquirer.provider
if hasattr(acquirer, custom_method_name):
fees = getattr(acquirer, custom_method_name)(
values.get('amount', 0.0), values.get('currency_id'), partner.country_id.id)
values['fees'] = fees
# custom create
custom_method_name = '%s_create' % acquirer.provider
if hasattr(acquirer, custom_method_name):
values.update(getattr(self, custom_method_name)(values))
if not values.get('reference'):
values['reference'] = self._compute_reference(values=values)
# Default value of reference is
tx = super(PaymentTransaction, self).create(values)
if not values.get('reference'):
tx.write({'reference': str(tx.id)})
# Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff
# (limited sudo env for checking callback presence, must work for manual transactions too)
@@ -594,38 +862,6 @@ class PaymentTransaction(models.Model):
return tx
@api.multi
def write(self, values):
if ('acquirer_id' in values or 'amount' in values) and 'fees' not in values:
# The acquirer or the amount has changed, and the fees are not explicitly forced. Fees must be recomputed.
acquirer = None
if values.get('acquirer_id'):
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
for tx in self:
vals = dict(values, fees=0.0)
if not acquirer:
acquirer = tx.acquirer_id
custom_method_name = '%s_compute_fees' % acquirer.provider
# TDE FIXME: shouldn't we use fee_implemented ?
if hasattr(acquirer, custom_method_name):
fees = getattr(acquirer, custom_method_name)(
(values['amount'] if 'amount' in values else tx.amount) or 0.0,
values.get('currency_id') or tx.currency_id.id,
values.get('partner_country_id') or tx.partner_country_id.id)
vals['fees'] = float_round(fees, 2)
res = super(PaymentTransaction, tx).write(vals)
return res
return super(PaymentTransaction, self).write(values)
@api.model
def get_next_reference(self, reference):
ref_suffix = 1
init_ref = reference
while self.env['payment.transaction'].sudo().search_count([('reference', '=', reference)]):
reference = init_ref + 'x' + str(ref_suffix)
ref_suffix += 1
return reference
def _generate_callback_hash(self):
self.ensure_one()
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
@@ -638,27 +874,6 @@ class PaymentTransaction(models.Model):
# FORM RELATED METHODS
# --------------------------------------------------
@api.multi
def render(self):
values = {
'reference': self.reference,
'amount': self.amount,
'currency_id': self.currency_id.id,
'currency': self.currency_id,
'partner': self.partner_id,
'partner_name': self.partner_name,
'partner_lang': self.partner_lang,
'partner_email': self.partner_email,
'partner_zip': self.partner_zip,
'partner_address': self.partner_address,
'partner_city': self.partner_city,
'partner_country_id': self.partner_country_id.id,
'partner_country': self.partner_country_id,
'partner_phone': self.partner_phone,
'partner_state': None,
}
return self.acquirer_id.render(None, None, None, values=values)
@api.model
def form_feedback(self, data, acquirer_name):
invalid_parameters, tx = None, None
@@ -697,8 +912,10 @@ class PaymentTransaction(models.Model):
@api.multi
def s2s_do_transaction(self, **kwargs):
custom_method_name = '%s_s2s_do_transaction' % self.acquirer_id.provider
if hasattr(self, custom_method_name):
return getattr(self, custom_method_name)(**kwargs)
for trans in self:
trans._log_payment_transaction_sent()
if hasattr(trans, custom_method_name):
return getattr(trans, custom_method_name)(**kwargs)
@api.multi
def s2s_do_refund(self, **kwargs):
@@ -750,15 +967,15 @@ class PaymentTransaction(models.Model):
@api.multi
def action_capture(self):
if any(self.mapped(lambda tx: tx.state != 'authorized')):
raise ValidationError(_('Only transactions in the Authorized status can be captured.'))
if any([t.state != 'authorized' for t in self]):
raise ValidationError(_('Only transactions having the capture status can be captured.'))
for tx in self:
tx.s2s_capture_transaction()
@api.multi
def action_void(self):
if any(self.mapped(lambda tx: tx.state != 'authorized')):
raise ValidationError(_('Only transactions in the Authorized status can be voided.'))
if any([t.state != 'authorized' for t in self]):
raise ValidationError(_('Only transactions having the capture status can be voided.'))
for tx in self:
tx.s2s_void_transaction()
@@ -838,7 +1055,7 @@ class PaymentToken(models.Model):
_logger.error("Error 'EUR' currency not found for payment method validation!")
return False
reference = "VALIDATION-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
reference = "VALIDATION-%s-%s" % (self.id, datetime.now().strftime('%y%m%d_%H%M%S'))
tx = self.env['payment.transaction'].sudo().create({
'amount': amount,
'acquirer_id': self.acquirer_id.id,
@@ -153,6 +153,7 @@ odoo.define('payment.payment_form', function (require) {
'success_url': self.options.successUrl,
'error_url': self.options.errorUrl,
'callback_method': self.options.callbackMethod,
'order_id': self.options.orderId,
}).then(function (result) {
if (result) {
// if the server sent us the html form, we create a form element
+2 -2
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
from odoo.tests import common
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
class PaymentAcquirerCommon(common.TransactionCase):
class PaymentAcquirerCommon(AccountingTestCase):
def setUp(self):
super(PaymentAcquirerCommon, self).setUp()
@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="account_invoice_view_form_inherit_payment" model="ir.ui.view">
<field name="name">account.invoice.view.form.inherit.payment</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<!--
The user must capture/void the authorized transactions before registering a new payment.
-->
<xpath expr="//button[@id='account_invoice_payment_btn']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('state', '!=', 'open'), ('authorized_transaction_ids', '!=', [])]}</attribute>
</xpath>
<xpath expr="//button[@id='account_invoice_payment_btn']" position="after">
<field name="authorized_transaction_ids" invisible="1"/>
<button name="payment_action_capture" type="object"
string="Capture Transaction" class="oe_highlight"
attrs="{'invisible': ['|', ('state', '!=', 'open'), ('authorized_transaction_ids', '=', [])]}"/>
<button name="payment_action_void" type="object"
string="Void Transaction"
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."
attrs="{'invisible': ['|', ('state', '!=', 'open'), ('authorized_transaction_ids', '=', [])]}"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -113,29 +113,33 @@
</template>
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<!--
Inform the portal user about the transaction status.
To be called with the 'payment_tx_id' value.
-->
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id.state_message">
<span t-esc="payment_tx_id.state_message"/>
</div>
</template>
</data>
+3 -1
View File
@@ -28,10 +28,12 @@
t-att-data-error-url="error_url or ''"
t-att-data-access-token="access_token or ''"
t-att-data-partner-id="partner_id"
t-att-data-callback-method="callback_method or ''">
t-att-data-callback-method="callback_method or ''"
t-att-data-order-id="order_id or ''">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" t-if="verify_validity==True" name="verify_validity" value="1"/>
<input type="hidden" t-if="prepare_tx_url" name="prepare_tx_url" t-att-value="prepare_tx_url"/>
<input type="hidden" t-if="order_id" name="order_id" t-att-value="order_id"/>
<!-- s2s form submission -->
<input type="hidden" t-if="access_token" name="access_token" t-att-value="access_token"/>
<input type="hidden" t-if="success_url" name="success_url" t-att-value="success_url"/>
+16 -10
View File
@@ -20,6 +20,7 @@
<sheet>
<field name="module_id" invisible="1"/>
<field name="module_state" invisible="1"/>
<field name="inbound_payment_method_ids" invisible="1"/>
<div class="oe_button_box" name="button_box" attrs="{'invisible': ['&amp;', ('module_id', '!=', False), ('module_state', '!=', 'installed')]}">
<!-- transfer referenced here due to view inheritance issue in current master (post-saas-16) -->
<button name="toggle_environment_value"
@@ -81,7 +82,7 @@
</page>
<page string="Configuration">
<group name="acquirer_config">
<field name="journal_id" context="{'default_type': 'bank'}" required="1"/>
<field name="journal_id" context="{'default_type': 'bank'}"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
@@ -215,16 +216,25 @@
<field name="name">payment.transaction.form</field>
<field name="model">payment.transaction</field>
<field name="arch" type="xml">
<form string="Payment Transactions">
<form string="Payment Transactions" create="false" edit="false">
<header>
<button type="object" name="action_capture" states="authorized" string="Capture Transaction" class="oe_highlight"/>
<button type="object" name="action_void" states="authorized" string="Void Transaction"/>
<field name="state" widget="statusbar" options="{'clickable': '1'}"/>
<button type="object" name="action_void" states="authorized" string="Void Transaction"
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_invoices" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('invoice_ids_nbr', '=', 0)]}">
<field name="invoice_ids_nbr" widget="statinfo" string="Invoice(s)"/>
</button>
</div>
<group>
<group>
<field name="reference"/>
<field name="payment_id"/>
<label for="amount"/>
<div class="o_row">
<field name="amount"/>
@@ -250,8 +260,7 @@
<field name="provider" invisible="1"/>
<field name="payment_token_id" options="{'no_create': True}"/>
<field name="acquirer_reference" readonly="1"/>
<field name="create_date"/>
<field name="date_validate"/>
<field name="date"/>
</group>
</group>
<group string="Message">
@@ -266,7 +275,7 @@
<field name="name">payment.transaction.list</field>
<field name="model">payment.transaction</field>
<field name="arch" type="xml">
<tree string="Payment Transactions">
<tree string="Payment Transactions" create="false">
<field name="reference"/>
<field name="create_date"/>
<field name="acquirer_id"/>
@@ -291,9 +300,6 @@
<div class="col-xs-6">
<strong><field name="reference"/></strong>
</div>
<div class="col-xs-6">
<span class="pull-right"><field name="create_date"/></span>
</div>
<div class="col-xs-6">
<span><field name="partner_name"/></span>
</div>
+1
View File
@@ -3,3 +3,4 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -13,4 +13,5 @@
'data/payment_acquirer_data.xml',
],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+6 -14
View File
@@ -217,24 +217,16 @@ class TxAdyen(models.Model):
def _adyen_form_validate(self, data):
status = data.get('authResult', 'PENDING')
if status == 'AUTHORISED':
self.write({
'state': 'done',
'acquirer_reference': data.get('pspReference'),
# 'date_validate': data.get('payment_date', fields.datetime.now()),
# 'paypal_txn_type': data.get('express_checkout')
})
self.write({'acquirer_reference': data.get('pspReference')})
self._set_transaction_done()
return True
elif status == 'PENDING':
self.write({
'state': 'pending',
'acquirer_reference': data.get('pspReference'),
})
self.write({'acquirer_reference': data.get('pspReference')})
self._set_transaction_pending()
return True
else:
error = _('Adyen: feedback error')
_logger.info(error)
self.write({
'state': 'error',
'state_message': error
})
self.write({'state_message': error})
self._set_transaction_cancel()
return False
+2
View File
@@ -5,6 +5,7 @@ from lxml import objectify
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.addons.payment_adyen.controllers.main import AdyenController
from werkzeug import urls
import odoo.tests
class AdyenCommon(PaymentAcquirerCommon):
@@ -29,6 +30,7 @@ class AdyenCommon(PaymentAcquirerCommon):
self.mistercash = (('6703444444444449', None, 'user', 'password'))
@odoo.tests.tagged('post_install', '-at_install', 'external', '-standard')
class AdyenForm(AdyenCommon):
def test_10_adyen_form_render(self):
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -13,4 +13,5 @@
'data/payment_acquirer_data.xml',
],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
@@ -277,7 +277,7 @@ class AuthorizeAPI():
payment_profile = etree.SubElement(profile, "paymentProfile")
etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
order = etree.SubElement(tx, "order")
etree.SubElement(order, "invoiceNumber").text = reference
etree.SubElement(order, "invoiceNumber").text = reference[:20]
response = self._authorize_request(root)
res = dict()
(has_error, error_msg) = error_check(response)
@@ -312,7 +312,7 @@ class AuthorizeAPI():
payment_profile = etree.SubElement(profile, "paymentProfile")
etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
order = etree.SubElement(tx, "order")
etree.SubElement(order, "invoiceNumber").text = reference
etree.SubElement(order, "invoiceNumber").text = reference[:20]
response = self._authorize_request(root)
res = dict()
(has_error, error_msg) = error_check(response)
+27 -54
View File
@@ -13,6 +13,7 @@ from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
from odoo.tools.float_utils import float_compare, float_repr
from odoo.tools.safe_eval import safe_eval
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -158,17 +159,6 @@ class TxAuthorize(models.Model):
# FORM RELATED METHODS
# --------------------------------------------------
@api.model
def create(self, vals):
# The reference is used in the Authorize form to fill a field (invoiceNumber) which is
# limited to 20 characters. We truncate the reference now, since it will be reused at
# payment validation to find back the transaction.
if 'reference' in vals and 'acquirer_id' in vals:
acquier = self.env['payment.acquirer'].browse(vals['acquirer_id'])
if acquier.provider == 'authorize':
vals['reference'] = vals.get('reference', '')[:20]
return super(TxAuthorize, self).create(vals)
@api.model
def _authorize_form_get_tx_from_data(self, data):
""" Given a data dict coming from authorize, verify it and find the related
@@ -202,22 +192,20 @@ class TxAuthorize(models.Model):
@api.multi
def _authorize_form_validate(self, data):
if self.state in ['done', 'refunded']:
if self.state == 'done':
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
return True
status_code = int(data.get('x_response_code', '0'))
if status_code == self._authorize_valid_tx_status:
if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
self.write({
'state': 'done',
'acquirer_reference': data.get('x_trans_id'),
'date_validate': fields.Datetime.now(),
'date': fields.Datetime.now(),
})
self._set_transaction_done()
elif data.get('x_type').lower() in ['auth_only']:
self.write({
'state': 'authorized',
'acquirer_reference': data.get('x_trans_id'),
})
self.write({'acquirer_reference': data.get('x_trans_id')})
self._set_transaction_authorized()
if self.partner_id and not self.payment_token_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
transaction = AuthorizeAPI(self.acquirer_id)
@@ -235,32 +223,35 @@ class TxAuthorize(models.Model):
self.payment_token_id.verified = True
return True
elif status_code == self._authorize_pending_tx_status:
self.write({
'state': 'pending',
'acquirer_reference': data.get('x_trans_id'),
})
self.write({'acquirer_reference': data.get('x_trans_id')})
self._set_transaction_pending()
return True
elif status_code == self._authorize_cancel_tx_status:
self.write({
'state': 'cancel',
'acquirer_reference': data.get('x_trans_id'),
'state_message': data.get('x_response_reason_text'),
})
self._set_transaction_cancel()
return True
else:
error = data.get('x_response_reason_text')
_logger.info(error)
self.write({
'state': 'error',
'state_message': error,
'acquirer_reference': data.get('x_trans_id'),
})
self._set_transaction_cancel()
return False
@api.multi
def authorize_s2s_do_transaction(self, **data):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
if not self.payment_token_id.authorize_profile:
raise UserError(_('Invalid token found: the Authorize profile is missing.'
'Please make sure the token has a valid acquirer reference.'))
if not self.acquirer_id.capture_manually:
res = transaction.auth_and_capture(self.payment_token_id, self.amount, self.reference)
else:
@@ -271,11 +262,7 @@ class TxAuthorize(models.Model):
def authorize_s2s_do_refund(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
self.state = 'refunding'
if self.type == 'validation':
res = transaction.void(self.acquirer_reference)
else:
res = transaction.credit(self.payment_token_id, self.amount, self.acquirer_reference)
res = transaction.credit(self.payment_token_id, self.amount, self.acquirer_reference)
return self._authorize_s2s_validate_tree(res)
@api.multi
@@ -298,7 +285,7 @@ class TxAuthorize(models.Model):
@api.multi
def _authorize_s2s_validate(self, tree):
if self.state in ['done', 'refunded']:
if self.state == 'done':
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
return True
status_code = int(tree.get('x_response_code', '0'))
@@ -306,9 +293,8 @@ class TxAuthorize(models.Model):
if tree.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
init_state = self.state
self.write({
'state': 'done',
'acquirer_reference': tree.get('x_trans_id'),
'date_validate': fields.Datetime.now(),
'date': fields.Datetime.now(),
})
if init_state != 'authorized':
self.execute_callback()
@@ -316,43 +302,30 @@ class TxAuthorize(models.Model):
if self.payment_token_id:
self.payment_token_id.verified = True
self._set_transaction_done()
if tree.get('x_type').lower() == 'auth_only':
self.write({
'state': 'authorized',
'acquirer_reference': tree.get('x_trans_id'),
})
self.write({'acquirer_reference': tree.get('x_trans_id')})
self._set_transaction_authorized()
self.execute_callback()
if tree.get('x_type').lower() == 'void':
if self.type == 'validation' and self.state == 'refunding':
self.write({
'state': 'refunded',
})
else:
self.write({
'state': 'cancel',
})
self._set_transaction_cancel()
return True
elif status_code == self._authorize_pending_tx_status:
new_state = 'refunding' if self.state == 'refunding' else 'pending'
self.write({
'state': new_state,
'acquirer_reference': tree.get('x_trans_id'),
})
self.write({'acquirer_reference': tree.get('x_trans_id')})
self._set_transaction_pending()
return True
elif status_code == self._authorize_cancel_tx_status:
self.write({
'state': 'cancel',
'acquirer_reference': tree.get('x_trans_id'),
})
self.write({'acquirer_reference': tree.get('x_trans_id')})
self._set_transaction_cancel()
return True
else:
error = tree.get('x_response_reason_text')
_logger.info(error)
self.write({
'state': 'error',
'state_message': error,
'acquirer_reference': tree.get('x_trans_id'),
})
self._set_transaction_cancel()
return False
@@ -26,7 +26,7 @@ class AuthorizeCommon(PaymentAcquirerCommon):
# self.authorize.auto_confirm = 'confirm_so'
@odoo.tests.tagged('post_install', '-at_install')
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'external')
class AuthorizeForm(AuthorizeCommon):
def _authorize_generate_hashing(self, values):
@@ -161,23 +161,31 @@ class AuthorizeForm(AuthorizeCommon):
'amount': 320.0,
'acquirer_id': self.authorize.id,
'currency_id': self.currency_usd.id,
'reference': 'SO004',
'reference': 'SO004-1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
tx._set_transaction_done()
# validate it
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
# check state
self.assertEqual(tx.state, 'done', 'Authorize: validation did not put tx into done state')
self.assertEqual(tx.acquirer_reference, authorize_post_data.get('x_trans_id'), 'Authorize: validation did not update tx payid')
# reset tx
tx.write({'state': 'draft', 'date_validate': False, 'acquirer_reference': False})
tx = self.env['payment.transaction'].create({
'amount': 320.0,
'acquirer_id': self.authorize.id,
'currency_id': self.currency_usd.id,
'reference': 'SO004-2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
tx._set_transaction_done()
# simulate an error
authorize_post_data['x_response_code'] = u'3'
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
# check state
self.assertEqual(tx.state, 'error', 'Authorize: erroneous validation did not put tx into error state')
self.assertEqual(tx.state, 'cancel', 'Authorize: erroneous validation did not put tx into error state')
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'external')
@@ -270,4 +278,4 @@ class AuthorizeForm(AuthorizeCommon):
})
transaction.authorize_s2s_do_transaction()
self.assertEqual(transaction.state, 'error')
self.assertEqual(transaction.state, 'cancel')
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -13,4 +13,5 @@
'data/payment_acquirer_data.xml',
],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+7 -13
View File
@@ -168,29 +168,23 @@ class TxBuckaroo(models.Model):
data = normalize_keys_upper(data)
status_code = int(data.get('BRQ_STATUSCODE', '0'))
if status_code in self._buckaroo_valid_tx_status:
self.write({
'state': 'done',
'acquirer_reference': data.get('BRQ_TRANSACTIONS'),
})
self.write({'acquirer_reference': data.get('BRQ_TRANSACTIONS')})
self._set_transaction_done()
return True
elif status_code in self._buckaroo_pending_tx_status:
self.write({
'state': 'pending',
'acquirer_reference': data.get('BRQ_TRANSACTIONS'),
})
self.write({'acquirer_reference': data.get('BRQ_TRANSACTIONS')})
self._set_transaction_pending()
return True
elif status_code in self._buckaroo_cancel_tx_status:
self.write({
'state': 'cancel',
'acquirer_reference': data.get('BRQ_TRANSACTIONS'),
})
self.write({'acquirer_reference': data.get('BRQ_TRANSACTIONS')})
self._set_transaction_cancel()
return True
else:
error = 'Buckaroo: feedback error'
_logger.info(error)
self.write({
'state': 'error',
'state_message': error,
'acquirer_reference': data.get('BRQ_TRANSACTIONS'),
})
self._set_transaction_cancel()
return False
+11 -5
View File
@@ -10,7 +10,7 @@ from odoo.addons.payment_buckaroo.controllers.main import BuckarooController
from odoo.tools import mute_logger
@odoo.tests.tagged('-post_install', '-at_install')
@odoo.tests.tagged('post_install', '-at_install', 'external', '-standard')
class BuckarooCommon(PaymentAcquirerCommon):
def setUp(self):
@@ -19,7 +19,7 @@ class BuckarooCommon(PaymentAcquirerCommon):
self.buckaroo = self.env.ref('payment.payment_acquirer_buckaroo')
@odoo.tests.tagged('-post_install', '-at_install')
@odoo.tests.tagged('post_install', '-at_install', 'external', '-standard')
class BuckarooForm(BuckarooCommon):
def test_10_Buckaroo_form_render(self):
@@ -134,7 +134,7 @@ class BuckarooForm(BuckarooCommon):
'amount': 2240.0,
'acquirer_id': self.buckaroo.id,
'currency_id': self.currency_euro.id,
'reference': 'SO004',
'reference': 'SO004-1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
@@ -145,7 +145,13 @@ class BuckarooForm(BuckarooCommon):
self.assertEqual(tx.acquirer_reference, buckaroo_post_data.get('BRQ_TRANSACTIONS'), 'Buckaroo: validation did not update tx payid')
# reset tx
tx.write({'state': 'draft', 'date_validate': False, 'acquirer_reference': False})
tx = self.env['payment.transaction'].create({
'amount': 2240.0,
'acquirer_id': self.buckaroo.id,
'currency_id': self.currency_euro.id,
'reference': 'SO004-2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# now buckaroo post is ok: try to modify the SHASIGN
buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a'
@@ -158,4 +164,4 @@ class BuckarooForm(BuckarooCommon):
tx.form_feedback(buckaroo_post_data, 'buckaroo')
# check state
self.assertEqual(tx.state, 'error', 'Buckaroo: erroneous validation did not put tx into error state')
self.assertEqual(tx.state, 'cancel', 'Buckaroo: erroneous validation did not put tx into error state')
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -13,4 +13,5 @@
'data/payment_acquirer_data.xml',
],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+15 -29
View File
@@ -283,15 +283,14 @@ class PaymentTxOgone(models.Model):
return invalid_parameters
def _ogone_form_validate(self, data):
if self.state in ['done', 'refunding', 'refunded']:
if self.state != 'draft':
_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 = {
'state': 'done',
'date_validate': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT),
'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 \
@@ -307,6 +306,7 @@ class PaymentTxOgone(models.Model):
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':
@@ -314,15 +314,11 @@ class PaymentTxOgone(models.Model):
return True
elif status in self._ogone_cancel_tx_status:
self.write({
'state': 'cancel',
'acquirer_reference': data.get('PAYID'),
})
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({
'state': 'pending',
'acquirer_reference': data.get('PAYID'),
})
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'),
@@ -331,10 +327,10 @@ class PaymentTxOgone(models.Model):
}
_logger.info(error)
self.write({
'state': 'error',
'state_message': error,
'acquirer_reference': data.get('PAYID'),
})
self._set_transaction_cancel()
return False
# --------------------------------------------------
@@ -397,12 +393,6 @@ class PaymentTxOgone(models.Model):
return self._ogone_s2s_validate_tree(tree)
def ogone_s2s_do_refund(self, **kwargs):
# we refund only if this transaction hasn't been already refunded and was paid.
if self.state != 'done':
return False
self.state = 'refunding'
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)
@@ -441,16 +431,14 @@ class PaymentTxOgone(models.Model):
return self._ogone_s2s_validate_tree(tree)
def _ogone_s2s_validate_tree(self, tree, tries=2):
if self.state not in ('draft', 'pending', 'refunding'):
if self.state != 'draft':
_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:
new_state = 'refunded' if self.state == 'refunding' else 'done'
self.write({
'state': new_state,
'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
'date': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': tree.get('PAYID'),
})
if tree.get('ALIAS') and self.partner_id and \
@@ -465,25 +453,23 @@ class PaymentTxOgone(models.Model):
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({
'state': 'cancel',
'acquirer_reference': tree.get('PAYID'),
})
self.write({'acquirer_reference': tree.get('PAYID')})
self._set_transaction_cancel()
elif status in self._ogone_pending_tx_status:
new_state = 'refunding' if self.state == 'refunding' else 'pending'
vals = {
'state': new_state,
'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_done()
elif status in self._ogone_wait_tx_status and tries > 0:
time.sleep(0.5)
self.write({'acquirer_reference': tree.get('PAYID')})
@@ -497,10 +483,10 @@ class PaymentTxOgone(models.Model):
}
_logger.info(error)
self.write({
'state': 'error',
'state_message': error,
'acquirer_reference': tree.get('PAYID'),
})
self._set_transaction_cancel()
return False
def _ogone_s2s_get_tx_status(self):
+9 -3
View File
@@ -128,7 +128,7 @@ class OgonePayment(PaymentAcquirerCommon):
'amount': 1.95,
'acquirer_id': self.ogone.id,
'currency_id': self.currency_euro.id,
'reference': 'test_ref_2',
'reference': 'test_ref_2-1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# validate it
@@ -138,7 +138,13 @@ class OgonePayment(PaymentAcquirerCommon):
self.assertEqual(tx.ogone_payid, ogone_post_data.get('PAYID'), 'ogone: validation did not update tx payid')
# reset tx
tx.write({'state': 'draft', 'date_validate': False, 'ogone_payid': False})
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'
@@ -150,7 +156,7 @@ class OgonePayment(PaymentAcquirerCommon):
ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691'
tx.form_feedback(ogone_post_data)
# check state
self.assertEqual(tx.state, 'error', 'ogone: erroneous validation did not put tx into error 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()
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -13,4 +13,5 @@
'data/payment_acquirer_data.xml',
],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+8 -5
View File
@@ -203,17 +203,20 @@ class TxPaypal(models.Model):
'PST': -8 * 3600,
'PDT': -7 * 3600,
}
date_validate = dateutil.parser.parse(data.get('payment_date'), tzinfos=tzinfos).astimezone(pytz.utc)
date = dateutil.parser.parse(data.get('payment_date'), tzinfos=tzinfos).astimezone(pytz.utc)
except:
date_validate = fields.Datetime.now()
res.update(state='done', date_validate=date_validate)
date = fields.Datetime.now()
res.update(date=date)
self._set_transaction_done()
return self.write(res)
elif status in ['Pending', 'Expired']:
_logger.info('Received notification for Paypal payment %s: set as pending' % (self.reference))
res.update(state='pending', state_message=data.get('pending_reason', ''))
res.update(state_message=data.get('pending_reason', ''))
self._set_transaction_pending()
return self.write(res)
else:
error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (self.reference, status)
_logger.info(error)
res.update(state='error', state_message=error)
res.update(state_message=error)
self._set_transaction_cancel()
return self.write(res)
+4 -2
View File
@@ -6,6 +6,7 @@ from odoo.addons.payment_paypal.controllers.main import PaypalController
from werkzeug import urls
from odoo.tools import mute_logger
from odoo.tests import tagged
from lxml import objectify
@@ -30,6 +31,7 @@ class PaypalCommon(PaymentAcquirerCommon):
self.switch_polo = (('6331101999990016', '123'))
@tagged('post_install', '-at_install', 'external', '-standard')
class PaypalForm(PaypalCommon):
def test_10_paypal_form_render(self):
@@ -180,7 +182,7 @@ class PaypalForm(PaypalCommon):
self.assertEqual(tx.state, 'pending', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.state_message, 'multi_currency', 'paypal: wrong state message after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertFalse(tx.date_validate, 'paypal: validation date should not be updated whenr receiving pending notification')
self.assertFalse(tx.date, 'paypal: validation date should not be updated whenr receiving pending notification')
# update tx
tx.write({
@@ -194,4 +196,4 @@ class PaypalForm(PaypalCommon):
# check
self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertEqual(tx.date_validate, '2013-11-18 11:21:19', 'paypal: wrong validation date')
self.assertEqual(tx.date, '2013-11-18 11:21:19', 'paypal: wrong validation date')
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -16,4 +16,5 @@
'views/payment_payumoney_templates.xml',
'data/payment_acquirer_data.xml',
],
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+11 -28
View File
@@ -125,31 +125,14 @@ class PaymentTransactionPayumoney(models.Model):
@api.multi
def _payumoney_form_validate(self, data):
status = data.get('status')
transaction_status = {
'success': {
'state': 'done',
'acquirer_reference': data.get('payuMoneyId'),
'date_validate': fields.Datetime.now(),
},
'pending': {
'state': 'pending',
'acquirer_reference': data.get('payuMoneyId'),
'date_validate': fields.Datetime.now(),
},
'failure': {
'state': 'cancel',
'acquirer_reference': data.get('payuMoneyId'),
'date_validate': fields.Datetime.now(),
},
'error': {
'state': 'error',
'state_message': data.get('error_Message') or _('PayUmoney: feedback error'),
'acquirer_reference': data.get('payuMoneyId'),
'date_validate': fields.Datetime.now(),
}
}
vals = transaction_status.get(status, False)
if not vals:
vals = transaction_status['error']
_logger.info(vals['state_message'])
return self.write(vals)
result = self.write({
'acquirer_reference': data.get('payuMoneyId'),
'date': fields.Datetime.now(),
})
if status == 'success':
self._set_transaction_done()
elif status != 'pending':
self._set_transaction_cancel()
else:
self._set_transaction_pending()
return result
+2
View File
@@ -1,2 +1,4 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -20,4 +20,5 @@ payments acquirer using Worldline SIPS.""",
'data/payment_acquirer_data.xml',
],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+22 -9
View File
@@ -177,7 +177,7 @@ class TxSips(models.Model):
data = {
'acquirer_reference': data.get('transactionReference'),
'partner_reference': data.get('customerId'),
'date_validate': data.get('transactionDateTime',
'date': data.get('transactionDateTime',
fields.Datetime.now())
}
res = False
@@ -185,33 +185,46 @@ class TxSips(models.Model):
msg = 'Payment for tx ref: %s, got response [%s], set as done.' % \
(self.reference, status)
_logger.info(msg)
data.update(state='done', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_done()
res = True
elif status in self._sips_error_tx_status:
msg = 'Payment for tx ref: %s, got response [%s], set as ' \
'error.' % (self.reference, status)
data.update(state='error', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
elif status in self._sips_wait_tx_status:
msg = 'Received wait status for payment ref: %s, got response ' \
'[%s], set as error.' % (self.reference, status)
data.update(state='error', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
elif status in self._sips_refused_tx_status:
msg = 'Received refused status for payment ref: %s, got response' \
' [%s], set as error.' % (self.reference, status)
data.update(state='error', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
elif status in self._sips_pending_tx_status:
msg = 'Payment ref: %s, got response [%s] set as pending.' \
% (self.reference, status)
data.update(state='pending', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_pending()
elif status in self._sips_cancel_tx_status:
msg = 'Received notification for payment ref: %s, got response ' \
'[%s], set as cancel.' % (self.reference, status)
data.update(state='cancel', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
else:
msg = 'Received unrecognized status for payment ref: %s, got ' \
'response [%s], set as error.' % (self.reference, status)
data.update(state='error', state_message=msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
_logger.info(msg)
self.write(data)
return res
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -14,4 +14,5 @@
],
'images': ['static/description/icon.png'],
'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+5 -6
View File
@@ -191,18 +191,17 @@ class PaymentTransactionStripe(models.Model):
@api.multi
def _stripe_s2s_validate_tree(self, tree):
self.ensure_one()
if self.state not in ('draft', 'pending', 'refunding'):
if self.state != 'draft':
_logger.info('Stripe: trying to validate an already validated tx (ref %s)', self.reference)
return True
status = tree.get('status')
if status == 'succeeded':
new_state = 'refunded' if self.state == 'refunding' else 'done'
self.write({
'state': new_state,
'date_validate': fields.datetime.now(),
'date': fields.datetime.now(),
'acquirer_reference': tree.get('id'),
})
self._set_transaction_done()
self.execute_callback()
if self.payment_token_id:
self.payment_token_id.verified = True
@@ -211,11 +210,11 @@ class PaymentTransactionStripe(models.Model):
error = tree['error']['message']
_logger.warn(error)
self.sudo().write({
'state': 'error',
'state_message': error,
'acquirer_reference': tree.get('id'),
'date_validate': fields.datetime.now(),
'date': fields.datetime.now(),
})
self._set_transaction_cancel()
return False
@api.multi
+9 -3
View File
@@ -146,7 +146,7 @@ class StripeTest(StripeCommon):
'amount': 4700,
'acquirer_id': self.stripe.id,
'currency_id': self.currency_euro.id,
'reference': 'SO100',
'reference': 'SO100-1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
@@ -155,11 +155,17 @@ class StripeTest(StripeCommon):
self.assertEqual(tx.state, 'done', 'Stripe: validation did not put tx into done state')
self.assertEqual(tx.acquirer_reference, stripe_post_data.get('id'), 'Stripe: validation did not update tx id')
# reset tx
tx.write({'state': 'draft', 'date_validate': False, 'acquirer_reference': False})
tx = self.env['payment.transaction'].create({
'amount': 4700,
'acquirer_id': self.stripe.id,
'currency_id': self.currency_euro.id,
'reference': 'SO100-2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# simulate an error
stripe_post_data['status'] = 'error'
stripe_post_data.update({u'error': {u'message': u"Your card's expiration year is invalid.", u'code': u'invalid_expiry_year', u'type': u'card_error', u'param': u'exp_year'}})
with mute_logger('odoo.addons.payment_stripe.models.payment'):
tx.form_feedback(stripe_post_data, 'stripe')
# check state
self.assertEqual(tx.state, 'error', 'Stipe: erroneous validation did not put tx into error state')
self.assertEqual(tx.state, 'cancel', 'Stipe: erroneous validation did not put tx into error state')
+2
View File
@@ -3,3 +3,5 @@
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+1
View File
@@ -14,4 +14,5 @@
],
'installable': True,
'auto_install': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
}
+15 -1
View File
@@ -15,6 +15,19 @@ class TransferPaymentAcquirer(models.Model):
provider = fields.Selection(selection_add=[('transfer', 'Wire Transfer')], default='transfer')
@api.model
def _create_missing_journal_for_acquirers(self):
# By default, the wire transfer method uses the default Bank journal.
company = self.env.user.company_id
acquirers = self.env['payment.acquirer'].search(
[('provider', '=', 'transfer'), ('journal_id', '=', False), ('company_id', '=', company.id)])
bank_journal = self.env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', company.id)], limit=1)
if bank_journal:
acquirers.write({'journal_id': bank_journal.id})
return super(TransferPaymentAcquirer, self)._create_missing_journal_for_acquirers()
def transfer_get_form_action_url(self):
return '/payment/transfer/feedback'
@@ -85,4 +98,5 @@ class TransferPaymentTransaction(models.Model):
def _transfer_form_validate(self, data):
_logger.info('Validated transfer payment for tx %s: set as pending' % (self.reference))
return self.write({'state': 'pending'})
self._set_transaction_pending()
return True
@@ -347,7 +347,9 @@
</div>
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div id="sales_settings_invoicing_policy"
class="col-xs-12 col-md-6 o_setting_box"
title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div class="o_setting_right_pane">
<label for="default_invoice_policy"/>
<div class="text-muted">
+2 -3
View File
@@ -16,11 +16,10 @@ Provide tools for sale-related payment
""",
'depends': ['payment', 'sale'],
'data': [
'views/payment_views.xml',
'views/crm_team_views.xml',
'views/sale_order_views.xml',
'views/sale_portal_templates.xml',
'views/settings.xml',
'views/payment_views.xml',
'views/sale_order_views.xml',
],
'installable': True,
'auto_install': False,
+30 -35
View File
@@ -24,26 +24,24 @@ class PaymentPortal(http.Controller):
return False
try:
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
acquirer_id = int(acquirer_id)
except:
return False
token = request.env['payment.token'].sudo() # currently no support of payment tokens
tx = request.env['payment.transaction'].sudo()._check_or_create_sale_tx(
order_sudo,
acquirer,
payment_token=token,
tx_type='form_save' if save_token else 'form',
add_tx_values={
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order_sudo._name)], limit=1).id,
'callback_res_id': order_sudo.id,
'callback_method': callback_method,
})
# Create transaction
vals = {
'acquirer_id': acquirer_id,
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order_sudo._name)], limit=1).id,
'callback_res_id': order_sudo.id,
'callback_method': callback_method,
}
# set the transaction id into the session
request.session['portal_sale_%s_transaction_id' % order_sudo.id] = tx.id
if save_token:
vals['type'] = 'form_save'
return tx.render_sale_button(
transaction = order_sudo._create_payment_transaction(vals)
return transaction.render_sale_button(
order_sudo,
success_url,
submit_txt=_('Pay'),
@@ -77,26 +75,23 @@ class PaymentPortal(http.Controller):
params['error'] = 'pay_sale_invalid_token'
return request.redirect(_build_url_w_params(error_url, params))
# find an existing tx or create a new one
tx = request.env['payment.transaction'].sudo()._check_or_create_sale_tx(
order_sudo,
token.acquirer_id,
payment_token=token,
tx_type='server2server',
add_tx_values={
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order_sudo._name)], limit=1).id,
'callback_res_id': order_sudo.id,
'callback_method': callback_method,
})
try:
pm_id = int(pm_id)
except (ValueError, TypeError):
params['error'] = 'pay_sale_invalid_token'
return request.redirect(_build_url_w_params(error_url, params))
# set the transaction id into the session
request.session['portal_sale_%s_transaction_id' % order_sudo.id] = tx.id
# Create transaction
vals = {
'payment_token_id': pm_id,
'type': 'server2server',
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order_sudo._name)], limit=1).id,
'callback_res_id': order_sudo.id,
'callback_method': callback_method,
}
order_sudo._create_payment_transaction(vals)
params['success'] = 'pay_sale'
# proceed to the payment
res = tx.confirm_sale_token()
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_sale'
return request.redirect(_build_url_w_params(success_url, params))
-1
View File
@@ -2,5 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
from . import crm_team
from . import sale_order
-44
View File
@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class CrmTeam(models.Model):
_inherit = 'crm.team'
pending_payment_transactions_count = fields.Integer(
compute='_compute_payment_transactions',
string='Number of pending transactions', readonly=True)
pending_payment_transactions_amount = fields.Integer(
compute='_compute_payment_transactions',
string='Amount of pending transactions', readonly=True)
authorized_payment_transactions_count = fields.Integer(
compute='_compute_payment_transactions',
string='Number of transactions to capture', readonly=True)
authorized_payment_transactions_amount = fields.Integer(
compute='_compute_payment_transactions',
string='Amount of transactions to capture', readonly=True)
def _compute_payment_transactions(self):
for team in self:
payment_data = self.env['payment.transaction'].read_group([
('state', 'in', ['authorized', 'pending']),
('sale_order_id.team_id', '=', team.id)
], ['amount', 'currency_id', 'state'], ['state', 'currency_id'], lazy=False)
for datum in payment_data:
datum_currency = self.env['res.currency'].browse(datum['currency_id'][0])
if datum['state'] == 'authorized':
team.authorized_payment_transactions_count += datum['__count']
team.authorized_payment_transactions_amount += datum_currency._convert(
datum['amount'],
self.env.user.company_id.currency_id,
self.env.user.company_id,
fields.Date.today())
elif datum['state'] == 'pending':
team.pending_payment_transactions_count += datum['__count']
team.pending_payment_transactions_amount += datum_currency._convert(
datum['amount'],
self.env.user.company_id.currency_id,
self.env.user.company_id,
fields.Date.today())
+84 -170
View File
@@ -11,185 +11,97 @@ _logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
# YTI FIXME: The auto_join seems useless
sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True)
so_state = fields.Selection('sale.order', string='Sale Order State', related='sale_order_id.state')
sale_order_ids = fields.Many2many('sale.order', 'sale_order_transaction_rel', 'transaction_id', 'sale_order_id',
string='Sales Orders', copy=False, readonly=True)
sale_order_ids_nbr = fields.Integer(compute='_compute_sale_order_ids_nbr', string='# of Sales Orders')
@api.depends('sale_order_ids')
def _compute_sale_order_ids_nbr(self):
for trans in self:
trans.sale_order_ids_nbr = len(trans.sale_order_ids)
@api.multi
def _log_payment_transaction_sent(self):
super(PaymentTransaction, self)._log_payment_transaction_sent()
for trans in self:
post_message = trans._get_payment_transaction_sent_message()
for so in trans.sale_order_ids:
so.message_post(body=post_message)
@api.multi
def _log_payment_transaction_received(self):
super(PaymentTransaction, self)._log_payment_transaction_received()
for trans in self.filtered(lambda t: t.provider not in ('manual', 'transfer')):
post_message = trans._get_payment_transaction_received_message()
for so in trans.sale_order_ids:
so.message_post(body=post_message)
@api.multi
def _set_transaction_pending(self):
# Override of '_set_transaction_pending' in the 'payment' module
# to sent the quotations automatically.
super(PaymentTransaction, self)._set_transaction_pending()
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'draft')
sales_orders.force_quotation_send()
@api.multi
def _set_transaction_authorized(self):
# Override of '_set_transaction_authorized' in the 'payment' module
# to confirm the quotations automatically.
super(PaymentTransaction, self)._set_transaction_authorized()
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'draft')
sales_orders.force_quotation_send()
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'sent')
for so in sales_orders:
# For loop because some override of action_confirm are ensure_one.
so.action_confirm()
@api.multi
def _set_transaction_done(self):
# Override of '_set_transaction_done' in the 'payment' module
# to confirm the quotations automatically and to generate the invoices if needed.
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'draft')
sales_orders.force_quotation_send()
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'sent')
sales_orders.action_confirm()
if self.env['ir.config_parameter'].sudo().get_param('website_sale.automatic_invoice'):
for trans in self.filtered(lambda t: t.sale_order_ids):
trans.sale_order_ids._force_lines_to_invoice_policy_order()
invoices = trans.sale_order_ids.action_invoice_create()
trans.invoice_ids = [(6, 0, invoices)]
return super(PaymentTransaction, self)._set_transaction_done()
@api.model
def form_feedback(self, data, acquirer_name):
""" Override to confirm the sales order, if defined, and if the transaction
is done. """
tx = None
res = super(PaymentTransaction, self).form_feedback(data, acquirer_name)
def _compute_reference_prefix(self, values):
prefix = super(PaymentTransaction, self)._compute_reference_prefix(values)
if not prefix and values and values.get('sale_order_ids'):
many_list = self.resolve_2many_commands('sale_order_ids', values['sale_order_ids'], fields=['name'])
return ','.join(dic['name'] for dic in many_list)
return prefix
# fetch the tx
tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
if hasattr(self, tx_find_method_name):
tx = getattr(self, tx_find_method_name)(data)
_logger.info('<%s> transaction processed: tx ref:%s, tx amount: %s', acquirer_name, tx.reference if tx else 'n/a', tx.amount if tx else 'n/a')
if tx and tx.sale_order_id:
# Auto-confirm SO if necessary
tx._confirm_so()
return res
# --------------------------------------------------
# Sale management
# --------------------------------------------------
def _confirm_so(self):
""" Check tx state, confirm the potential SO """
self.ensure_one()
if self.sale_order_id.state not in ['draft', 'sent', 'sale']:
_logger.warning('<%s> transaction STATE INCORRECT for order %s (ID %s, state %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id, self.sale_order_id.state)
return 'pay_sale_invalid_doc_state'
if not float_compare(self.amount, self.sale_order_id.amount_total, 2) == 0:
_logger.warning(
'<%s> transaction AMOUNT MISMATCH for order %s (ID %s): expected %r, got %r',
self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id,
self.sale_order_id.amount_total, self.amount,
)
self.sale_order_id.message_post(
subject=_("Amount Mismatch (%s)") % self.acquirer_id.provider,
body=_("The sale order was not confirmed despite response from the acquirer (%s): SO amount is %r but acquirer replied with %r.") % (
self.acquirer_id.provider,
self.sale_order_id.amount_total,
self.amount,
)
)
return 'pay_sale_tx_amount'
if self.state == 'authorized' and self.acquirer_id.capture_manually:
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
if self.sale_order_id.state in ('draft', 'sent'):
self.sale_order_id.with_context(send_email=True).action_confirm()
elif self.state == 'done':
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
if self.sale_order_id.state in ('draft', 'sent'):
self.sale_order_id.with_context(send_email=True).action_confirm()
elif self.state not in ['cancel', 'error'] and self.sale_order_id.state == 'draft':
_logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
self.sale_order_id.force_quotation_send()
@api.multi
def action_view_sales_orders(self):
action = {
'name': _('Sales Order(s)'),
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'target': 'current',
}
sale_order_ids = self.sale_order_ids.ids
if len(sale_order_ids) == 1:
action['res_id'] = sale_order_ids[0]
action['view_mode'] = 'form'
else:
_logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
return 'pay_sale_tx_state'
return True
def _generate_and_pay_invoice(self):
self.sale_order_id._force_lines_to_invoice_policy_order()
# force company to ensure journals/accounts etc. are correct
# company_id needed for default_get on account.journal
# force_company needed for company_dependent fields
ctx_company = {'company_id': self.sale_order_id.company_id.id,
'force_company': self.sale_order_id.company_id.id}
created_invoice = self.sale_order_id.with_context(**ctx_company).action_invoice_create()
created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company)
if created_invoice:
_logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)',
self.acquirer_id.provider, created_invoice.name, created_invoice.id, self.sale_order_id.name, self.sale_order_id.id)
created_invoice.action_invoice_open()
if not self.acquirer_id.journal_id:
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if not default_journal:
_logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)',
self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
return False
self.acquirer_id.journal_id = default_journal
# TDE Note: in post-v11 we could probably add an explicit parameter + update account.payment tx directly at creation, not afterwards
created_invoice.with_context(default_currency_id=self.currency_id.id).pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=created_invoice.amount_total)
if created_invoice.payment_ids:
created_invoice.payment_ids[0].payment_transaction_id = self
self._post_process_after_done(invoice_id=created_invoice)
else:
_logger.warning('<%s> transaction completed, could not auto-generate invoice for %s (ID %s)',
self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
action['view_mode'] = 'tree,form'
action['domain'] = [('id', 'in', sale_order_ids)]
return action
# --------------------------------------------------
# Tools for payment
# --------------------------------------------------
def confirm_sale_token(self):
""" Confirm a transaction token and call SO confirmation if it is a success.
:return: True if success; error string otherwise """
self.ensure_one()
if self.payment_token_id and self.partner_id == self.sale_order_id.partner_id:
try:
s2s_result = self.s2s_do_transaction()
except Exception as e:
_logger.warning(
_("<%s> transaction (%s) failed: <%s>") %
(self.acquirer_id.provider, self.id, str(e)))
return 'pay_sale_tx_fail'
valid_state = 'authorized' if self.acquirer_id.capture_manually else 'done'
if not s2s_result or self.state != valid_state:
_logger.warning(
_("<%s> transaction (%s) invalid state: %s") %
(self.acquirer_id.provider, self.id, self.state_message))
return 'pay_sale_tx_state'
try:
return self._confirm_so()
except Exception as e:
_logger.warning(
_("<%s> transaction (%s) order confirmation failed: <%s>") %
(self.acquirer_id.provider, self.id, str(e)))
return 'pay_sale_tx_confirm'
return 'pay_sale_tx_token'
def _check_or_create_sale_tx(self, order, acquirer, payment_token=None, tx_type='form', add_tx_values=None, reset_draft=True):
tx = self
if not tx:
tx = self.search([('reference', '=', order.name)], limit=1)
if tx.state in ['error', 'cancel']: # filter incorrect states
tx = False
if (tx and tx.acquirer_id != acquirer) or (tx and tx.sale_order_id != order): # filter unmatching
tx = False
if tx and payment_token and tx.payment_token_id and payment_token != tx.payment_token_id: # new or distinct token
tx = False
# still draft tx, no more info -> rewrite on tx or create a new one depending on parameter
if tx and tx.state == 'draft':
if reset_draft:
tx.write(dict(
self.on_change_partner_id(order.partner_id.id).get('value', {}),
amount=order.amount_total,
type=tx_type)
)
else:
tx = False
if not tx:
tx_values = {
'acquirer_id': acquirer.id,
'type': tx_type,
'amount': order.amount_total,
'currency_id': order.pricelist_id.currency_id.id,
'partner_id': order.partner_id.id,
'partner_country_id': order.partner_id.country_id.id,
'reference': self.get_next_reference(order.name),
'sale_order_id': order.id,
}
if add_tx_values:
tx_values.update(add_tx_values)
if payment_token and payment_token.sudo().partner_id == order.partner_id:
tx_values['payment_token_id'] = payment_token.id
tx = self.create(tx_values)
# update quotation
order.write({
'payment_tx_id': tx.id,
})
return tx
def render_sale_button(self, order, return_url, submit_txt=None, render_values=None):
values = {
'return_url': return_url,
@@ -198,6 +110,8 @@ class PaymentTransaction(models.Model):
}
if render_values:
values.update(render_values)
# Not very elegant to do that here but no choice regarding the design.
self._log_payment_transaction_sent()
return self.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=submit_txt or _('Pay Now')).sudo().render(
self.reference,
order.amount_total,
+96 -29
View File
@@ -1,24 +1,93 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class SaleOrder(models.Model):
_inherit = 'sale.order'
payment_tx_ids = fields.One2many('payment.transaction', 'sale_order_id', string='Transactions')
payment_tx_id = fields.Many2one('payment.transaction', string='Last Transaction', copy=False)
payment_acquirer_id = fields.Many2one('payment.acquirer', string='Payment Acquirer', related='payment_tx_id.acquirer_id', store=True)
payment_transaction_count = fields.Integer(
string="Number of payment transactions",
compute='_compute_payment_transaction_count')
transaction_ids = fields.Many2many('payment.transaction', 'sale_order_transaction_rel', 'sale_order_id', 'transaction_id',
string='Transactions', copy=False, readonly=True)
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
string='Authorized Transactions', copy=False, readonly=True)
def _compute_payment_transaction_count(self):
transaction_data = self.env['payment.transaction'].read_group([('sale_order_id', 'in', self.ids)], ['sale_order_id'], ['sale_order_id'])
mapped_data = dict([(m['sale_order_id'][0], m['sale_order_id_count']) for m in transaction_data])
for order in self:
order.payment_transaction_count = mapped_data.get(order.id, 0)
@api.depends('transaction_ids')
def _compute_authorized_transaction_ids(self):
for trans in self:
trans.authorized_transaction_ids = trans.transaction_ids.filtered(lambda t: t.state == 'authorized')
@api.multi
def get_portal_last_transaction(self):
self.ensure_one()
return self.transaction_ids.get_last_transaction()
@api.multi
def _create_payment_transaction(self, vals):
'''Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
current sales orders fields (e.g. the partner or the currency).
:param vals: The values to create a new payment.transaction.
:return: The newly created payment.transaction record.
'''
# Ensure the currencies are the same.
currency = self[0].pricelist_id.currency_id
if any([so.pricelist_id.currency_id != currency for so in self]):
raise ValidationError(_('A transaction can\'t be linked to sales orders having different currencies.'))
# Ensure the partner are the same.
partner = self[0].partner_id
if any([so.partner_id != partner for so in self]):
raise ValidationError(_('A transaction can\'t be linked to sales orders having different partners.'))
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
acquirer_id = vals.get('acquirer_id')
acquirer = False
payment_token_id = vals.get('payment_token_id')
if payment_token_id:
payment_token = self.env['payment.token'].sudo().browse(payment_token_id)
# Check payment_token/acquirer matching or take the acquirer from token
if acquirer_id:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
if payment_token and payment_token.acquirer_id != acquirer:
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
payment_token.acquirer_id.name, acquirer.name))
if payment_token and payment_token.partner_id != partner:
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
payment_token.partner.name, partner.name))
else:
acquirer = payment_token.acquirer_id
# Check an acquirer is there.
if not acquirer_id and not acquirer:
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
if not acquirer:
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
# Check a journal is set on acquirer.
if not acquirer.journal_id:
raise ValidationError(_('A journal must be specified of the acquirer %s.' % acquirer.name))
if not acquirer_id and acquirer:
vals['acquirer_id'] = acquirer.id
vals.update({
'amount': sum(self.mapped('amount_total')),
'currency_id': currency.id,
'partner_id': partner.id,
'sale_order_ids': [(6, 0, self.ids)],
})
transaction = self.env['payment.transaction'].create(vals)
# Process directly if payment_token
if transaction.payment_token_id:
transaction.s2s_do_transaction()
return transaction
def _force_lines_to_invoice_policy_order(self):
for line in self.order_line:
@@ -27,20 +96,18 @@ class SaleOrder(models.Model):
else:
line.qty_to_invoice = 0
def action_view_transaction(self):
action = {
'type': 'ir.actions.act_window',
'name': 'Payment Transactions',
'res_model': 'payment.transaction',
}
if self.payment_transaction_count == 1:
action.update({
'res_id': self.env['payment.transaction'].search([('sale_order_id', '=', self.id)]).id,
'view_mode': 'form',
})
else:
action.update({
'view_mode': 'tree,form',
'domain': [('sale_order_id', '=', self.id)],
})
return action
@api.multi
def _prepare_invoice(self):
# Override
# Add the transactions in the SO to the invoices.
invoice_vals = super(SaleOrder, self)._prepare_invoice()
invoice_vals['transaction_ids'] = [(6, 0, self.transaction_ids.ids)]
return invoice_vals
@api.multi
def payment_action_capture(self):
self.authorized_transaction_ids.s2s_capture_transaction()
@api.multi
def payment_action_void(self):
self.authorized_transaction_ids.s2s_void_transaction()
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="crm_team_salesteams_view_kanban_inherit_website_portal_sale" model="ir.ui.view">
<field name="name">crm.team.kanban</field>
<field name="model">crm.team</field>
<field name="inherit_id" ref="sales_team.crm_team_salesteams_view_kanban"/>
<field name="arch" type="xml">
<data>
<xpath expr="//t[@name='third_options']" position="after">
<div class="row" t-if="record.pending_payment_transactions_count.raw_value">
<div class="col-xs-8">
<div>
<a name="%(sale_payment.payment_transaction_action_pending)d" type="action">
<field name="pending_payment_transactions_count"/>
<t t-if="record.pending_payment_transactions_count.raw_value == 1">Awaiting Payment</t>
<t t-else="">Awaiting Payments</t>
</a>
</div>
</div>
<div class="col-xs-4 text-right">
<field name="pending_payment_transactions_amount" widget="monetary"/>
</div>
</div>
<div class="row" t-if="record.authorized_payment_transactions_count.raw_value">
<div class="col-xs-8">
<a name="%(sale_payment.payment_transaction_action_authorized)d" type="action">
<field name="authorized_payment_transactions_count"/>
<t t-if="record.authorized_payment_transactions_count.raw_value == 1">Payment to Capture</t>
<t t-else="">Payments to Capture</t>
</a>
</div>
<div class="col-xs-4 text-right">
<field name="authorized_payment_transactions_amount" widget="monetary"/>
</div>
</div>
</xpath>
</data>
</field>
</record>
</odoo>
+16 -15
View File
@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_transaction_action_pending" model="ir.actions.act_window">
<field name="name">Pending Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('sale_order_id.team_id', '=', active_id), ('state', '=', 'pending')]</field>
</record>
<record id="payment_transaction_action_authorized" model="ir.actions.act_window">
<field name="name">Authorized Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('sale_order_id.team_id', '=', active_id), ('state', '=', 'authorized')]</field>
</record>
<data>
<record id="transaction_form_inherit_sale_payment" model="ir.ui.view">
<field name="name">payment.transaction.form.inherit.sale.payment</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment.transaction_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="action_view_sales_orders" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('sale_order_ids_nbr', '=', 0)]}">
<field name="sale_order_ids_nbr" widget="statinfo" string="Sales Order(s)"/>
</button>
</xpath>
</field>
</record>
</data>
</odoo>
+22 -12
View File
@@ -1,15 +1,25 @@
<?xml version="1.0"?>
<odoo>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_transaction" type="object" class="oe_stat_button" icon="fa-money" attrs="{'invisible': [('payment_transaction_count', '=', 0)]}">
<field name="payment_transaction_count" widget="statinfo" string="Transactions"/>
</button>
</xpath>
</field>
</record>
<data>
<record id="sale_order_view_form_inherit_sale_payment" model="ir.ui.view">
<field name="name">sale.order.view.form.inherit.sale.payment</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!--
Allow the user to capture/void the authorized transactions in the sale order.
-->
<xpath expr="//button[1]" position="before">
<field name="authorized_transaction_ids" invisible="1"/>
<button name="payment_action_capture" type="object"
string="Capture Transaction" class="oe_highlight"
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
<button name="payment_action_void" type="object"
string="Void Transaction"
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,37 +1,8 @@
<odoo>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<template id="portal_my_orders_payment" name="Payment on my orders" inherit_id="sale.portal_order_page">
<xpath expr="//t[@name='portal_confirmation_sign']" position="after">
<t t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" name="portal_confirmation_pay">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and payment_tx_id and payment_tx_id.state != 'done'" name="portal_confirmation_pay">
<a class="btn btn-primary ml8 pull-right" href="#portal_pay">
<i class="fa fa-arrow-circle-right"/> Pay Now
</a>
@@ -43,13 +14,13 @@
</t>
</xpath>
<xpath expr="//t[@t-call='sale.portal_order_success']" position="after">
<div t-if="order.payment_tx_ids and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" class="o_sale_payment_tx_status" t-att-data-order-id="order.id">
<t t-set="payment_tx_id" t-value="order.payment_tx_id"/>
<t t-call="sale_payment.payment_confirmation_status"/>
<div t-if="order.transaction_ids and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" class="o_sale_payment_tx_status" t-att-data-order-id="order.id">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-call="payment.payment_confirmation_status"/>
</div>
</xpath>
<xpath expr="//div[@id='portal_sign']" position="after">
<div class="row" t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" id="portal_pay">
<div class="row" t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.get_portal_last_transaction().state != 'done'" id="portal_pay">
<div class="col-md-offset-3 col-md-6">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left col-md-13">
<h4 class="mb24">Pay with</h4>
@@ -70,7 +41,7 @@
</div>
</xpath>
<xpath expr="//t[@t-if='invoices']" position="before">
<t t-set="tx_ids" t-value="order.payment_tx_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
<t t-set="tx_ids" t-value="order.transaction_ids.filtered(lambda tx: tx.state != 'draft')"/>
<t t-if="tx_ids">
<div>
<strong>Transactions</strong>
@@ -97,36 +68,25 @@
<template id="portal_order_error" name="Order error display: payment errors"
inherit_id="sale.portal_order_error">
<xpath expr="//t[@name='generic']" position="after">
<t t-set="tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-if="error == 'pay_sale_invalid_doc'">
There was an error processing your payment: invalid order.
</t>
<t t-if="error == 'pay_sale_invalid_doc_state'">
There was an error processing your payment: invalid order state.
</t>
<t t-if="error == 'pay_sale_invalid_token'">
There was en error processing your payment: invalid credit card ID.
</t>
<t t-if="error == 'pay_sale_tx_amount'">
There was an error processing your payment: transaction amount issue.<br />
<t t-if="order.payment_tx_id">
<t t-esc="order.payment_tx_id.amount"/> / <t t-esc="order.amount_total"/>
</t>
</t>
<t t-if="error == 'pay_sale_tx_fail'">
There was an error processing your payment: transaction failed.<br />
<t t-if="order.payment_tx_id and order.payment_tx_id.state_message">
<t t-esc="order.payment_tx_id.state_message"/>
<t t-if="tx_id and tx_id.state_message">
<t t-esc="tx_id.state_message"/>
</t>
</t>
<t t-if="error == 'pay_sale_tx_state'">
There was an error processing your payment: transaction issue.<br />
<t t-if="order.payment_tx_id and order.payment_tx_id.state_message">
<t t-esc="order.payment_tx_id.state_message"/>
<t t-if="tx_id and tx_id.state_message">
<t t-esc="tx_id.state_message"/>
</t>
</t>
<t t-if="error == 'pay_sale_tx_confirm'">
There was an error processing your payment: impossible to validate order.
</t>
<t t-if="error == 'pay_sale_tx_token'">
There was an error processing your payment: issue with credit card ID validation.
</t>
@@ -137,8 +97,9 @@
inherit_id="sale.portal_order_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_sale'">
<span t-if='order.payment_acquirer_id.done_msg' t-raw="order.payment_acquirer_id.done_msg"/>
<span t-if='order.payment_acquirer_id.post_msg' t-raw="order.payment_acquirer_id.post_msg"/>
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<span t-if='payment_tx_id and payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</t>
</xpath>
</template>
+1 -1
View File
@@ -51,7 +51,7 @@ class SaleOrder(models.Model):
def action_confirm(self):
""" On SO confirmation, some lines should generate a task or a project. """
result = super(SaleOrder, self).action_confirm()
self.order_line.sudo()._timesheet_service_generation()
self.mapped('order_line').sudo()._timesheet_service_generation()
return result
@api.multi
+1 -1
View File
@@ -7,7 +7,7 @@
'website': 'https://www.odoo.com/page/quote-builder',
'version': '1.0',
'description': "",
'depends': ['website', 'sale_management', 'mail', 'payment', 'website_mail'],
'depends': ['website', 'sale_management', 'mail', 'sale_payment', 'website_mail'],
'data': [
'data/website_quote_data.xml',
'report/sale_order_reports.xml',
+40 -39
View File
@@ -64,11 +64,7 @@ class sale_quote(http.Controller):
pdf = request.env.ref('website_quote.report_web_quote').sudo().with_context(set_viewport_size=True).render_qweb_pdf([order_sudo.id])[0]
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
transaction_id = request.session.get('quote_%s_transaction_id' % order_sudo.id)
if not transaction_id:
Transaction = request.env['payment.transaction'].sudo().search([('reference', '=', order_sudo.name)])
else:
Transaction = request.env['payment.transaction'].sudo().browse(transaction_id)
transaction = order_sudo.get_portal_last_transaction()
values = {
'quotation': order_sudo,
'message': message and int(message) or False,
@@ -77,11 +73,11 @@ class sale_quote(http.Controller):
'days_valid': days,
'action': request.env.ref('sale.action_quotations').id,
'no_breadcrumbs': request.env.user.partner_id.commercial_partner_id not in order_sudo.message_partner_ids,
'tx_id': Transaction.id if Transaction else False,
'tx_state': Transaction.state if Transaction else False,
'tx_post_msg': Transaction.acquirer_id.post_msg if Transaction else False,
'payment_tx': Transaction,
'need_payment': order_sudo.invoice_status == 'to invoice' and Transaction.state in ['draft', 'cancel', 'error'],
'tx_id': transaction.id if transaction else False,
'tx_state': transaction.state if transaction else False,
'payment_tx': transaction,
'tx_post_msg': transaction.acquirer_id.post_msg if transaction else False,
'need_payment': order_sudo.invoice_status == 'to invoice' and transaction.state in ['draft', 'cancel'],
'token': token,
'return_url': '/shop/payment/validate',
'bootstrap_formatting': True,
@@ -171,23 +167,36 @@ class sale_quote(http.Controller):
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
order = request.env['sale.order'].sudo().browse(order_id)
if not order or not order.order_line or acquirer_id is None:
# Ensure a payment acquirer is selected
if not acquirer_id:
return False
# find an already existing transaction
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
token = request.env['payment.token'].sudo() # currently no support of payment tokens
tx = request.env['payment.transaction'].sudo().search([('reference', '=', order.name)], limit=1)
tx_type = order._get_payment_type()
tx = tx._check_or_create_sale_tx(order, acquirer, payment_token=token, tx_type=tx_type)
request.session['quote_%s_transaction_id' % order.id] = tx.id
try:
acquirer_id = int(acquirer_id)
except:
return False
return tx.render_sale_button(order, '/quote/%s/%s' % (order_id, access_token) if access_token else '/quote/%s' % order_id,
submit_txt=_('Pay & Confirm'), render_values={
'type': order._get_payment_type(),
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
})
order = request.env['sale.order'].sudo().browse(order_id)
if not order or not order.order_line:
return False
# Create transaction
vals = {
'acquirer_id': acquirer_id,
'type': order._get_payment_type(),
}
transaction = order._create_payment_transaction(vals)
return transaction.render_sale_button(
order,
'/quote/%s/%s' % (order_id, access_token) if access_token else '/quote/%s' % order_id,
submit_txt=_('Pay & Confirm'),
render_values={
'type': order._get_payment_type(),
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
}
)
@http.route('/quote/<int:order_id>/transaction/token', type='http', auth='public', website=True)
def payment_token(self, order_id, pm_id=None, **kwargs):
@@ -202,20 +211,12 @@ class sale_quote(http.Controller):
except ValueError:
return request.redirect('/quote/%s' % order_id)
# retrieve the token from its id
token = request.env['payment.token'].sudo().browse(pm_id)
if not token:
return request.redirect('/quote/%s' % order_id)
# Create transaction
vals = {
'payment_token_id': pm_id,
'type': 'server2server',
}
order._create_payment_transaction(vals)
# find an already existing transaction
tx = request.env['payment.transaction'].sudo().search([('reference', '=', order.name)], limit=1)
# set the transaction type to server2server
tx_type = 'server2server'
# check if the transaction exists, if not then it create one
tx = tx._check_or_create_sale_tx(order, token.acquirer_id, payment_token=token, tx_type=tx_type)
# set the transaction id into the session
request.session['quote_%s_transaction_id' % order_id] = tx.id
# proceed to the payment
tx.confirm_sale_token()
# redirect the user to the online quote
return request.redirect('/quote/%s/%s' % (order_id, order.access_token))
-2
View File
@@ -18,9 +18,7 @@
'views/sale_order_views.xml',
'views/crm_team_views.xml',
'views/templates.xml',
'views/payment_views.xml',
'views/snippets.xml',
'views/report_shop_saleorder.xml',
'views/res_config_settings_views.xml',
],
'demo': [
+1 -1
View File
@@ -91,7 +91,7 @@ class WebsiteSaleBackend(WebsiteBackend):
payment_to_capture_count=request.env['payment.transaction'].search_count([
('state', '=', 'authorized'),
# that part perform a search on sale.order in order to comply with access rights as tx do not have any
('sale_order_id.id', 'in', request.env['sale.order'].search(sale_order_domain + [('state', '!=', 'cancel')]).ids),
('sale_order_ids', 'in', request.env['sale.order'].search(sale_order_domain + [('state', '!=', 'cancel')]).ids),
]),
total_sold=sum(price_line['price_subtotal'] for price_line in report_price_lines)
)
+43 -46
View File
@@ -826,30 +826,43 @@ class WebsiteSale(http.Controller):
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
tx_type = 'form'
if save_token:
tx_type = 'form_save'
# Ensure a payment acquirer is selected
if not acquirer_id:
return False
# In case the route is called directly from the JS (as done in Stripe payment method)
if so_id and access_token:
order = request.env['sale.order'].sudo().search([('id', '=', so_id), ('access_token', '=', access_token)])
elif so_id:
order = request.env['sale.order'].search([('id', '=', so_id)])
try:
acquirer_id = int(acquirer_id)
except:
return False
# Retrieve the sale order
if so_id:
env = request.env['sale.order']
domain = [('id', '=', so_id)]
if access_token:
env = env.sudo()
domain.append(('access_token', '=', access_token))
order = env.search(domain, limit=1)
else:
order = request.website.sale_get_order()
if not order or not order.order_line or acquirer_id is None:
# Ensure there is something to proceed
if not order or (order and not order.order_line):
return False
assert order.partner_id.id != request.website.partner_id.id
# find or create transaction
tx = request.website.sale_get_transaction() or request.env['payment.transaction'].sudo()
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
payment_token = request.env['payment.token'].sudo().browse(int(token)) if token else None
tx = tx._check_or_create_sale_tx(order, acquirer, payment_token=payment_token, tx_type=tx_type)
request.session['sale_transaction_id'] = tx.id
# Create transaction
vals = {'acquirer_id': acquirer_id}
return tx.render_sale_button(order, '/shop/payment/validate')
if save_token:
vals['type'] = 'form_save'
if token:
vals['payment_token_id'] = int(token)
transaction = order._create_payment_transaction(vals)
return transaction.render_sale_button(order, '/shop/payment/validate')
@http.route('/shop/payment/token', type='http', auth='public', website=True)
def payment_token(self, pm_id=None, **kwargs):
@@ -870,23 +883,13 @@ class WebsiteSale(http.Controller):
return request.redirect('/shop/?error=invalid_token_id')
# We retrieve the token the user want to use to pay
token = request.env['payment.token'].sudo().browse(pm_id)
if not token:
if not request.env['payment.token'].sudo().search_count([('id', '=', pm_id)]):
return request.redirect('/shop/?error=token_not_found')
# we retrieve an existing transaction (if it exists obviously)
tx = request.website.sale_get_transaction() or request.env['payment.transaction'].sudo()
# we check if the transaction is Ok, if not then we create it
tx = tx._check_or_create_sale_tx(order, token.acquirer_id, payment_token=token, tx_type='server2server')
# we set the transaction id into the session (so `sale_get_transaction` can retrieve it )
request.session['sale_transaction_id'] = tx.id
# we proceed the s2s payment
res = tx.confirm_sale_token()
# we then redirect to the page that validates the payment by giving it error if there's one
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
return request.redirect('/shop/payment/validate?success=False&error=%s' % res)
return request.redirect('/shop/payment/validate?success=True')
# Create transaction
vals = {'payment_token_id': pm_id}
order._create_payment_transaction(vals)
return request.redirect('/shop/payment/validate')
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
@@ -895,7 +898,7 @@ class WebsiteSale(http.Controller):
assert order.id == request.session.get('sale_last_order_id')
return {
'recall': order.payment_tx_id.state == 'pending',
'recall': order.get_portal_last_transaction().state == 'pending',
'message': request.env['ir.ui.view'].render_template("website_sale.payment_confirmation_status", {
'order': order
})
@@ -908,29 +911,23 @@ class WebsiteSale(http.Controller):
- UDPATE ME
"""
if transaction_id is None:
tx = request.website.sale_get_transaction()
else:
tx = request.env['payment.transaction'].browse(transaction_id)
if sale_order_id is None:
order = request.website.sale_get_order()
else:
order = request.env['sale.order'].sudo().browse(sale_order_id)
assert order.id == request.session.get('sale_last_order_id')
if transaction_id:
tx = request.env['payment.transaction'].sudo().browse(transaction_id)
assert tx in order.transaction_ids()
elif order:
tx = order.get_portal_last_transaction()
else:
tx = None
if not order or (order.amount_total and not tx):
return request.redirect('/shop')
if (not order.amount_total and not tx) or tx.state in ['pending', 'done', 'authorized']:
if (not order.amount_total and not tx):
# Orders are confirmed by payment transactions, but there is none for free orders,
# (e.g. free events), so confirm immediately
order.with_context(send_email=True).action_confirm()
elif tx and tx.state == 'cancel':
# cancel the quotation
order.action_cancel()
# clean context and session, then redirect to the confirmation page
request.website.sale_reset()
if tx and tx.state == 'draft':
-17
View File
@@ -1035,23 +1035,6 @@ Weight: 31 grams</field>
<field name="price_unit">1799.0</field>
</record>
<record id="website_payment_transaction_2" model="payment.transaction">
<field name="create_date" eval="datetime.now()"/>
<field name="acquirer_id" ref="payment.payment_acquirer_transfer"/>
<field name="sale_order_id" ref="website_sale_order_13"/>
<field name="reference" ref="website_sale_order_13"/>
<field name="type">form</field>
<field name="state">pending</field>
<field name="amount">1799.0</field>
<field name="currency_id" ref="base.USD"/>
<field name="partner_id" ref="base.res_partner_3"/>
</record>
<record id="website_sale_order_13" model="sale.order">
<field name="payment_acquirer_id" ref="payment.payment_acquirer_transfer"/>
<field name="payment_tx_id" ref="website_payment_transaction_2"/>
</record>
<!-- Order to Invoice -->
<record id="website_sale_order_14" model="sale.order">
<field name="partner_id" ref="base.res_partner_3"/>
+3 -3
View File
@@ -30,11 +30,11 @@ class SaleOrder(models.Model):
is_abandoned_cart = fields.Boolean('Abandoned Cart', compute='_compute_abandoned_cart', search='_search_abandoned_cart')
cart_recovery_email_sent = fields.Boolean('Cart recovery email already sent')
@api.depends('state', 'payment_tx_id', 'payment_tx_id.state',
'payment_acquirer_id', 'payment_acquirer_id.provider')
@api.depends('state', 'transaction_ids')
def _compute_can_directly_mark_as_paid(self):
for order in self:
order.can_directly_mark_as_paid = order.state in ['sent', 'sale'] and order.payment_tx_id and order.payment_acquirer_id.provider in ['transfer', 'manual']
transaction = order.get_portal_last_transaction()
order.can_directly_mark_as_paid = order.state in ['sent', 'sale'] and transaction and transaction.acquirer_id.provider in ['transfer', 'manual']
@api.multi
@api.depends('website_order_line.product_uom_qty', 'website_order_line.product_id')
-19
View File
@@ -299,28 +299,9 @@ class Website(models.Model):
return sale_order
def sale_get_transaction(self):
tx_id = request.session.get('sale_transaction_id')
if tx_id:
transaction = self.env['payment.transaction'].sudo().browse(tx_id)
# Ugly hack for SIPS: SIPS does not allow to reuse a payment reference, even if the
# payment was not not proceeded. For example:
# - Select SIPS for payment
# - Be redirected to SIPS website
# - Go back to eCommerce without paying
# - Be redirected to SIPS website again => error
# Since there is no link module between 'website_sale' and 'payment_sips', we prevent
# here to reuse any previous transaction for SIPS.
if transaction.state != 'cancel' and transaction.acquirer_id.provider != 'sips':
return transaction
else:
request.session['sale_transaction_id'] = False
return False
def sale_reset(self):
request.session.update({
'sale_order_id': False,
'sale_transaction_id': False,
'website_sale_current_pl': False,
})
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_transaction_view_form_inherit_website_sale" model="ir.ui.view">
<field name="name">payment.transaction.view.form.inherit.website_sale</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment.transaction_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='reference']" position="after">
<field name="sale_order_id"/>
</xpath>
</field>
</record>
<record id="payment_transaction_action_payments_to_capture" model="ir.actions.act_window">
<field name="name">Payments To Capture</field>
<field name="res_model">payment.transaction</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('state', '=', 'authorized'), ('sale_order_id.state', '!=', 'cancel')]</field>
<field name="context">{'create': False}</field>
<field name="view_id" ref="payment.transaction"/>
<field name="help">You don't have any payments to capture from the website.</field>
</record>
</odoo>
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_shop_saleorder_document" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[@name='total']" position="after">
<t t-if="doc.payment_acquirer_id.name">
<table class="table table-condensed mt32 mb32">
<thead>
<tr>
<th>Payment Information</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody class="sale_tbody">
<tr>
<td>
<strong>Payment Acquirer:</strong> <t t-esc="doc.payment_acquirer_id.name"/><br />
<strong>Payment Status:</strong>
<t t-if="doc.payment_acquirer_id.journal_id">
<t t-if="doc.payment_tx_id.state == 'pending'">
<t t-raw="doc.payment_acquirer_id.pending_msg"/>
</t>
<t t-if="doc.payment_tx_id.state in ['done', 'authorized']">
<t t-raw="doc.payment_acquirer_id.done_msg"/>
</t>
<t t-if="doc.payment_tx_id.state == 'cancel'">
<t t-raw="doc.payment_acquirer_id.cancel_msg"/>
</t>
<t t-if="doc.payment_tx_id.state == 'error'">
<t t-raw="doc.payment_acquirer_id.error_msg"/>
</t>
</t>
</td>
<td class="text-right">
<span t-field="doc.amount_total"
t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/>
</td>
</tr>
</tbody>
</table>
</t>
</xpath>
</template>
</odoo>
@@ -6,6 +6,23 @@
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='sales_settings_invoicing_policy']" position="after">
<!--
Show the automatic invoices setting in sales settings.
-->
<div class="col-xs-12 col-md-6 o_setting_box"
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), '&amp;', ('portal_confirmation_options', '!=', 'pay'), ('module_website_quote', '=', False)]}">
<div class="o_setting_left_pane">
<field name="automatic_invoice" nolabel="1"/>
</div>
<div class="o_setting_right_pane">
<label for="automatic_invoice"/>
<div class="text-muted">
Generate the invoice automatically when the payment is received
</div>
</div>
</div>
</xpath>
<div id="webmaster_settings" position="after">
<h2>Product Catalog</h2>
<div class="row mt16 o_settings_container" id="sale_product_catalog_settings">
+13 -1
View File
@@ -206,7 +206,19 @@
<!-- Configuration sub-menus -->
<menuitem id="menu_ecommerce_payment_acquirers"
action="payment.action_payment_acquirer"
parent="menu_ecommerce_settings" name="Payment Acquirers" />
parent="menu_ecommerce_settings" name="Payment Acquirers"/>
<menuitem id="menu_ecommerce_payment_tokens"
action="payment.payment_token_action"
groups="base.group_no_one"
parent="menu_ecommerce_settings"/>
<menuitem id="menu_ecommerce_payment_icons"
action="payment.action_payment_icon"
groups="base.group_no_one"
parent="menu_ecommerce_settings"/>
<menuitem id="menu_ecommerce_payment_transactions"
action="payment.action_payment_transaction"
groups="base.group_no_one"
parent="menu_ecommerce_settings"/>
<menuitem id="menu_catalog_categories"
action="product_public_category_action"
parent="menu_product_settings" sequence="1"/>
+15 -17
View File
@@ -1471,7 +1471,7 @@
<tbody>
<tr>
<td colspan="2">
<t t-esc="order.payment_acquirer_id.name" />
<t t-esc="order.get_portal_last_transaction().acquirer_id.name" />
</td>
<td class="text-right" width="100">
<strong>Total:</strong>
@@ -1560,34 +1560,32 @@
<template id="payment_confirmation_status">
<div class="oe_website_sale_tx_status" t-att-data-order-id="order.id">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<div t-att-class="'panel %s' % (
(order.payment_tx_id.state == 'pending' and 'panel-info') or
(order.payment_tx_id.state == 'done' and 'panel-success') or
(order.payment_tx_id.state == 'authorized' and 'panel-success') or
(payment_tx_id.state == 'pending' and 'panel-info') or
(payment_tx_id.state == 'done' and 'panel-success') or
(payment_tx_id.state == 'authorized' and 'panel-success') or
'panel-danger')">
<a groups="base.group_system" class="btn pull-right" target="_blank"
t-att-href="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.acquirer', order.payment_acquirer_id.id, 'payment.action_payment_acquirer')">
t-att-href="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.acquirer', payment_tx_id.acquirer_id.id, 'payment.action_payment_acquirer')">
<i class="fa fa-pencil"></i>
</a>
<div class="panel-heading">
<t t-if="order.payment_tx_id.state == 'pending'">
<t t-raw="order.payment_acquirer_id.pending_msg"/>
<t t-if="payment_tx_id.state == 'pending'">
<t t-raw="payment_tx_id.acquirer_id.pending_msg"/>
</t>
<t t-if="order.payment_tx_id.state == 'done'">
<t t-raw="order.payment_acquirer_id.done_msg"/>
<t t-if="payment_tx_id.state == 'done'">
<t t-raw="payment_tx_id.acquirer_id.done_msg"/>
</t>
<t t-if="order.payment_tx_id.state == 'cancel'">
<t t-raw="order.payment_acquirer_id.cancel_msg"/>
<t t-if="payment_tx_id.state == 'cancel'">
<t t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</t>
<t t-if="order.payment_tx_id.state == 'error'">
<t t-raw="order.payment_acquirer_id.error_msg"/>
</t>
<t t-if="order.payment_tx_id.state == 'authorized'">
<t t-if="payment_tx_id.state == 'authorized'">
Your payment has been authorized.
</t>
</div>
<div class="panel-body" t-if="order.payment_acquirer_id.post_msg">
<t t-raw="order.payment_acquirer_id.post_msg" />
<div class="panel-body" t-if="payment_tx_id.acquirer_id.post_msg">
<t t-raw="payment_tx_id.acquirer_id.post_msg" />
</div>
</div>
</div>