[IMP] sale: improve sale flows

Rework and code cleanup of automatic invoice and auto-confirm order
flows.

task-2541188

closes odoo/odoo#72368

Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Valentin Chevalier
2022-06-23 17:16:49 +02:00
parent ae3bca08d3
commit 4d2b04a843
3 changed files with 145 additions and 66 deletions
+74 -60
View File
@@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import re
from datetime import datetime
from dateutil import relativedelta
from odoo import api, fields, models, _, SUPERUSER_ID
from odoo.tools import float_compare
from odoo import _, api, Command, fields, models, SUPERUSER_ID
from odoo.tools import format_amount
_logger = logging.getLogger(__name__)
@@ -49,34 +46,46 @@ class PaymentTransaction(models.Model):
sales_orders._send_order_confirmation_mail()
def _check_amount_and_confirm_order(self):
self.ensure_one()
for order in self.sale_order_ids.filtered(lambda so: so.state in ('draft', 'sent')):
if order.currency_id.compare_amounts(self.amount, order.amount_total) == 0:
order.with_context(send_email=True).action_confirm()
else:
_logger.warning(
'<%s> transaction AMOUNT MISMATCH for order %s (ID %s): expected %r, got %r',
self.acquirer_id.provider,order.name, order.id,
order.amount_total, self.amount,
)
order.message_post(
subject=_("Amount Mismatch (%s)", self.acquirer_id.provider),
body=_("The order was not confirmed despite response from the acquirer (%s): order total is %r but acquirer replied with %r.") % (
self.acquirer_id.provider,
order.amount_total,
self.amount,
)
)
""" Confirm the sales order based on the amount of a transaction.
Confirm the sales orders only if the transaction amount is equal to the total amount of the
sales orders. Neither partial payments nor grouped payments (paying multiple sales orders in
one transaction) are not supported.
:return: The confirmed sales orders.
:rtype: a `sale.order` recordset
"""
confirmed_orders = self.env['sale.order']
for tx in self:
# We only support the flow where exactly one quotation is linked to a transaction and
# vice versa.
if len(tx.sale_order_ids) == 1:
quotation = tx.sale_order_ids.filtered(lambda so: so.state in ('draft', 'sent'))
if quotation and len(quotation.transaction_ids) == 1:
# Check if the SO is fully paid
if quotation.currency_id.compare_amounts(tx.amount, quotation.amount_total) == 0:
quotation.with_context(send_email=True).action_confirm()
confirmed_orders |= quotation
else:
_logger.warning(
'<%(provider)s> transaction AMOUNT MISMATCH for order %(so_name)s '
'(ID %(so_id)s): expected %(so_amount)s, got %(tx_amount)s', {
'provider': tx.provider,
'so_name': quotation.name,
'so_id': quotation.id,
'so_amount': format_amount(
quotation.env, quotation.amount_total, quotation.currency_id
),
'tx_amount': format_amount(tx.env, tx.amount, tx.currency_id),
},
)
return confirmed_orders
def _set_authorized(self, state_message=None):
""" Override of payment to confirm the quotations automatically. """
super()._set_authorized(state_message=state_message)
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state in ('draft', 'sent'))
for tx in self:
tx._check_amount_and_confirm_order()
# send order confirmation mail
sales_orders._send_order_confirmation_mail()
confirmed_orders = self._check_amount_and_confirm_order()
confirmed_orders._send_order_confirmation_mail()
def _log_message_on_linked_documents(self, message):
""" Override of payment to log a message on the sales orders linked to the transaction.
@@ -92,33 +101,37 @@ class PaymentTransaction(models.Model):
def _reconcile_after_done(self):
""" Override of payment to automatically confirm quotations and generate invoices. """
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state in ('draft', 'sent'))
for tx in self:
tx._check_amount_and_confirm_order()
# send order confirmation mail
sales_orders._send_order_confirmation_mail()
# invoice the sale orders if needed
self._invoice_sale_orders()
res = super()._reconcile_after_done()
if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice') and any(so.state in ('sale', 'done') for so in self.sale_order_ids):
self.filtered(lambda t: t.sale_order_ids.filtered(lambda so: so.state in ('sale', 'done')))._send_invoice()
return res
confirmed_orders = self._check_amount_and_confirm_order()
confirmed_orders._send_order_confirmation_mail()
# invoice the sale orders if needed and send it
if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
# Invoice the sale orders in self instead of in confirmed_orders to create the invoice
# even if only a partial payment was made.
self._invoice_sale_orders()
self._send_invoice()
return super()._reconcile_after_done()
def _send_invoice(self):
default_template = self.env['ir.config_parameter'].sudo().get_param('sale.default_invoice_email_template')
if not default_template:
template_id = self.env['ir.config_parameter'].sudo().get_param(
'sale.default_invoice_email_template'
)
if not template_id:
return
for trans in self:
trans = trans.with_company(trans.acquirer_id.company_id).with_context(
company_id=trans.acquirer_id.company_id.id,
for tx in self:
tx = tx.with_company(tx.company_id).with_context(
company_id=tx.company_id.id,
)
invoice_to_send = trans.invoice_ids.filtered(
invoice_to_send = tx.invoice_ids.filtered(
lambda i: not i.is_move_sent and i.state == 'posted' and i._is_ready_to_be_sent()
)
invoice_to_send.is_move_sent = True # Mark invoice as sent
for invoice in invoice_to_send.with_user(SUPERUSER_ID):
invoice.message_post_with_template(int(default_template), email_layout_xmlid="mail.mail_notification_paynow")
invoice.message_post_with_template(
int(template_id),
email_layout_xmlid='mail.mail_notification_paynow',
)
def _cron_send_invoice(self):
"""
@@ -142,19 +155,20 @@ class PaymentTransaction(models.Model):
])._send_invoice()
def _invoice_sale_orders(self):
if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
for trans in self.filtered(lambda t: t.sale_order_ids):
trans = trans.with_company(trans.acquirer_id.company_id)\
.with_context(company_id=trans.acquirer_id.company_id.id)
confirmed_orders = trans.sale_order_ids.filtered(lambda so: so.state in ('sale', 'done'))
if confirmed_orders:
confirmed_orders._force_lines_to_invoice_policy_order()
invoices = confirmed_orders._create_invoices()
# Setup access token in advance to avoid serialization failure between
# edi postprocessing of invoice and displaying the sale order on the portal
for invoice in invoices:
invoice._portal_ensure_token()
trans.invoice_ids = [(6, 0, invoices.ids)]
for tx in self.filtered(lambda tx: tx.sale_order_ids):
# Create invoices
tx = tx.with_company(tx.company_id).with_context(company_id=tx.company_id.id)
confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state in ('sale', 'done'))
if confirmed_orders:
confirmed_orders._force_lines_to_invoice_policy_order()
invoices = confirmed_orders.with_context(
raise_if_nothing_to_invoice=False
)._create_invoices()
# Setup access token in advance to avoid serialization failure between
# edi postprocessing of invoice and displaying the sale order on the portal
for invoice in invoices:
invoice._portal_ensure_token()
tx.invoice_ids = [Command.set(invoices.ids)]
@api.model
def _compute_reference_prefix(self, provider, separator, **values):
+10 -6
View File
@@ -813,16 +813,20 @@ class SaleOrder(models.Model):
return {'done', 'cancel'}
def _send_order_confirmation_mail(self):
if not self:
return
if self.env.su:
# sending mail in sudo was meant for it being sent from superuser
self = self.with_user(SUPERUSER_ID)
mail_template = self._find_mail_template(force_confirmation_template=True)
if mail_template:
for order in self:
order.with_context(force_send=True).message_post_with_template(
mail_template.id, composition_mode='comment',
email_layout_xmlid="mail.mail_notification_paynow"
)
self.with_context(force_send=True).message_post_with_template(
mail_template.id,
composition_mode='comment',
email_layout_xmlid='mail.mail_notification_paynow',
)
def action_done(self):
for order in self:
@@ -1070,7 +1074,7 @@ class SaleOrder(models.Model):
invoice_vals['invoice_line_ids'] += invoice_line_vals
invoice_vals_list.append(invoice_vals)
if not invoice_vals_list:
if not invoice_vals_list and self._context.get('raise_if_nothing_to_invoice', True):
raise UserError(order._nothing_to_invoice_error_message())
# 2) Manage 'grouped' parameter: group by (partner_id, currency_id).
+61
View File
@@ -81,6 +81,7 @@ class TestSalePayment(PaymentHttpCommon):
# Check validation of transaction correctly confirms the SO
self.assertEqual(self.order.state, 'draft')
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
tx_sudo._finalize_post_processing()
self.assertEqual(self.order.state, 'sale')
@@ -120,6 +121,7 @@ class TestSalePayment(PaymentHttpCommon):
self.assertEqual(tx_sudo.company_id, self.order.company_id)
self.assertEqual(tx_sudo.currency_id, self.order.currency_id)
self.assertEqual(tx_sudo.reference, self.reference)
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
with mute_logger('odoo.addons.sale.models.payment_transaction'):
@@ -194,3 +196,62 @@ class TestSalePayment(PaymentHttpCommon):
tx_sudo._finalize_post_processing()
self.assertEqual(self.order.state, 'draft', 'a partial transaction with automatic invoice and invoice_policy = delivery should not validate a quote')
def test_auto_confirm_and_auto_invoice(self):
# Set automatic invoice
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
# Create the payment
self.amount = self.order.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.order.id], state='done')
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx._reconcile_after_done()
self.assertEqual(self.order.state, 'sale')
self.assertTrue(tx.invoice_ids)
self.assertTrue(self.order.invoice_ids)
def test_auto_done_and_auto_invoice(self):
# Set automatic invoice
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
# Lock the sale orders when confirmed
self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')
# Create the payment
self.amount = self.order.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.order.id], state='done')
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx._reconcile_after_done()
self.assertEqual(self.order.state, 'done')
self.assertTrue(tx.invoice_ids)
self.assertTrue(self.order.invoice_ids)
def test_so_partial_payment_no_invoice(self):
# Set automatic invoice
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
# Create the payment
self.amount = self.order.amount_total / 10.
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.order.id], state='done')
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx._reconcile_after_done()
self.assertEqual(self.order.state, 'draft')
self.assertFalse(tx.invoice_ids)
self.assertFalse(self.order.invoice_ids)
def test_already_confirmed_so_payment(self):
# Set automatic invoice
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
# Confirm order before payment
self.order.action_confirm()
# Create the payment
self.amount = self.order.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.order.id], state='done')
tx._reconcile_after_done()
self.assertTrue(tx.invoice_ids)
self.assertTrue(self.order.invoice_ids)