commit f75fd84af08b15d50ffde7d134e2b1630d448d48 Author: admin.suherdy Date: Thu Nov 20 20:49:42 2025 +0700 first commit diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..bfc01eb --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Vendor Bill Price Edit", + "version": "17.0.1.0.0", + "summary": "Allow editing vendor bill tax-exclusive and tax-inclusive amounts with automatic price unit recomputation.", + "license": "GPL-3", + "author": "Suherdy Yacob", + "category": "Accounting", + "depends": [ + "account" + ], + "data": [ + "views/account_move_views.xml" + ], + "application": False, + "installable": True +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..19e4a0f Binary files /dev/null and b/__pycache__/__init__.cpython-310.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..cb78dd3 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +from . import account_move_line \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-310.pyc b/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..ec5ef63 Binary files /dev/null and b/models/__pycache__/__init__.cpython-310.pyc differ diff --git a/models/__pycache__/account_move_line.cpython-310.pyc b/models/__pycache__/account_move_line.cpython-310.pyc new file mode 100644 index 0000000..c20f704 Binary files /dev/null and b/models/__pycache__/account_move_line.cpython-310.pyc differ diff --git a/models/account_move_line.py b/models/account_move_line.py new file mode 100644 index 0000000..9c69513 --- /dev/null +++ b/models/account_move_line.py @@ -0,0 +1,147 @@ +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") \ No newline at end of file diff --git a/views/account_move_views.xml b/views/account_move_views.xml new file mode 100644 index 0000000..d061355 --- /dev/null +++ b/views/account_move_views.xml @@ -0,0 +1,32 @@ + + + + + account.move.form.vendor.bill.price.edit + account.move + + + + + { + 'default_move_type': context.get('default_move_type'), + 'journal_id': journal_id, + 'default_partner_id': commercial_partner_id, + 'default_currency_id': currency_id or company_currency_id, + 'default_display_type': 'product', + 'quick_encoding_vals': quick_encoding_vals, + 'vendor_bill_price_edit': True + } + + + + {'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]} + + + False + {'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]} + + + + + \ No newline at end of file