first commit
This commit is contained in:
commit
f75fd84af0
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
16
__manifest__.py
Normal file
16
__manifest__.py
Normal file
@ -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
|
||||||
|
}
|
||||||
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
1
models/__init__.py
Normal file
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import account_move_line
|
||||||
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/account_move_line.cpython-310.pyc
Normal file
BIN
models/__pycache__/account_move_line.cpython-310.pyc
Normal file
Binary file not shown.
147
models/account_move_line.py
Normal file
147
models/account_move_line.py
Normal file
@ -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")
|
||||||
32
views/account_move_views.xml
Normal file
32
views/account_move_views.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="view_move_form_vendor_bill_price_edit" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.form.vendor.bill.price.edit</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_move_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@id='invoice_tab']//field[@name='invoice_line_ids']" position="attributes">
|
||||||
|
<attribute name="context">
|
||||||
|
{
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//page[@id='invoice_tab']//field[@name='invoice_line_ids']//tree//field[@name='price_subtotal']" position="attributes">
|
||||||
|
<attribute name="attrs">{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//page[@id='invoice_tab']//field[@name='invoice_line_ids']//tree//field[@name='price_total']" position="attributes">
|
||||||
|
<attribute name="column_invisible">False</attribute>
|
||||||
|
<attribute name="attrs">{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]}</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user