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