vendor_bill_price_edit/models/account_move_line.py
2025-11-20 20:49:42 +07:00

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")