147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
from odoo import api, fields, models
|
|
from odoo.tools.float_utils import float_is_zero
|
|
|
|
|
|
class AccountMoveLine(models.Model):
|
|
_inherit = "account.move.line"
|
|
|
|
price_subtotal = fields.Monetary(
|
|
string="Subtotal",
|
|
compute="_compute_totals",
|
|
store=True,
|
|
readonly=False,
|
|
currency_field="currency_id",
|
|
)
|
|
price_total = fields.Monetary(
|
|
string="Total",
|
|
compute="_compute_totals",
|
|
store=True,
|
|
readonly=False,
|
|
currency_field="currency_id",
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Helpers
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _is_vendor_bill_price_editable_line(self):
|
|
self.ensure_one()
|
|
return (
|
|
self.move_id.move_type in ("in_invoice", "in_refund")
|
|
and self.move_id.state == "draft"
|
|
and self.display_type == "product"
|
|
and not float_is_zero(self.quantity or 0.0, precision_rounding=(self.product_uom_id or self.product_id.uom_id or self.env.ref("uom.product_uom_unit")).rounding)
|
|
and not self.env.context.get("skip_vendor_bill_price_edit")
|
|
)
|
|
|
|
def _get_price_edit_currency(self):
|
|
return self.currency_id or self.company_currency_id or self.env.company.currency_id
|
|
|
|
def _compute_amount_from_current_price_unit(self, target_key):
|
|
self.ensure_one()
|
|
currency = self._get_price_edit_currency()
|
|
discount_factor = 1 - (self.discount or 0.0) / 100.0
|
|
qty = self.quantity or 0.0
|
|
price_after_discount = self.price_unit * discount_factor
|
|
taxes = self.tax_ids
|
|
partner = self.partner_id or self.move_id.partner_id
|
|
if taxes:
|
|
res = taxes.compute_all(
|
|
price_after_discount,
|
|
quantity=qty,
|
|
currency=currency,
|
|
product=self.product_id,
|
|
partner=partner,
|
|
is_refund=self.is_refund,
|
|
)
|
|
return res["total_included" if target_key == "total_included" else "total_excluded"]
|
|
return price_after_discount * qty
|
|
|
|
def _compute_price_unit_from_target_amount(self, target_amount, target_key):
|
|
"""Return a price_unit producing the target amount.
|
|
|
|
:param target_amount: desired subtotal/total value (sign included)
|
|
:param target_key: 'total_excluded' or 'total_included'
|
|
"""
|
|
currency = self._get_price_edit_currency()
|
|
qty = self.quantity or 0.0
|
|
if float_is_zero(qty, precision_rounding=currency.rounding or 0.01):
|
|
return False
|
|
|
|
discount_factor = 1 - (self.discount or 0.0) / 100.0
|
|
if float_is_zero(discount_factor, precision_digits=6):
|
|
return False
|
|
|
|
partner = self.partner_id or self.move_id.partner_id
|
|
taxes = self.tax_ids
|
|
target_abs = abs(target_amount)
|
|
sign = 1 if target_amount >= 0 else -1
|
|
|
|
def compute_total(base_price):
|
|
if taxes:
|
|
res = taxes.compute_all(
|
|
base_price,
|
|
quantity=qty,
|
|
currency=currency,
|
|
product=self.product_id,
|
|
partner=partner,
|
|
is_refund=self.is_refund,
|
|
)
|
|
value = res["total_included" if target_key == "total_included" else "total_excluded"]
|
|
else:
|
|
value = base_price * qty
|
|
return abs(value)
|
|
|
|
if float_is_zero(target_abs, precision_rounding=currency.rounding or 0.01):
|
|
base_after_discount = 0.0
|
|
else:
|
|
price_guess = abs(self.price_unit * discount_factor)
|
|
per_unit_target = target_abs / max(qty, 1.0)
|
|
high = max(price_guess * 2.0, per_unit_target * 2.0, 1.0)
|
|
low = 0.0
|
|
base_after_discount = None
|
|
for _ in range(40):
|
|
mid = (low + high) / 2.0
|
|
computed = compute_total(sign * mid)
|
|
if abs(computed - target_abs) <= (currency.rounding or 0.01):
|
|
base_after_discount = mid
|
|
break
|
|
if computed > target_abs:
|
|
high = mid
|
|
else:
|
|
low = mid
|
|
if base_after_discount is None:
|
|
base_after_discount = mid
|
|
|
|
return (sign * base_after_discount) / discount_factor
|
|
|
|
def _apply_manual_price_edit(self, field_name):
|
|
self.ensure_one()
|
|
if not self._is_vendor_bill_price_editable_line():
|
|
return
|
|
|
|
currency = self._get_price_edit_currency()
|
|
target_key = "total_excluded" if field_name == "price_subtotal" else "total_included"
|
|
target_value = self[field_name]
|
|
current_amount = self._compute_amount_from_current_price_unit(target_key)
|
|
if currency.compare_amounts(target_value, current_amount) == 0:
|
|
return
|
|
|
|
new_price_unit = self._compute_price_unit_from_target_amount(target_value, target_key)
|
|
if new_price_unit is False:
|
|
return
|
|
self.with_context(skip_vendor_bill_price_edit=True).price_unit = new_price_unit
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Onchanges
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.onchange("price_subtotal")
|
|
def _onchange_vendor_bill_price_subtotal(self):
|
|
for line in self:
|
|
line._apply_manual_price_edit("price_subtotal")
|
|
|
|
@api.onchange("price_total")
|
|
def _onchange_vendor_bill_price_total(self):
|
|
for line in self:
|
|
line._apply_manual_price_edit("price_total") |