# -*- 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 from odoo.tools import float_compare from datetime import timedelta 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 # Fix: Do not sync if triggered by Credit Note creation (Reversal Wizard) if line.move_id.move_type == 'in_invoice' and \ line.purchase_line_id and \ self.env.context.get('active_model') != 'account.move.reversal': 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 = po_line.order_id was_locked = po.state == 'done' if was_locked: po.button_unlock() po_line.write({ 'price_unit': price_unit, 'product_qty': product_qty, }) if was_locked: po.button_done() # --------------------------------------------------------- # INVENTORY VALUATION UPDATE (AVCO/FIFO FIX) # --------------------------------------------------------- # If PO is received, updating price should update Stock Value # COMMENTED OUT TO PREVENT INCORRECT "Revaluation" JOURNAL ENTRIES # User request: "do not create revaluation journal only sync the change to PO" # if po_line.state in ['purchase', 'done'] and po_line.product_id.type == 'product': # for stock_move in po_line.move_ids.filtered(lambda m: m.state == 'done'): # # Calculate Diff based on NEW Price (updated above) # new_val = price_unit * stock_move.quantity # # Current Value from SVLs # current_val = sum(stock_move.stock_valuation_layer_ids.mapped('value')) # diff = new_val - current_val # # currency = stock_move.company_id.currency_id # if not currency.is_zero(diff): # # 1. Create Correction SVL # svl_vals = { # 'company_id': stock_move.company_id.id, # 'product_id': stock_move.product_id.id, # 'description': _("Valuation correction from Vendor Bill %s") % line.move_id.name, # 'value': diff, # 'quantity': 0, # 'stock_move_id': stock_move.id, # } # svl = self.env['stock.valuation.layer'].create(svl_vals) # # # Backdate SVL # if stock_move.date: # new_date = stock_move.date + timedelta(seconds=1) # self.env.cr.execute("UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s", (new_date, svl.id)) # # # 2. AVCO/FIFO Logic: Update Standard Price and Distribute Value # product = stock_move.product_id # if product.categ_id.property_cost_method in ['average', 'fifo'] and product.quantity_svl > 0: # new_std_price = product.standard_price + (diff / product.quantity_svl) # product.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price}) # # remaining_svls = self.env['stock.valuation.layer'].search([ # ('product_id', '=', product.id), # ('remaining_qty', '>', 0), # ('company_id', '=', stock_move.company_id.id), # ]) # # if remaining_svls: # remaining_qty_total = sum(remaining_svls.mapped('remaining_qty')) # if remaining_qty_total > 0: # remaining_value_to_distribute = diff # remaining_value_unit_cost = remaining_value_to_distribute / remaining_qty_total # # for layer in remaining_svls: # if float_compare(layer.remaining_qty, remaining_qty_total, precision_rounding=product.uom_id.rounding) >= 0: # taken_remaining_value = remaining_value_to_distribute # else: # taken_remaining_value = remaining_value_unit_cost * layer.remaining_qty # # taken_remaining_value = stock_move.company_id.currency_id.round(taken_remaining_value) # layer.sudo().write({'remaining_value': layer.remaining_value + taken_remaining_value}) # # remaining_value_to_distribute -= taken_remaining_value # remaining_qty_total -= layer.remaining_qty # # # 3. Create Accounting Entry # if stock_move.product_id.categ_id.property_valuation == 'real_time': # accounts = stock_move.product_id.product_tmpl_id.get_product_accounts() # acc_expense = accounts.get('expense') # acc_valuation = accounts.get('stock_valuation') # # if acc_expense and acc_valuation: # if diff > 0: # debit_acc = acc_valuation.id # credit_acc = acc_expense.id # amount = diff # else: # debit_acc = acc_expense.id # credit_acc = acc_valuation.id # amount = abs(diff) # # acc_date = stock_move.date.date() if stock_move.date else fields.Date.today() # # move_vals = { # 'journal_id': accounts['stock_journal'].id, # 'company_id': stock_move.company_id.id, # 'ref': _("Revaluation for %s from Bill Edit") % stock_move.product_id.name, # 'date': acc_date, # 'move_type': 'entry', # 'stock_valuation_layer_ids': [(6, 0, [svl.id])], # 'line_ids': [ # (0, 0, { # 'name': _("Valuation Correction - %s") % stock_move.product_id.name, # 'account_id': debit_acc, # 'debit': amount, # 'credit': 0, # 'product_id': stock_move.product_id.id, # }), # (0, 0, { # 'name': _("Valuation Correction - %s") % stock_move.product_id.name, # 'account_id': credit_acc, # 'debit': 0, # 'credit': amount, # 'product_id': stock_move.product_id.id, # }) # ] # } # am = self.env['account.move'].create(move_vals) # am._post() return res