mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 19:52:06 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -1,13 +1,11 @@
|
|||
# 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, str2bool
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo import SUPERUSER_ID, Command, _, api, fields, models
|
||||
from odoo.tools import str2bool
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
|
|
@ -21,10 +19,12 @@ class PaymentTransaction(models.Model):
|
|||
self.ensure_one()
|
||||
if self.provider_id.so_reference_type == 'so_name':
|
||||
order_reference = order.name
|
||||
else:
|
||||
# self.provider_id.so_reference_type == 'partner'
|
||||
elif self.provider_id.so_reference_type == 'partner':
|
||||
identification_number = order.partner_id.id
|
||||
order_reference = '%s/%s' % ('CUST', str(identification_number % 97).rjust(2, '0'))
|
||||
else:
|
||||
# self.provider_id.so_reference_type is empty
|
||||
order_reference = False
|
||||
|
||||
invoice_journal = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
if invoice_journal:
|
||||
|
|
@ -37,73 +37,95 @@ class PaymentTransaction(models.Model):
|
|||
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.
|
||||
def _post_process(self):
|
||||
""" Override of `payment` to add Sales-specific logic to the post-processing.
|
||||
|
||||
:param str state_message: The reason for which the transaction is set in 'pending' state.
|
||||
:return: updated transactions.
|
||||
:rtype: `payment.transaction` recordset.
|
||||
In particular, for pending transactions, we send the quotation by email; for authorized
|
||||
transactions, we confirm the quotation; for confirmed transactions, we automatically confirm
|
||||
the quotation and generate invoices.
|
||||
"""
|
||||
txs_to_process = super()._set_pending(state_message=state_message)
|
||||
|
||||
for tx in txs_to_process: # Consider only transactions that are indeed set pending.
|
||||
sales_orders = tx.sale_order_ids.filtered(lambda so: so.state in ['draft', 'sent'])
|
||||
for pending_tx in self.filtered(lambda tx: tx.state == 'pending'):
|
||||
super(PaymentTransaction, pending_tx)._post_process()
|
||||
sales_orders = pending_tx.sale_order_ids.filtered(
|
||||
lambda so: so.state in ['draft', 'sent']
|
||||
)
|
||||
sales_orders.filtered(
|
||||
lambda so: so.state == 'draft'
|
||||
).with_context(tracking_disable=True).action_quotation_sent()
|
||||
|
||||
if tx.provider_id.code == 'custom':
|
||||
for so in tx.sale_order_ids:
|
||||
so.reference = tx._compute_sale_order_reference(so)
|
||||
# send order confirmation mail.
|
||||
sales_orders._send_order_confirmation_mail()
|
||||
if pending_tx.provider_id.code == 'custom':
|
||||
for order in pending_tx.sale_order_ids:
|
||||
order.reference = pending_tx._compute_sale_order_reference(order)
|
||||
|
||||
return txs_to_process
|
||||
if pending_tx.operation == 'validation':
|
||||
continue
|
||||
# Send the payment status email.
|
||||
# The transactions are manually cached while in a sudoed environment to prevent an
|
||||
# AccessError: In some circumstances, sending the mail would generate the report assets
|
||||
# during the rendering of the mail body, causing a cursor commit, a flush, and forcing
|
||||
# the re-computation of the pending computed fields of the `mail.compose.message`,
|
||||
# including part of the template. Since that template reads the order's transactions and
|
||||
# the re-computation of the field is not done with the same environment, reading fields
|
||||
# that were not already available in the cache could trigger an AccessError (e.g., if
|
||||
# the payment was initiated by a public user).
|
||||
sales_orders.mapped('transaction_ids')
|
||||
sales_orders._send_payment_succeeded_for_order_mail()
|
||||
|
||||
for authorized_tx in self.filtered(lambda tx: tx.state == 'authorized'):
|
||||
super(PaymentTransaction, authorized_tx)._post_process()
|
||||
confirmed_orders = authorized_tx._check_amount_and_confirm_order()
|
||||
if authorized_tx.operation == 'validation':
|
||||
continue
|
||||
if remaining_orders := (authorized_tx.sale_order_ids - confirmed_orders):
|
||||
remaining_orders._send_payment_succeeded_for_order_mail()
|
||||
|
||||
super(PaymentTransaction, self.filtered(
|
||||
lambda tx: tx.state not in ['pending', 'authorized', 'done'])
|
||||
)._post_process()
|
||||
|
||||
for done_tx in self.filtered(lambda tx: tx.state == 'done'):
|
||||
if done_tx.operation != 'validation':
|
||||
confirmed_orders = done_tx._check_amount_and_confirm_order()
|
||||
(done_tx.sale_order_ids - confirmed_orders)._send_payment_succeeded_for_order_mail()
|
||||
|
||||
auto_invoice = str2bool(
|
||||
self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice')
|
||||
)
|
||||
if auto_invoice:
|
||||
# Invoice the sales orders of confirmed transactions instead of only confirmed
|
||||
# orders to create the invoice even if only a partial payment was made.
|
||||
done_tx._invoice_sale_orders()
|
||||
super(PaymentTransaction, done_tx)._post_process() # Post the invoices.
|
||||
if auto_invoice and not self.env.context.get('skip_sale_auto_invoice_send'):
|
||||
if (
|
||||
str2bool(self.env['ir.config_parameter'].sudo().get_param('sale.async_emails'))
|
||||
and (send_invoice_cron := self.env.ref('sale.send_invoice_cron', raise_if_not_found=False))
|
||||
):
|
||||
send_invoice_cron._trigger()
|
||||
else:
|
||||
self._send_invoice()
|
||||
|
||||
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.
|
||||
Confirm the sales orders only if the transaction amount (or the sum of the partial
|
||||
transaction amounts) is equal to or greater than the required amount for order confirmation
|
||||
|
||||
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.
|
||||
# We only support the flow where exactly one quotation is linked to a transaction.
|
||||
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_code,
|
||||
'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),
|
||||
},
|
||||
)
|
||||
if quotation and quotation._is_confirmation_amount_reached():
|
||||
quotation.with_context(send_email=True).action_confirm()
|
||||
confirmed_orders |= quotation
|
||||
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.
|
||||
|
||||
|
|
@ -113,35 +135,18 @@ class PaymentTransaction(models.Model):
|
|||
:return: None
|
||||
"""
|
||||
super()._log_message_on_linked_documents(message)
|
||||
self = self.with_user(SUPERUSER_ID) # Log messages as 'OdooBot'
|
||||
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()
|
||||
|
||||
auto_invoice = str2bool(
|
||||
self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'))
|
||||
if auto_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()
|
||||
super()._reconcile_after_done()
|
||||
if auto_invoice:
|
||||
# Must be called after the super() call to make sure the invoice are correctly posted.
|
||||
self._send_invoice()
|
||||
if self.env.uid == SUPERUSER_ID or self.env.context.get('payment_backend_action'):
|
||||
author = self.env.user.partner_id
|
||||
else:
|
||||
author = self.partner_id
|
||||
for order in self.sale_order_ids or self.source_transaction_id.sale_order_ids:
|
||||
order.message_post(body=message, author_id=author.id)
|
||||
|
||||
def _send_invoice(self):
|
||||
template_id = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'sale.default_invoice_email_template'
|
||||
)
|
||||
if not template_id:
|
||||
return
|
||||
template_id = int(template_id)
|
||||
template = self.env['mail.template'].browse(template_id)
|
||||
for tx in self:
|
||||
# Send messages as OdooBot so that
|
||||
# * logged in users receive the invoice
|
||||
# * the mail and notifications are not sent by the public user
|
||||
for tx in self.with_user(SUPERUSER_ID):
|
||||
tx = tx.with_company(tx.company_id).with_context(
|
||||
company_id=tx.company_id.id,
|
||||
)
|
||||
|
|
@ -149,15 +154,22 @@ class PaymentTransaction(models.Model):
|
|||
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:
|
||||
lang = template._render_lang(invoice.ids)[invoice.id]
|
||||
model_desc = invoice.with_context(lang=lang).type_name
|
||||
invoice.with_context(model_description=model_desc).with_user(
|
||||
SUPERUSER_ID
|
||||
).message_post_with_template(
|
||||
template_id=template_id,
|
||||
email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
|
||||
)
|
||||
|
||||
send_context = {'allow_raising': False, 'allow_fallback_pdf': True}
|
||||
default_template_param = (
|
||||
self.env['ir.config_parameter']
|
||||
.sudo()
|
||||
.get_param('sale.default_invoice_email_template', False)
|
||||
)
|
||||
if default_template_param:
|
||||
mail_template = self.env['mail.template'].sudo().browse(int(default_template_param))
|
||||
if mail_template.exists():
|
||||
send_context['mail_template'] = mail_template
|
||||
|
||||
tx.env['account.move.send']._generate_and_send_invoices(
|
||||
invoice_to_send,
|
||||
**send_context,
|
||||
)
|
||||
|
||||
def _cron_send_invoice(self):
|
||||
"""
|
||||
|
|
@ -176,20 +188,31 @@ class PaymentTransaction(models.Model):
|
|||
('is_move_sent', '=', False),
|
||||
('state', '=', 'posted'),
|
||||
])),
|
||||
('sale_order_ids.state', 'in', ('sale', 'done')),
|
||||
('sale_order_ids.state', '=', 'sale'),
|
||||
('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'))
|
||||
tx = tx.with_company(tx.company_id)
|
||||
|
||||
confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state == 'sale')
|
||||
if confirmed_orders:
|
||||
confirmed_orders._force_lines_to_invoice_policy_order()
|
||||
invoices = confirmed_orders.with_context(
|
||||
# Filter orders between those fully paid and those partially paid.
|
||||
fully_paid_orders = confirmed_orders.filtered(lambda so: so._is_paid())
|
||||
|
||||
# Create a down payment invoice for partially paid orders
|
||||
downpayment_invoices = (
|
||||
confirmed_orders - fully_paid_orders
|
||||
)._generate_downpayment_invoices()
|
||||
|
||||
# For fully paid orders create a final invoice.
|
||||
fully_paid_orders._force_lines_to_invoice_policy_order()
|
||||
final_invoices = fully_paid_orders.with_context(
|
||||
raise_if_nothing_to_invoice=False
|
||||
)._create_invoices()
|
||||
)._create_invoices(final=True)
|
||||
invoices = downpayment_invoices + final_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:
|
||||
|
|
@ -197,14 +220,13 @@ class PaymentTransaction(models.Model):
|
|||
tx.invoice_ids = [Command.set(invoices.ids)]
|
||||
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, provider_code, separator, **values):
|
||||
def _compute_reference_prefix(self, 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_code: The code of the provider 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), ...], ...}.
|
||||
|
|
@ -218,8 +240,9 @@ class PaymentTransaction(models.Model):
|
|||
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_code, separator, **values)
|
||||
return super()._compute_reference_prefix(separator, **values)
|
||||
|
||||
@api.readonly
|
||||
def action_view_sales_orders(self):
|
||||
action = {
|
||||
'name': _('Sales Order(s)'),
|
||||
|
|
@ -232,6 +255,6 @@ class PaymentTransaction(models.Model):
|
|||
action['res_id'] = sale_order_ids[0]
|
||||
action['view_mode'] = 'form'
|
||||
else:
|
||||
action['view_mode'] = 'tree,form'
|
||||
action['view_mode'] = 'list,form'
|
||||
action['domain'] = [('id', 'in', sale_order_ids)]
|
||||
return action
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue