From ad0e20a97374a8175ab1c41f7745a697563f25c3 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sat, 24 Jan 2026 18:24:32 +0700 Subject: [PATCH] fix: Prevent automatic inventory revaluation and stock valuation layer creation when editing vendor bill lines and add corresponding tests. --- models/account_move_line.py | 203 +++++++++++++++++----------------- tests/debug_button.py | 50 +++++++++ tests/test_revaluation_fix.py | 98 ++++++++++++++++ 3 files changed, 251 insertions(+), 100 deletions(-) create mode 100644 tests/debug_button.py create mode 100644 tests/test_revaluation_fix.py diff --git a/models/account_move_line.py b/models/account_move_line.py index 745a0e1..a0feb55 100755 --- a/models/account_move_line.py +++ b/models/account_move_line.py @@ -167,106 +167,109 @@ class AccountMoveLine(models.Model): # INVENTORY VALUATION UPDATE (AVCO/FIFO FIX) # --------------------------------------------------------- # If PO is received, updating price should update Stock Value - 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() + # 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 diff --git a/tests/debug_button.py b/tests/debug_button.py new file mode 100644 index 0000000..7206be1 --- /dev/null +++ b/tests/debug_button.py @@ -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") diff --git a/tests/test_revaluation_fix.py b/tests/test_revaluation_fix.py new file mode 100644 index 0000000..4c8d05e --- /dev/null +++ b/tests/test_revaluation_fix.py @@ -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") +