vendor_bill_editable_totals/models/account_move_line.py

154 lines
6.7 KiB
Python
Executable File

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
# Override price_unit to use more decimal places
price_unit = fields.Float(
string='Unit Price',
digits=(16, 10), # Use 10 decimal places for maximum precision
)
@api.onchange('price_subtotal')
def _onchange_price_subtotal(self):
"""
Recalculate price_unit when price_subtotal is manually edited.
This method is triggered when a user modifies the price_subtotal field
on a vendor bill line. It automatically calculates the unit price to
maintain the exact entered subtotal amount.
Requirements: 1.2, 1.3, 1.4, 3.1, 3.5, 5.3
"""
for line in self:
# Skip if not a vendor bill line or if in a computed context
if line.move_id.move_type not in ('in_invoice', 'in_refund'):
continue
# Validate quantity is not zero
if line.quantity == 0:
raise UserError(_("Cannot calculate unit price: quantity must be greater than zero"))
# Calculate price_unit from price_subtotal
# Formula: price_unit = price_subtotal / quantity
new_price_unit = line.price_subtotal / line.quantity
# Set the price_unit - now with 10 decimal precision
line.price_unit = new_price_unit
@api.onchange('price_total')
def _onchange_price_total(self):
"""
Recalculate price_unit when price_total is manually edited.
This method is triggered when a user modifies the price_total field
on a vendor bill line. It automatically calculates the unit price by
first deriving the price_subtotal from the price_total (accounting for
taxes), then calculating the unit price.
Requirements: 2.2, 2.3, 2.5, 3.1, 3.3, 3.4, 3.5, 5.3
"""
for line in self:
# Skip if not a vendor bill line or if in a computed context
if line.move_id.move_type not in ('in_invoice', 'in_refund'):
continue
# Validate quantity is not zero
if line.quantity == 0:
raise UserError(_("Cannot calculate unit price: quantity must be greater than zero"))
# Handle case with no taxes: price_total equals price_subtotal
if not line.tax_ids:
new_price_unit = line.price_total / line.quantity
line.price_unit = new_price_unit
continue
# Check if any taxes are price-included
# For tax-included taxes, the price_unit already includes the tax
has_price_included_tax = any(tax.price_include for tax in line.tax_ids)
if has_price_included_tax:
# For tax-included taxes, price_unit = price_total / quantity
# because the tax is already included in the unit price
new_price_unit = line.price_total / line.quantity
line.price_unit = new_price_unit
else:
# For tax-excluded taxes, we need to calculate the tax factor
# Use a temporary price_unit of 1.0 to get the tax multiplier
tax_results = line.tax_ids.compute_all(
price_unit=1.0,
currency=line.currency_id,
quantity=1.0,
product=line.product_id,
partner=line.move_id.partner_id
)
# Calculate the tax factor (total_included / total_excluded)
# This tells us the multiplier from subtotal to total
if tax_results['total_excluded'] != 0:
tax_factor = tax_results['total_included'] / tax_results['total_excluded']
else:
tax_factor = 1.0
# Derive price_subtotal from price_total
# Formula: price_subtotal = price_total / tax_factor
derived_price_subtotal = line.price_total / tax_factor
# Calculate price_unit from derived price_subtotal
# Formula: price_unit = price_subtotal / quantity
new_price_unit = derived_price_subtotal / line.quantity
line.price_unit = new_price_unit
def write(self, vals):
"""
Override write to sync changes to linked Purchase Order Line.
"""
res = super(AccountMoveLine, self).write(vals)
# Check if we need to sync
if 'price_unit' in vals or 'quantity' in vals:
for line in self:
# Only for vendor bills and if linked to a PO line
if line.move_id.move_type in ('in_invoice', 'in_refund') and line.purchase_line_id:
po_line = line.purchase_line_id
# 1. Sync Price
# Convert bill price (in bill currency) to PO currency
bill_currency = line.currency_id or line.move_id.currency_id
po_currency = po_line.currency_id or po_line.order_id.currency_id
price_unit = line.price_unit
if bill_currency and po_currency and bill_currency != po_currency:
price_unit = bill_currency._convert(
price_unit,
po_currency,
line.company_id,
line.move_id.invoice_date or fields.Date.today()
)
# Convert Price to PO UoM if needed
if line.product_uom_id and po_line.product_uom and line.product_uom_id != po_line.product_uom:
price_unit = line.product_uom_id._compute_price(price_unit, po_line.product_uom)
# 2. Sync Quantity
# Convert bill qty (in bill UoM) to PO UoM
product_qty = line.quantity
if line.product_uom_id and po_line.product_uom and line.product_uom_id != po_line.product_uom:
product_qty = line.product_uom_id._compute_quantity(product_qty, po_line.product_uom)
# Update PO Line
# We update both to ensure consistency
po_line.write({
'price_unit': price_unit,
'product_qty': product_qty,
})
return res