From 8c3360d0dac84707cc0c8a103945a08ca2b7de92 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Tue, 4 Aug 2026 18:24:40 +0200 Subject: [PATCH 1/2] [FIX] stock_account: product.value and stock.valuation.layer have different semantics --- .../stock_account/19.0.1.1/post-migration.py | 102 ++++++++++++++---- .../stock_account/19.0.1.1/pre-migration.py | 24 ----- .../19.0.1.1/upgrade_analysis_work.txt | 21 ++-- .../tests/data_stock_account_migration.py | 84 +++++++++++++++ .../tests/test_stock_account_migration.py | 81 ++++++++++++++ 5 files changed, 253 insertions(+), 59 deletions(-) create mode 100644 openupgrade_scripts/scripts/stock_account/tests/data_stock_account_migration.py create mode 100644 openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py diff --git a/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py b/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py index 8f30980f82ba..a53d81f49de0 100644 --- a/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py +++ b/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py @@ -4,21 +4,6 @@ from openupgradelib import openupgrade -def product_value_product_id(env): - """ - Fill product.value#product_id from move_id.product_id - """ - env.cr.execute( - """ - UPDATE product_value - SET product_id=stock_move.product_id - FROM stock_move - WHERE product_value.move_id=stock_move.id - AND product_value.product_id IS NULL - """ - ) - - def stock_move_account_move_id(env): """ Fill stock.move#account_move_id from account.move#stock_move_id @@ -84,20 +69,97 @@ def stock_move_value(env): SET value=aggregated_values.agg_value FROM ( SELECT - move_id, sum(value) AS agg_value + stock_move_id, sum(value) AS agg_value FROM - product_value - GROUP BY move_id + stock_valuation_layer + GROUP BY stock_move_id ) aggregated_values - WHERE aggregated_values.move_id=stock_move.id + WHERE aggregated_values.stock_move_id=stock_move.id + """ + ) + + +def product_value(env): + """ + Fill product.value with stock valuations not assigned to a move + (=manual valuations) + """ + openupgrade.logged_query( + env.cr, """ + ALTER TABLE product_value + ADD COLUMN IF NOT EXISTS stock_valuation_layer_id int + """, + ) + # simple case: the valuation layer has unit_cost set + openupgrade.logged_query( + env.cr, + """ + INSERT INTO product_value + ( + create_uid, create_date, write_uid, write_date, date, lot_id, + product_id, user_id, value, company_id, description, + stock_valuation_layer_id + ) + SELECT + create_uid, create_date, write_uid, write_date, create_date, lot_id, + product_id, create_uid, unit_cost, company_id, description, + id + FROM stock_valuation_layer + WHERE + stock_move_id IS NULL + AND + unit_cost IS NOT NULL + """, + ) + # otherwise: compute unit cost from sum of all previous values/sum of quantities + openupgrade.logged_query( + env.cr, + """ + INSERT INTO product_value + ( + create_uid, create_date, write_uid, write_date, + date, lot_id, product_id, user_id, + company_id, description, value, + stock_valuation_layer_id + ) + SELECT + svl1.create_uid, svl1.create_date, svl1.write_uid, svl1.write_date, + svl1.create_date, svl1.lot_id, svl1.product_id, svl1.create_uid, + svl1.company_id, svl1.description, SUM(svl2.value) / SUM(svl2.quantity), + svl1.id + FROM stock_valuation_layer svl1 + JOIN stock_valuation_layer svl2 + ON + svl1.product_id=svl2.product_id + AND + svl1.company_id=svl2.company_id + AND + ( + svl1.lot_id=svl2.lot_id + OR svl1.lot_id IS NULL AND svl2.lot_id IS NULL + ) + AND + svl1.create_date >= svl2.create_date + WHERE + svl1.stock_move_id IS NULL + AND + svl1.unit_cost IS NULL + AND + svl1.quantity = 0 + GROUP BY + svl1.id + HAVING + SUM(svl2.quantity) <> 0 + """, ) + openupgrade.lift_constraints(env.cr, "stock_valuation_layer", "id", cascade=True) @openupgrade.migrate() def migrate(env, version): - product_value_product_id(env) stock_move_account_move_id(env) product_category_property_valuation(env) stock_location_valuation_account_id(env) stock_move_value(env) + product_value(env) diff --git a/openupgrade_scripts/scripts/stock_account/19.0.1.1/pre-migration.py b/openupgrade_scripts/scripts/stock_account/19.0.1.1/pre-migration.py index 74b694fb7286..bfa89cebc09f 100644 --- a/openupgrade_scripts/scripts/stock_account/19.0.1.1/pre-migration.py +++ b/openupgrade_scripts/scripts/stock_account/19.0.1.1/pre-migration.py @@ -3,26 +3,6 @@ from openupgradelib import openupgrade -_renamed_models = [ - ("stock.valuation.layer", "product.value"), -] - -_renamed_tables = [ - ("stock_valuation_layer", "product_value"), -] - -_renamed_fields = [ - ("product.value", "product_value", "", ""), -] - -_copied_columns = { - "product_value": [ - ("create_date", "date", None), - ("create_uid", "user_id", None), - ("stock_move_id", "move_id", None), - ], -} - _deleted_xmlids = [ "stock_account.stock_valuation_layer_company_rule", "stock_account.group_stock_accounting_automatic", @@ -81,10 +61,6 @@ def stock_move_is_fields(env): @openupgrade.migrate() def migrate(env, version): - openupgrade.rename_models(env.cr, _renamed_models) - openupgrade.rename_tables(env.cr, _renamed_tables) - openupgrade.rename_fields(env, _renamed_fields) - openupgrade.copy_columns(env.cr, _copied_columns) openupgrade.delete_records_safely_by_xml_id(env, _deleted_xmlids) stock_lot_avg_cost(env) stock_move_is_fields(env) diff --git a/openupgrade_scripts/scripts/stock_account/19.0.1.1/upgrade_analysis_work.txt b/openupgrade_scripts/scripts/stock_account/19.0.1.1/upgrade_analysis_work.txt index 70b6412d3524..84e60e103585 100644 --- a/openupgrade_scripts/scripts/stock_account/19.0.1.1/upgrade_analysis_work.txt +++ b/openupgrade_scripts/scripts/stock_account/19.0.1.1/upgrade_analysis_work.txt @@ -1,7 +1,7 @@ ---Models in module 'stock_account'--- obsolete model stock.valuation.layer -# DONE: renamed to product.value +# DONE: used to compute stock.move#value obsolete model stock.valuation.layer.revaluation [transient] @@ -9,7 +9,7 @@ obsolete model stock.valuation.layer.revaluation [transient] new model product.value -# DONE: renamed from stock.valuation.layer +# DONE: filled from stock.valuation.layer where appropriate new model stock_account.stock.valuation.report [abstract] new model stock.avco.report [abstract] @@ -64,32 +64,23 @@ stock_account / product.template / valuation (selection) : selec stock_account / product.value / company_id (many2one) : NEW relation: res.company, required, hasdefault: compute -# NOTHING TO DO +# DONE: set from stock.valuation.layer stock_account / product.value / date (datetime) : NEW required, hasdefault: default -# DONE: set from create_date +# DONE: set from stock.valuation.layer#create_date stock_account / product.value / description (char) : NEW stock_account / product.value / lot_id (many2one) : NEW relation: stock.lot - -# NOTHING TO DO - stock_account / product.value / move_id (many2one) : NEW relation: stock.move - -# DONE: renamed from stock_move_id - stock_account / product.value / product_id (many2one) : NEW relation: product.product - -# DONE: set from move_id - stock_account / product.value / user_id (many2one) : NEW relation: res.users, required, hasdefault: default -# DONE: set from create_uid +# DONE: set from stock.valuation.layer stock_account / product.value / value (float) : NEW required -# NOTHING TO DO +# DONE: set from stock.valuation.layer#unit_cost stock_account / res.company / account_stock_journal_id (many2one): NEW relation: account.journal stock_account / res.company / account_stock_valuation_id (many2one): NEW relation: account.account diff --git a/openupgrade_scripts/scripts/stock_account/tests/data_stock_account_migration.py b/openupgrade_scripts/scripts/stock_account/tests/data_stock_account_migration.py new file mode 100644 index 000000000000..fc078b5eec2f --- /dev/null +++ b/openupgrade_scripts/scripts/stock_account/tests/data_stock_account_migration.py @@ -0,0 +1,84 @@ +env = locals().get("env") + +category_fifo = env["product.category"].create( + { + "name": "FIFO category", + "property_valuation": "real_time", + "property_cost_method": "fifo", + } +) +category_avg = env["product.category"].create( + { + "name": "AVG category", + "property_valuation": "real_time", + "property_cost_method": "average", + } +) + +product_fifo = env["product.product"].create( + { + "name": "FIFO product", + "categ_id": category_fifo.id, + "standard_price": 10, + "is_storable": True, + } +) + +product_avg = env["product.product"].create( + { + "name": "AVG product", + "categ_id": category_avg.id, + "standard_price": 10, + "is_storable": True, + } +) + +location_customer = env.ref("stock.stock_location_customers") +location_supplier = env.ref("stock.stock_location_suppliers") +location_stock = env.ref("stock.stock_location_stock") + + +def buy(product, **kwargs): + in_move = env["stock.move"].create( + dict( + { + "name": "/", + "product_id": product.id, + "location_id": location_supplier.id, + "location_dest_id": location_stock.id, + }, + **kwargs, + ) + ) + in_move._action_confirm() + in_move.picked = True + in_move._action_done() + return in_move + + +# fifo product +buy(product_fifo, price_unit=20, product_uom_qty=2) +buy(product_fifo, price_unit=22, product_uom_qty=2) +product_fifo_out_move = env["stock.move"].create( + { + "name": "OUT FIFO", + "product_id": product_fifo.id, + "location_id": location_stock.id, + "location_dest_id": location_customer.id, + "product_uom_qty": 2, + } +) +product_fifo_out_move._action_confirm() +product_fifo_out_move.picked = True +product_fifo_out_move._action_assign(force_qty=2) +product_fifo_out_move._action_done() +assert product_fifo.standard_price == 22 +# avg product +buy(product_avg, price_unit=20, product_uom_qty=2) +buy(product_avg, price_unit=22, product_uom_qty=2) +assert product_avg.standard_price == 21 +# setting the price must happen in its own transaction, otherwise the create date +# of the resulting stock valuation layer is equal to the previous or following ones +env.cr.commit() +product_avg.standard_price = 22 +env.cr.commit() diff --git a/openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py b/openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py new file mode 100644 index 000000000000..f77025394f51 --- /dev/null +++ b/openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py @@ -0,0 +1,81 @@ +from unittest import mock + +from odoo.tests import TransactionCase, tagged + +from odoo.addons.openupgrade_framework import openupgrade_test + + +@openupgrade_test +@tagged("-at_install", "post_install") +class TestStockAccountMigration(TransactionCase): + def setUp(self): + super().setUp() + self.product_avg = self.env["product.product"].search( + [("name", "=", "AVG product")] + ) + self.product_fifo = self.env["product.product"].search( + [("name", "=", "FIFO product")] + ) + self.location_customer = self.env.ref("stock.stock_location_customers") + self.location_supplier = self.env.ref("stock.stock_location_suppliers") + self.location_stock = self.env.ref("stock.stock_location_stock") + + def _buy(self, product, price_unit, quantity): + in_move = self.env["stock.move"].create( + { + "product_id": product.id, + "location_id": self.location_supplier.id, + "location_dest_id": self.location_stock.id, + "product_uom_qty": quantity, + } + ) + in_move._action_confirm() + in_move.picked = True + with mock.patch.object( + in_move.__class__, "_get_value_from_account_move" + ) as patched_get_value: + patched_get_value.return_value = { + "value": price_unit * quantity, + "quantity": quantity, + "description": "None", + } + in_move._action_done() + return in_move + + def _sell(self, product, quantity): + out_move = self.env["stock.move"].create( + { + "product_id": product.id, + "location_id": self.location_stock.id, + "location_dest_id": self.location_customer.id, + "product_uom_qty": quantity, + } + ) + out_move._action_confirm() + out_move.picked = True + out_move._action_assign(force_qty=quantity) + out_move._action_done() + return out_move + + def test_average_price(self): + self.assertEqual(self.product_avg.standard_price, 22) + + self._buy(self.product_avg, price_unit=24, quantity=4) + + # ( 4*22 (after v18 adjusted price) + 4*24 (v19) ) / 8 + self.assertEqual(self.product_avg.standard_price, 23) + + def test_fifo_price(self): + self.assertEqual(self.product_fifo.standard_price, 22) + + out_move = self._sell(self.product_fifo, 2) + # 2*22 (v18) + self.assertEqual(out_move.value, 44) + self.assertEqual(self.product_fifo.standard_price, 22) + + self._buy(self.product_fifo, price_unit=24, quantity=4) + + out_move = self._sell(self.product_fifo, 2) + # 2*24 (v19) + self.assertEqual(out_move.value, 48) + self.assertEqual(self.product_fifo.standard_price, 24) From 91b3931500602ac7dde6855c9dd23f89dcbbac8c Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Tue, 25 Aug 2026 15:52:26 +0200 Subject: [PATCH 2/2] [IMP] stock_account: use logged_query at some places, cr.execute on others --- .../stock_account/19.0.1.1/post-migration.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py b/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py index a53d81f49de0..5a3b815f2d38 100644 --- a/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py +++ b/openupgrade_scripts/scripts/stock_account/19.0.1.1/post-migration.py @@ -49,13 +49,14 @@ def stock_location_valuation_account_id(env): Set stock.location#valuation_account_id from valuation_in_account_id and valuation_out_account_id if they are the same """ - env.cr.execute( + openupgrade.logged_query( + env.cr, """ UPDATE stock_location SET valuation_account_id=valuation_in_account_id WHERE valuation_in_account_id=valuation_out_account_id - """ + """, ) @@ -63,7 +64,8 @@ def stock_move_value(env): """ Set stock.move#value to sum of product.value#value for this move """ - env.cr.execute( + openupgrade.logged_query( + env.cr, """ UPDATE stock_move SET value=aggregated_values.agg_value @@ -75,7 +77,7 @@ def stock_move_value(env): GROUP BY stock_move_id ) aggregated_values WHERE aggregated_values.stock_move_id=stock_move.id - """ + """, ) @@ -84,16 +86,14 @@ def product_value(env): Fill product.value with stock valuations not assigned to a move (=manual valuations) """ - openupgrade.logged_query( - env.cr, + env.cr.execute( """ ALTER TABLE product_value ADD COLUMN IF NOT EXISTS stock_valuation_layer_id int """, ) # simple case: the valuation layer has unit_cost set - openupgrade.logged_query( - env.cr, + env.cr.execute( """ INSERT INTO product_value ( @@ -113,8 +113,7 @@ def product_value(env): """, ) # otherwise: compute unit cost from sum of all previous values/sum of quantities - openupgrade.logged_query( - env.cr, + env.cr.execute( """ INSERT INTO product_value (