mirror of
https://github.com/bringout/oca-financial.git
synced 2026-04-26 19:22:04 +02:00
389 lines
16 KiB
Python
389 lines
16 KiB
Python
# Copyright 2019 Tecnativa - David Vidal
|
|
# Copyright 2020-2021 Tecnativa - Pedro M. Baeza
|
|
# Copyright 2021 Tecnativa - Víctor Martínez
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
from odoo import _, api, exceptions, fields, models
|
|
from odoo.tools import config
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = "account.move"
|
|
|
|
# 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(
|
|
string="Invoice Global Discounts (readonly)",
|
|
related="global_discount_ids",
|
|
readonly=True,
|
|
)
|
|
global_discount_ids = fields.Many2many(
|
|
comodel_name="global.discount",
|
|
column1="invoice_id",
|
|
column2="global_discount_id",
|
|
string="Invoice Global Discounts",
|
|
domain="[('discount_scope', 'in', {"
|
|
" 'out_invoice': ['sale'], "
|
|
" 'out_refund': ['sale'], "
|
|
" 'in_refund': ['purchase'], "
|
|
" 'in_invoice': ['purchase']"
|
|
"}.get(move_type, [])), ('account_id', '!=', False), '|', "
|
|
"('company_id', '=', company_id), ('company_id', '=', False)]",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
)
|
|
amount_global_discount = fields.Monetary(
|
|
string="Total Global Discounts",
|
|
compute="_compute_amount",
|
|
currency_field="currency_id",
|
|
readonly=True,
|
|
compute_sudo=True,
|
|
store=True,
|
|
)
|
|
amount_untaxed_before_global_discounts = fields.Monetary(
|
|
string="Amount Untaxed Before Discounts",
|
|
compute="_compute_amount",
|
|
currency_field="currency_id",
|
|
readonly=True,
|
|
compute_sudo=True,
|
|
store=True,
|
|
)
|
|
invoice_global_discount_ids = fields.One2many(
|
|
comodel_name="account.invoice.global.discount",
|
|
inverse_name="invoice_id",
|
|
readonly=True,
|
|
)
|
|
|
|
def _prepare_global_discount_vals(self, global_discount, base, tax_ids):
|
|
"""Prepare the dictionary values for an invoice global discount
|
|
line.
|
|
"""
|
|
self.ensure_one()
|
|
discount = global_discount._get_global_discount_vals(base)
|
|
return {
|
|
"name": global_discount.display_name,
|
|
"invoice_id": self.id,
|
|
"global_discount_id": global_discount.id,
|
|
"discount": global_discount.discount,
|
|
"base": base,
|
|
"base_discounted": discount["base_discounted"],
|
|
"account_id": global_discount.account_id.id,
|
|
"tax_ids": [(4, tax_id) for tax_id in tax_ids],
|
|
}
|
|
|
|
def _set_global_discounts_by_tax(self):
|
|
"""Create invoice global discount lines by taxes combinations and
|
|
discounts.
|
|
|
|
This also resets previous global discounts in case they existed.
|
|
"""
|
|
self.ensure_one()
|
|
if not self.is_invoice():
|
|
return
|
|
in_draft_mode = self != self._origin
|
|
taxes_keys = {}
|
|
# Perform a sanity check for discarding cases that will lead to
|
|
# incorrect data in discounts
|
|
_self = self.filtered("global_discount_ids")
|
|
for inv_line in _self.invoice_line_ids.filtered(
|
|
lambda l: l.display_type not in ["line_section", "line_note"]
|
|
):
|
|
if inv_line.product_id.bypass_global_discount:
|
|
continue
|
|
taxes_keys.setdefault(tuple(inv_line.tax_ids.ids), 0)
|
|
taxes_keys[tuple(inv_line.tax_ids.ids)] += inv_line.price_subtotal
|
|
# Reset previous global discounts
|
|
self.invoice_global_discount_ids -= self.invoice_global_discount_ids
|
|
model = "account.invoice.global.discount"
|
|
create_method = in_draft_mode and self.env[model].new or self.env[model].create
|
|
for tax_line in _self.line_ids.filtered("tax_line_id"):
|
|
key = []
|
|
discount_line_base = 0
|
|
for key in taxes_keys:
|
|
if tax_line.tax_line_id.id in key:
|
|
discount_line_base = taxes_keys[key]
|
|
taxes_keys[key] = 0 # mark for not duplicating
|
|
break # we leave in key variable the proper taxes value
|
|
if not discount_line_base:
|
|
continue
|
|
for global_discount in self.global_discount_ids:
|
|
vals = self._prepare_global_discount_vals(
|
|
global_discount, discount_line_base, key
|
|
)
|
|
create_method(vals)
|
|
discount_line_base = vals["base_discounted"]
|
|
_self._set_global_discounts_by_zero_tax(taxes_keys, create_method)
|
|
|
|
def _set_global_discounts_by_zero_tax(self, taxes_keys, create_method):
|
|
# Check all moves with defined taxes to check if there's any discount not
|
|
# created (tax amount is zero and only one tax is applied)
|
|
base_total = 0
|
|
zero_taxes = self.env["account.tax"]
|
|
for line in self.line_ids.filtered("tax_ids"):
|
|
if line.product_id.bypass_global_discount:
|
|
continue
|
|
key = tuple(line.tax_ids.ids)
|
|
if taxes_keys.get(key):
|
|
base_total += line.price_subtotal
|
|
zero_taxes |= line.tax_ids
|
|
for global_discount in self.global_discount_ids:
|
|
if not base_total:
|
|
break
|
|
vals = self._prepare_global_discount_vals(
|
|
global_discount, base_total, zero_taxes.ids
|
|
)
|
|
create_method(vals)
|
|
base_total = vals["base_discounted"]
|
|
|
|
def _recompute_global_discount_lines(self):
|
|
"""Append global discounts move lines.
|
|
|
|
This is called when recomputing dynamic lines before calling
|
|
`_recompute_payment_terms_lines`, but after calling `_recompute_tax_lines`.
|
|
"""
|
|
self.ensure_one()
|
|
in_draft_mode = self != self._origin
|
|
model = "account.move.line"
|
|
create_method = in_draft_mode and self.env[model].new or self.env[model].create
|
|
for discount in self.invoice_global_discount_ids.filtered("discount"):
|
|
sign = -1 if self.move_type in {"in_invoice", "out_refund"} else 1
|
|
disc_amount = sign * discount.discount_amount
|
|
disc_amount_company_currency = disc_amount
|
|
if self.currency_id != self.company_id.currency_id:
|
|
disc_amount_company_currency = self.currency_id._convert(
|
|
disc_amount,
|
|
self.company_id.currency_id,
|
|
self.company_id,
|
|
self.date or fields.Date.context_today(self),
|
|
)
|
|
create_method(
|
|
{
|
|
"invoice_global_discount_id": discount.id,
|
|
"move_id": self.id,
|
|
"name": "%s - %s"
|
|
% (discount.name, ", ".join(discount.tax_ids.mapped("name"))),
|
|
"debit": disc_amount_company_currency > 0.0
|
|
and disc_amount_company_currency
|
|
or 0.0,
|
|
"credit": disc_amount_company_currency < 0.0
|
|
and -disc_amount_company_currency
|
|
or 0.0,
|
|
"amount_currency": (disc_amount > 0.0 and disc_amount or 0.0)
|
|
- (disc_amount < 0.0 and -disc_amount or 0.0),
|
|
"account_id": discount.account_id.id,
|
|
"tax_ids": [(4, x.id) for x in discount.tax_ids],
|
|
"partner_id": self.commercial_partner_id.id,
|
|
"currency_id": self.currency_id.id,
|
|
"price_unit": -1 * abs(disc_amount_company_currency),
|
|
}
|
|
)
|
|
|
|
@api.onchange("partner_id", "company_id")
|
|
def _onchange_partner_id(self):
|
|
res = super()._onchange_partner_id()
|
|
discounts = False
|
|
if (
|
|
self.move_type in ["out_invoice", "out_refund"]
|
|
and self.partner_id.customer_global_discount_ids
|
|
):
|
|
discounts = self.partner_id.customer_global_discount_ids.filtered(
|
|
lambda d: d.company_id == self.company_id
|
|
)
|
|
elif (
|
|
self.move_type in ["in_refund", "in_invoice"]
|
|
and self.partner_id.supplier_global_discount_ids
|
|
):
|
|
discounts = self.partner_id.supplier_global_discount_ids.filtered(
|
|
lambda d: d.company_id == self.company_id
|
|
)
|
|
if discounts:
|
|
self.global_discount_ids = discounts
|
|
return res
|
|
|
|
def _compute_amount_one(self):
|
|
"""Perform totals computation of a move with global discounts."""
|
|
if not self.invoice_global_discount_ids:
|
|
self.amount_global_discount = 0.0
|
|
self.amount_untaxed_before_global_discounts = 0.0
|
|
return
|
|
round_curr = self.currency_id.round
|
|
self.amount_global_discount = sum(
|
|
round_curr(discount.discount_amount) * -1
|
|
for discount in self.invoice_global_discount_ids
|
|
)
|
|
self.amount_untaxed_before_global_discounts = (
|
|
self.amount_untaxed - self.amount_global_discount
|
|
)
|
|
|
|
@api.depends(
|
|
"line_ids.matched_debit_ids.debit_move_id.move_id.payment_id.is_matched",
|
|
"line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual",
|
|
"line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency",
|
|
"line_ids.matched_credit_ids.credit_move_id.move_id.payment_id.is_matched",
|
|
"line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual",
|
|
"line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency",
|
|
"line_ids.balance",
|
|
"line_ids.currency_id",
|
|
"line_ids.amount_currency",
|
|
"line_ids.amount_residual",
|
|
"line_ids.amount_residual_currency",
|
|
"line_ids.payment_id.state",
|
|
"line_ids.full_reconcile_id",
|
|
"invoice_global_discount_ids",
|
|
"global_discount_ids",
|
|
)
|
|
def _compute_amount(self):
|
|
"""Modify totals computation for including global discounts."""
|
|
res = super()._compute_amount()
|
|
for record in self:
|
|
record._compute_amount_one()
|
|
return res
|
|
|
|
def _clean_global_discount_lines(self):
|
|
self.ensure_one()
|
|
gbl_disc_lines = self.env["account.move.line"].search(
|
|
[
|
|
("move_id", "=", self.id),
|
|
"|",
|
|
("global_discount_item", "=", True),
|
|
("invoice_global_discount_id", "!=", False),
|
|
]
|
|
)
|
|
if gbl_disc_lines:
|
|
move_container = {"records": self}
|
|
with self._check_balanced(move_container), self._sync_dynamic_lines(
|
|
move_container
|
|
):
|
|
gbl_disc_lines.unlink()
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""If we create the invoice with the discounts already set like from
|
|
a sales order, we must compute the global discounts as well, as some data
|
|
like ``tax_ids`` is not set until the final step.
|
|
"""
|
|
moves = super().create(vals_list)
|
|
for move in moves:
|
|
if move.move_type in ["out_refund", "in_refund"]:
|
|
move._clean_global_discount_lines()
|
|
move._set_global_discounts_by_tax()
|
|
move._recompute_global_discount_lines()
|
|
return moves
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
if "invoice_line_ids" in vals or "global_discount_ids" in vals:
|
|
for move in self:
|
|
move._clean_global_discount_lines()
|
|
move._set_global_discounts_by_tax()
|
|
move._recompute_global_discount_lines()
|
|
move_container = {"records": self}
|
|
self._global_discount_check(move_container)
|
|
return res
|
|
|
|
def _global_discount_check(self, container):
|
|
test_condition = not config["test_enable"] or self.env.context.get(
|
|
"test_account_global_discount"
|
|
)
|
|
for move in container["records"]:
|
|
if not move.is_invoice() or not move.global_discount_ids:
|
|
continue
|
|
taxes_keys = {}
|
|
for inv_line in move.invoice_line_ids:
|
|
if inv_line.display_type != "product":
|
|
continue
|
|
if not inv_line.tax_ids and test_condition:
|
|
raise exceptions.UserError(
|
|
_("With global discounts, taxes in lines are required.")
|
|
)
|
|
for key in taxes_keys:
|
|
if key == tuple(inv_line.tax_ids.ids):
|
|
break
|
|
elif set(key) & set(inv_line.tax_ids.ids) and test_condition:
|
|
raise exceptions.UserError(
|
|
_("Incompatible taxes found for global discounts.")
|
|
)
|
|
else:
|
|
taxes_keys[tuple(inv_line.tax_ids.ids)] = True
|
|
return True
|
|
|
|
|
|
class AccountMoveLine(models.Model):
|
|
_inherit = "account.move.line"
|
|
|
|
invoice_global_discount_id = fields.Many2one(
|
|
comodel_name="account.invoice.global.discount",
|
|
string="Invoice Global Discount",
|
|
)
|
|
base_before_global_discounts = fields.Monetary(
|
|
string="Amount Untaxed Before Discounts",
|
|
readonly=True,
|
|
)
|
|
# TODO: To be removed on future versions if invoice_global_discount_id is properly filled
|
|
# Provided for compatibility in stable branch
|
|
# UPD: can be removed past version 16.0
|
|
global_discount_item = fields.Boolean()
|
|
|
|
|
|
class AccountInvoiceGlobalDiscount(models.Model):
|
|
_name = "account.invoice.global.discount"
|
|
_description = "Invoice Global Discount"
|
|
|
|
name = fields.Char(string="Discount Name", required=True)
|
|
invoice_id = fields.Many2one(
|
|
"account.move",
|
|
string="Invoice",
|
|
ondelete="cascade",
|
|
index=True,
|
|
readonly=True,
|
|
domain=[
|
|
(
|
|
"move_type",
|
|
"in",
|
|
["out_invoice", "out_refund", "in_invoice", "in_refund"],
|
|
)
|
|
],
|
|
)
|
|
global_discount_id = fields.Many2one(
|
|
comodel_name="global.discount",
|
|
string="Global Discount",
|
|
)
|
|
discount = fields.Float(string="Discount (number)")
|
|
discount_display = fields.Char(
|
|
compute="_compute_discount_display",
|
|
string="Discount",
|
|
)
|
|
base = fields.Float(string="Base before discount", digits="Product Price")
|
|
base_discounted = fields.Float(string="Base after discount", digits="Product Price")
|
|
currency_id = fields.Many2one(related="invoice_id.currency_id", readonly=True)
|
|
discount_amount = fields.Monetary(
|
|
string="Discounted Amount",
|
|
compute="_compute_discount_amount",
|
|
currency_field="currency_id",
|
|
compute_sudo=True,
|
|
)
|
|
tax_ids = fields.Many2many(comodel_name="account.tax", string="Taxes")
|
|
account_id = fields.Many2one(
|
|
comodel_name="account.account",
|
|
required=True,
|
|
string="Account",
|
|
domain="[('account_type', 'not in', ['asset_receivable', 'liability_payable'])]",
|
|
)
|
|
account_analytic_id = fields.Many2one(
|
|
comodel_name="account.analytic.account",
|
|
string="Analytic account",
|
|
)
|
|
company_id = fields.Many2one(related="invoice_id.company_id", readonly=True)
|
|
|
|
def _compute_discount_display(self):
|
|
"""Given a discount type, we need to render a different symbol"""
|
|
for one in self:
|
|
precision = self.env["decimal.precision"].precision_get("Discount")
|
|
one.discount_display = "{0:.{1}f}%".format(one.discount * -1, precision)
|
|
|
|
@api.depends("base", "base_discounted")
|
|
def _compute_discount_amount(self):
|
|
"""Compute the amount discounted"""
|
|
for one in self:
|
|
one.discount_amount = one.base - one.base_discounted
|