diff --git a/addons/sale_payment/__manifest__.py b/addons/sale_payment/__manifest__.py
index bb53a6f1e82..1dea7daab0b 100644
--- a/addons/sale_payment/__manifest__.py
+++ b/addons/sale_payment/__manifest__.py
@@ -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,
diff --git a/addons/sale_payment/controllers/payment.py b/addons/sale_payment/controllers/payment.py
index 1d00409a204..3ebd7373ec7 100644
--- a/addons/sale_payment/controllers/payment.py
+++ b/addons/sale_payment/controllers/payment.py
@@ -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))
diff --git a/addons/sale_payment/models/__init__.py b/addons/sale_payment/models/__init__.py
index 3f93126a308..51af63e66bb 100644
--- a/addons/sale_payment/models/__init__.py
+++ b/addons/sale_payment/models/__init__.py
@@ -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
\ No newline at end of file
diff --git a/addons/sale_payment/models/crm_team.py b/addons/sale_payment/models/crm_team.py
deleted file mode 100644
index 5ef72ff3ef1..00000000000
--- a/addons/sale_payment/models/crm_team.py
+++ /dev/null
@@ -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())
diff --git a/addons/sale_payment/models/payment.py b/addons/sale_payment/models/payment.py
index 8076addab09..66c856992ca 100644
--- a/addons/sale_payment/models/payment.py
+++ b/addons/sale_payment/models/payment.py
@@ -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,
diff --git a/addons/sale_payment/models/sale_order.py b/addons/sale_payment/models/sale_order.py
index 6544734887f..17f3781b460 100644
--- a/addons/sale_payment/models/sale_order.py
+++ b/addons/sale_payment/models/sale_order.py
@@ -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()
diff --git a/addons/sale_payment/views/crm_team_views.xml b/addons/sale_payment/views/crm_team_views.xml
deleted file mode 100644
index 8f2b607a3d7..00000000000
--- a/addons/sale_payment/views/crm_team_views.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
- crm.team.kanban
- crm.team
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/sale_payment/views/payment_views.xml b/addons/sale_payment/views/payment_views.xml
index 939089fbf74..a909d53afea 100644
--- a/addons/sale_payment/views/payment_views.xml
+++ b/addons/sale_payment/views/payment_views.xml
@@ -1,18 +1,19 @@
-
- Pending Payment Transactions
- payment.transaction
- form
- tree,form
- [('sale_order_id.team_id', '=', active_id), ('state', '=', 'pending')]
-
-
-
- Authorized Payment Transactions
- payment.transaction
- form
- tree,form
- [('sale_order_id.team_id', '=', active_id), ('state', '=', 'authorized')]
-
+
+
+ payment.transaction.form.inherit.sale.payment
+ payment.transaction
+
+
+
+
+
+
+
+
diff --git a/addons/sale_payment/views/sale_order_views.xml b/addons/sale_payment/views/sale_order_views.xml
index 002866c781f..e0cb9f5fc14 100644
--- a/addons/sale_payment/views/sale_order_views.xml
+++ b/addons/sale_payment/views/sale_order_views.xml
@@ -1,15 +1,25 @@
-
- sale.order.form
- sale.order
-
-
-
-
-
-
-
+
+
+ sale.order.view.form.inherit.sale.payment
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/sale_payment/views/sale_portal_templates.xml b/addons/sale_payment/views/sale_portal_templates.xml
index 06b3f402351..11e07dae31d 100644
--- a/addons/sale_payment/views/sale_portal_templates.xml
+++ b/addons/sale_payment/views/sale_portal_templates.xml
@@ -1,37 +1,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Your payment has been authorized.
-
-
-
-
-
-
+
+
Pay Now
@@ -43,13 +14,13 @@
-
-
-
+
+
+
-
+
Pay with
@@ -70,7 +41,7 @@
-
+
Transactions
@@ -97,36 +68,25 @@
+
There was an error processing your payment: invalid order.
-
- There was an error processing your payment: invalid order state.
-
There was en error processing your payment: invalid credit card ID.
-
- There was an error processing your payment: transaction amount issue.
-
- /
-
-
There was an error processing your payment: transaction failed.
-
-
+
+
There was an error processing your payment: transaction issue.
-
-
+
+
-
- There was an error processing your payment: impossible to validate order.
-
There was an error processing your payment: issue with credit card ID validation.
@@ -137,8 +97,9 @@
inherit_id="sale.portal_order_success">
-
-
+
+
+
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index cbaab9e9fa7..6269e217efe 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -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
diff --git a/addons/website_quote/__manifest__.py b/addons/website_quote/__manifest__.py
index e6a0a1c274f..46ed0cb4847 100644
--- a/addons/website_quote/__manifest__.py
+++ b/addons/website_quote/__manifest__.py
@@ -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',
diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py
index f795de5f4e6..1594aa542ca 100644
--- a/addons/website_quote/controllers/main.py
+++ b/addons/website_quote/controllers/main.py
@@ -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/
/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))
diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py
index d04ce5400ba..034c85ddb14 100644
--- a/addons/website_sale/__manifest__.py
+++ b/addons/website_sale/__manifest__.py
@@ -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': [
diff --git a/addons/website_sale/controllers/backend.py b/addons/website_sale/controllers/backend.py
index 646a1575c9b..eb5b0d78d26 100644
--- a/addons/website_sale/controllers/backend.py
+++ b/addons/website_sale/controllers/backend.py
@@ -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)
)
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 41b45932c15..90a8d18da13 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -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/', 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':
diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml
index d40143e033f..8e78e3675f6 100644
--- a/addons/website_sale/data/demo.xml
+++ b/addons/website_sale/data/demo.xml
@@ -1035,23 +1035,6 @@ Weight: 31 grams
1799.0
-
-
-
-
-
- form
- pending
- 1799.0
-
-
-
-
-
-
-
-
-
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 987bdaa6a66..9d994353c83 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -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')
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index 0a3b20d9070..76bb95280dd 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -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,
})
diff --git a/addons/website_sale/views/payment_views.xml b/addons/website_sale/views/payment_views.xml
deleted file mode 100644
index c0b56d12f43..00000000000
--- a/addons/website_sale/views/payment_views.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- payment.transaction.view.form.inherit.website_sale
- payment.transaction
-
-
-
-
-
-
-
-
-
- Payments To Capture
- payment.transaction
- form
- tree,form
- [('state', '=', 'authorized'), ('sale_order_id.state', '!=', 'cancel')]
- {'create': False}
-
- You don't have any payments to capture from the website.
-
-
diff --git a/addons/website_sale/views/report_shop_saleorder.xml b/addons/website_sale/views/report_shop_saleorder.xml
deleted file mode 100644
index f38ce5433e6..00000000000
--- a/addons/website_sale/views/report_shop_saleorder.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
- | Payment Information |
- Total |
-
-
-
-
-
- Payment Acquirer:
- Payment Status:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
- |
-
-
-
-
-
-
-
diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml
index 718a68ed73f..6127270874d 100644
--- a/addons/website_sale/views/res_config_settings_views.xml
+++ b/addons/website_sale/views/res_config_settings_views.xml
@@ -6,6 +6,23 @@
res.config.settings
+
+
+
+
+
+
+
+
+
+ Generate the invoice automatically when the payment is received
+
+
+
+
Product Catalog
diff --git a/addons/website_sale/views/sale_order_views.xml b/addons/website_sale/views/sale_order_views.xml
index 848ce6106d4..5782921f68e 100644
--- a/addons/website_sale/views/sale_order_views.xml
+++ b/addons/website_sale/views/sale_order_views.xml
@@ -206,7 +206,19 @@
+ parent="menu_ecommerce_settings" name="Payment Acquirers"/>
+
+
+
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 83c184f24c0..f5adfd01b39 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1471,7 +1471,7 @@
|
-
+
|
Total:
@@ -1560,34 +1560,32 @@
|