Before this commit, orders were not confirmed when there was more than
one transaction linked to them, regardless of their state.
As of now, we check if there is only one confirmed transaction (meaning
that the state of the transaction is either 'authorized' or 'done').
Fix 4d2b04a843
task-2906040
closes odoo/odoo#95443
Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
214 lines
9.9 KiB
Python
214 lines
9.9 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import logging
|
|
|
|
from datetime import datetime
|
|
from dateutil import relativedelta
|
|
|
|
from odoo import _, api, Command, fields, models, SUPERUSER_ID
|
|
from odoo.tools import format_amount
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentTransaction(models.Model):
|
|
_inherit = 'payment.transaction'
|
|
|
|
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')
|
|
|
|
def _compute_sale_order_reference(self, order):
|
|
self.ensure_one()
|
|
if self.acquirer_id.so_reference_type == 'so_name':
|
|
return order.name
|
|
else:
|
|
# self.acquirer_id.so_reference_type == 'partner'
|
|
identification_number = order.partner_id.id
|
|
return '%s/%s' % ('CUST', str(identification_number % 97).rjust(2, '0'))
|
|
|
|
@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)
|
|
|
|
def _set_pending(self, state_message=None):
|
|
""" Override of payment to send the quotations automatically. """
|
|
super(PaymentTransaction, self)._set_pending(state_message=state_message)
|
|
|
|
for record in self:
|
|
sales_orders = record.sale_order_ids.filtered(lambda so: so.state in ['draft', 'sent'])
|
|
sales_orders.filtered(lambda so: so.state == 'draft').with_context(tracking_disable=True).write({'state': 'sent'})
|
|
|
|
if record.acquirer_id.provider == 'transfer':
|
|
for so in record.sale_order_ids:
|
|
so.reference = record._compute_sale_order_reference(so)
|
|
# send order confirmation mail
|
|
sales_orders._send_order_confirmation_mail()
|
|
|
|
def _check_amount_and_confirm_order(self):
|
|
""" 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.filtered(
|
|
lambda tx: tx.state in ('authorized', 'done') # Only consider confirmed tx
|
|
)) == 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)
|
|
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.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param str message: The message to be logged
|
|
:return: None
|
|
"""
|
|
super()._log_message_on_linked_documents(message)
|
|
for order in self.sale_order_ids:
|
|
order.message_post(body=message)
|
|
|
|
def _reconcile_after_done(self):
|
|
""" Override of payment to automatically confirm quotations and generate invoices. """
|
|
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):
|
|
template_id = self.env['ir.config_parameter'].sudo().get_param(
|
|
'sale.default_invoice_email_template'
|
|
)
|
|
if not template_id:
|
|
return
|
|
|
|
for tx in self:
|
|
tx = tx.with_company(tx.company_id).with_context(
|
|
company_id=tx.company_id.id,
|
|
)
|
|
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(template_id),
|
|
email_layout_xmlid='mail.mail_notification_paynow',
|
|
)
|
|
|
|
def _cron_send_invoice(self):
|
|
"""
|
|
Cron to send invoice that where not ready to be send directly after posting
|
|
"""
|
|
if not self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
|
|
return
|
|
|
|
# No need to retrieve old transactions
|
|
retry_limit_date = datetime.now() - relativedelta.relativedelta(days=2)
|
|
# Retrieve all transactions matching the criteria for post-processing
|
|
self.search([
|
|
('state', '=', 'done'),
|
|
('is_post_processed', '=', True),
|
|
('invoice_ids', 'in', self.env['account.move']._search([
|
|
('is_move_sent', '=', False),
|
|
('state', '=', 'posted'),
|
|
])),
|
|
('sale_order_ids.state', 'in', ('sale', 'done')),
|
|
('last_state_change', '>=', retry_limit_date),
|
|
])._send_invoice()
|
|
|
|
def _invoice_sale_orders(self):
|
|
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):
|
|
""" Override of payment to compute the reference prefix based on Sales-specific values.
|
|
|
|
If the `values` parameter has an entry with 'sale_order_ids' as key and a list of (4, id, O)
|
|
or (6, 0, ids) X2M command as value, the prefix is computed based on the sales order name(s)
|
|
Otherwise, the computation is delegated to the super method.
|
|
|
|
:param str provider: The provider of the acquirer handling the transaction
|
|
:param str separator: The custom separator used to separate data references
|
|
:param dict values: The transaction values used to compute the reference prefix. It should
|
|
have the structure {'sale_order_ids': [(X2M command), ...], ...}.
|
|
:return: The computed reference prefix if order ids are found, the one of `super` otherwise
|
|
:rtype: str
|
|
"""
|
|
command_list = values.get('sale_order_ids')
|
|
if command_list:
|
|
# Extract sales order id(s) from the X2M commands
|
|
order_ids = self._fields['sale_order_ids'].convert_to_cache(command_list, self)
|
|
orders = self.env['sale.order'].browse(order_ids).exists()
|
|
if len(orders) == len(order_ids): # All ids are valid
|
|
return separator.join(orders.mapped('name'))
|
|
return super()._compute_reference_prefix(provider, separator, **values)
|
|
|
|
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:
|
|
action['view_mode'] = 'tree,form'
|
|
action['domain'] = [('id', 'in', sale_order_ids)]
|
|
return action
|