fix: Prevent automatic inventory revaluation and stock valuation layer creation when editing vendor bill lines and add corresponding tests.

This commit is contained in:
Suherdy Yacob 2026-01-24 18:24:32 +07:00
parent b09d75d7c5
commit ad0e20a973
3 changed files with 251 additions and 100 deletions

View File

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

50
tests/debug_button.py Normal file
View File

@ -0,0 +1,50 @@
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestButtonVisibility(TransactionCase):
def setUp(self):
super().setUp()
self.vendor = self.env['res.partner'].create({'name': 'Test Vendor'})
self.product = self.env['product.product'].create({'name': 'Test Product', 'standard_price': 100})
def test_reset_to_draft_visibility(self):
# Find existing posted bills for the vendor
bills = self.env['account.move'].search([
('move_type', '=', 'in_invoice'),
('partner_id', '=', self.vendor.id),
('state', '=', 'posted')
])
print(f"\nFOUND {len(bills)} POSTED BILLS FOR VENDOR {self.vendor.name}")
for bill in bills:
print(f"\nChecking Bill {bill.name} (ID: {bill.id}):")
print(f" state: {bill.state}")
print(f" restrict_mode_hash_table: {bill.restrict_mode_hash_table}")
print(f" need_cancel_request: {bill.need_cancel_request}")
print(f" show_reset_to_draft_button: {bill.show_reset_to_draft_button}")
print(f" journal_id.restrict_mode_hash_table: {bill.journal_id.restrict_mode_hash_table}")
# Additional check for localization overrides if any
# (Printed above via logic)
if not bills:
print("No posted bills found to check. Creating one...")
# Create Bill
bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.vendor.id,
'invoice_date': '2024-01-01',
'line_ids': [(0, 0, {
'product_id': self.product.id,
'quantity': 1,
'price_unit': 100,
})]
})
# Post Bill
bill.action_post()
print(f"\nCREATED FRESH BILL {bill.name}:")
print(f" state: {bill.state}")
print(f" show_reset_to_draft_button: {bill.show_reset_to_draft_button}")
self.assertTrue(bill.show_reset_to_draft_button, "Fresh bill should have button visible")

View File

@ -0,0 +1,98 @@
from odoo.tests import TransactionCase, tagged
from odoo.exceptions import UserError
from datetime import date, timedelta
@tagged('post_install', '-at_install')
class TestReproduction(TransactionCase):
def setUp(self):
super().setUp()
self.vendor = self.env['res.partner'].create({'name': 'Test Vendor'})
self.stock_journal = self.env['account.journal'].search([('type', '=', 'general')], limit=1)
# Create a product with AVCO/Automated valuation
self.product_category = self.env['product.category'].create({
'name': 'Test Category',
'property_cost_method': 'average',
'property_valuation': 'real_time',
})
self.product = self.env['product.product'].create({
'name': 'Test Product',
'type': 'product',
'categ_id': self.product_category.id,
'standard_price': 10.0,
})
# Ensure accounts are set on category (simplified for test)
account_type = self.env.ref('account.data_account_type_expenses')
self.account_expense = self.env['account.account'].create({
'name': 'Expense',
'code': 'EXP001',
'account_type': account_type.id,
'reconcile': True,
})
self.account_valuation = self.env['account.account'].create({
'name': 'Stock Valuation',
'code': 'STK001',
'account_type': account_type.id,
'reconcile': True,
})
self.product_category.property_stock_account_input_categ_id = self.account_expense
self.product_category.property_stock_account_output_categ_id = self.account_expense
self.product_category.property_stock_valuation_account_id = self.account_valuation
self.product_category.property_stock_journal = self.stock_journal
def test_revaluation_journal_creation(self):
# 1. Create PO
po = self.env['purchase.order'].create({
'partner_id': self.vendor.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
'product_qty': 10.0,
'price_unit': 10.0, # $100 total
})]
})
po.button_confirm()
# 2. Receive Goods
picking = po.picking_ids
picking.button_validate()
# 3. Create Bill
move_form = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.vendor.id,
'invoice_date': date.today(),
'purchase_vendor_bill_id': po.id,
'line_ids': [(0, 0, {
'product_id': self.product.id,
'quantity': 10.0,
'price_unit': 10.0,
'purchase_line_id': po.order_line.id
})]
})
# Simulate linking causing sync? Actually just creating the line linked to PO line should be enough
# But for test, we can just create the line manually linked to PO line as above
bill_line = move_form.line_ids.filtered(lambda l: l.product_id == self.product)
# 4. Modify Price (Trigger the issue)
# Edit price to 12.0
bill_line.with_context(active_model='account.move').write({'price_unit': 12.0})
# 5. Check for Revaluation Journal
# Search for account.move with 'Revaluation for' in ref
revaluation_moves = self.env['account.move'].search([
('ref', 'like', 'Revaluation for %s from Bill Edit' % self.product.name)
])
# Assert that it exists (Current bad behavior)
self.assertEqual(len(revaluation_moves), 0, "Revaluation journal should NOT exist after fix")
# 6. Check for Stock Valuation Layer
svls = self.env['stock.valuation.layer'].search([
('description', 'like', 'Valuation correction from Vendor Bill %')
])
self.assertEqual(len(svls), 0, "Stock Valuation Layer should NOT be created")