Files
Odoo-18.0-20251222/sale_global_discount/models/sale_order.py
tocmo0nlord adbe430761
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled
Initial commit: Odoo 18.0-20251222 extra-addons
2026-03-13 20:43:25 +00:00

249 lines
10 KiB
Python
Executable File

# Copyright 2020 Tecnativa - David Vidal
# Copyright 2020 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, exceptions, fields, models
from odoo.tools.misc import formatLang
class SaleOrder(models.Model):
_inherit = "sale.order"
global_discount_ids = fields.Many2many(
comodel_name="global.discount",
string="Sale Global Discounts",
domain="[('discount_scope', '=', 'sale'), "
"('account_id', '!=', False), '|', "
"('company_id', '=', company_id), ('company_id', '=', False)]",
compute="_compute_global_discount_ids",
store=True,
readonly=False,
)
# HACK: Looks like UI doesn't behave well with Many2many fields and
# negative groups when the same field is shown. In this case, we want to
# show the readonly version to any not in the global discount group.
# TODO: Check if it's fixed in future versions
global_discount_ids_readonly = fields.Many2many(
related="global_discount_ids",
string="Sale Global Discounts (readonly)",
readonly=True,
)
amount_global_discount = fields.Monetary(
string="Total Global Discounts",
compute="_compute_amounts", # pylint: disable=C8108
currency_field="currency_id",
compute_sudo=True, # Odoo core fields are storable so compute_sudo is True
readonly=True,
store=True,
)
amount_untaxed_before_global_discounts = fields.Monetary(
string="Amount Untaxed Before Discounts",
compute="_compute_amounts", # pylint: disable=C8108
currency_field="currency_id",
compute_sudo=True, # Odoo core fields are storable so compute_sudo is True
readonly=True,
store=True,
)
amount_total_before_global_discounts = fields.Monetary(
string="Amount Total Before Discounts",
compute="_compute_amounts", # pylint: disable=C8108
currency_field="currency_id",
compute_sudo=True, # Odoo core fields are storable so compute_sudo is True
readonly=True,
store=True,
)
@api.model
def get_discounted_global(self, price=0, discounts=None):
if not discounts:
return price
discounted_price = price
for discount in discounts:
discounted_price *= 1 - (discount / 100)
return discounted_price
def _check_global_discounts_sanity(self):
"""Perform a sanity check for discarding cases that will lead to
incorrect data in discounts.
"""
self.ensure_one()
if not self.global_discount_ids:
return True
taxes_keys = {}
for line in self.order_line.filtered(
lambda _line: not _line.display_type
and _line.product_id
and not _line.product_id.bypass_global_discount
):
if not line.tax_id:
raise exceptions.UserError(
self.env._("With global discounts, taxes in lines are required.")
)
for key in taxes_keys:
if key == line.tax_id:
break
elif key & line.tax_id:
raise exceptions.UserError(
self.env._("Incompatible taxes found for global discounts.")
)
else:
taxes_keys[line.tax_id] = True
@api.depends(
"order_line.product_id.bypass_global_discount",
"order_line.price_subtotal",
"order_line.price_tax",
"order_line.price_total",
"global_discount_ids",
)
def _compute_amounts(self):
res = super()._compute_amounts()
for order in self:
order._check_global_discounts_sanity()
amount_untaxed_before_global_discounts = order.amount_untaxed
amount_total_before_global_discounts = order.amount_total
discounts = order.global_discount_ids.mapped("discount")
amount_discounted_untaxed = amount_discounted_tax = 0
for line in order.order_line:
discounted_subtotal = line.price_subtotal
if not line.product_id.bypass_global_discount:
discounted_subtotal = self.get_discounted_global(
line.price_subtotal, discounts.copy()
)
amount_discounted_untaxed += discounted_subtotal
discounted_tax = line.tax_id.with_context(
force_price_include=False
).compute_all(
discounted_subtotal,
line.order_id.currency_id,
1.0,
product=line.product_id,
partner=line.order_id.partner_shipping_id,
)
amount_discounted_tax += sum(
t.get("amount", 0.0) for t in discounted_tax.get("taxes", [])
)
order.update(
{
"amount_untaxed_before_global_discounts": (
amount_untaxed_before_global_discounts
),
"amount_total_before_global_discounts": (
amount_total_before_global_discounts
),
"amount_global_discount": (
amount_untaxed_before_global_discounts
- amount_discounted_untaxed
),
"amount_untaxed": amount_discounted_untaxed,
"amount_tax": amount_discounted_tax,
"amount_total": (amount_discounted_untaxed + amount_discounted_tax),
}
)
return res
def _compute_tax_totals(self):
res = super()._compute_tax_totals()
for order in self:
amount_discount_by_group = {}
cumulative_discount_rate = 1.0
currency = order.currency_id
# Calculate cumulative discount rate
for gbl_disc in order.global_discount_ids:
discount_rate = gbl_disc.discount / 100
cumulative_discount_rate *= 1 - discount_rate
base_amount = 0.0
# Calculate the total discount amount and discount by tax group
for line in order.order_line:
if line.display_type or not line.product_id:
continue
# Apply cumulative discount rate only if bypass_global_discount is False
if not line.product_id.bypass_global_discount:
discounted_price_subtotal = (
line.price_subtotal * cumulative_discount_rate
)
else:
discounted_price_subtotal = line.price_subtotal
base_amount += discounted_price_subtotal
# Calculate tax amounts for each tax group based on the discounted
# subtotal
for tax in line.tax_id:
tax_group_id = tax.tax_group_id.id
if tax_group_id not in amount_discount_by_group:
amount_discount_by_group[tax_group_id] = 0.0
# Compute taxes on the correct base amount
discounted_tax_vals = tax.with_context(
force_price_include=False
).compute_all(
discounted_price_subtotal,
currency,
1.0,
product=line.product_id,
partner=order.partner_shipping_id,
)
amount_discount_by_group[tax_group_id] += sum(
t.get("amount", 0.0)
for t in discounted_tax_vals.get("taxes", [])
)
# Calculate the final amount total
total_amount = base_amount + sum(amount_discount_by_group.values())
base_amount_currency = formatLang(
self.env, base_amount, currency_obj=currency
)
order.tax_totals.update(
{
"base_amount": base_amount,
"total_amount": total_amount,
"base_amount_currency": base_amount,
"total_amount_currency": total_amount,
}
)
# Update subtotals and groups by subtotal
for group in order.tax_totals["subtotals"]:
group.update(
{
"base_amount": base_amount,
"base_amount_currency": base_amount,
"amount": base_amount,
"formatted_amount": base_amount_currency,
}
)
for tax_group in group["tax_groups"]:
discounted_tax_amount = amount_discount_by_group.get(
tax_group["id"], 0.0
)
tax_group.update(
{
"base_amount": base_amount,
"base_amount_currency": base_amount,
"tax_amount": discounted_tax_amount,
"tax_amount_currency": discounted_tax_amount,
}
)
return res
@api.depends("partner_id", "company_id")
def _compute_global_discount_ids(self):
for order in self:
commercial_global_disc = (
order.partner_id.commercial_partner_id.customer_global_discount_ids
)
_discounts = (
commercial_global_disc
if commercial_global_disc
else order.partner_id.customer_global_discount_ids
)
discounts = self.env["global.discount"]
for discount in _discounts:
if discount.company_id == order.company_id:
discounts |= discount
order.global_discount_ids = discounts
def _prepare_invoice(self):
invoice_vals = super()._prepare_invoice()
if self.global_discount_ids:
invoice_vals.update(
{"global_discount_ids": [(6, 0, self.global_discount_ids.ids)]}
)
return invoice_vals