249 lines
10 KiB
Python
Executable File
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
|