From b719403fc8c070112ed91a5b4a461a79df5e20f0 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Wed, 3 Jun 2026 11:44:59 -0400 Subject: [PATCH 1/5] [OU-FIX] stock_account: skip non-existent accounts in COA backfill update_from_coa_generic builds company_data from the chart-of-accounts template and feeds it to AccountChartTemplate._load_data. When a template references an account that no longer resolves in the migrated database the load crashes. Guard the comprehension with ref_or_id(record_id, model_name) so only records resolving to an existing account are loaded. --- .../scripts/stock_account/19.0.1.1/end-migration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openupgrade_scripts/scripts/stock_account/19.0.1.1/end-migration.py b/openupgrade_scripts/scripts/stock_account/19.0.1.1/end-migration.py index 4a5b37c7248c..481cf240281a 100644 --- a/openupgrade_scripts/scripts/stock_account/19.0.1.1/end-migration.py +++ b/openupgrade_scripts/scripts/stock_account/19.0.1.1/end-migration.py @@ -53,6 +53,7 @@ def ref_or_id(ref_or_id, model_name, AccountChartTemplate=AccountChartTemplate): } for record_id, record_data in template_data[model_name].items() if any(record_data.get(key) for key in field_names) + and ref_or_id(record_id, model_name) } AccountChartTemplate._load_data(company_data) From f05789d3dbf4799fcea7ea0ec95233cfb5d16af9 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 12 Jun 2026 15:32:17 -0400 Subject: [PATCH 2/5] [OU-FIX] stock_account: collapse migrated layers to one product.value per move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit product.value in 19.0 is the history of MANUAL value overrides: _get_manual_value() takes the single LATEST row per move as the move's entire value and suppresses the landed-cost extra. The 1:1 layer rename makes the newest layer (typically a landed-cost adjustment) silently replace the whole move value on the first ORM recompute — posting a vendor bill against a migrated landed-cost receipt collapsed a $1,050 move to $50 and cascaded a wrong standard_price. Reproduced and fix validated on a minimal two-PO + landed-cost fixture (receipt values survive bill post + payment at $1,050). --- .../stock_account/19.0.1.1/post-migration.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) 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 c90e05afd159..7c13f522c2a7 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 @@ -74,6 +74,48 @@ def stock_location_valuation_account_id(env): ) +def product_value_aggregate_move_rows(env): + """ + product.value is the history of MANUAL value overrides in 19.0: + _get_manual_value() takes the single LATEST row per move as the move's + entire value and suppresses the landed-cost extra. One row per former + valuation layer makes the newest layer (often a landed-cost adjustment) + silently replace the whole move value on the first ORM recompute (e.g. + posting a vendor bill against a migrated receipt). Collapse the layers + into one summed row per move. + """ + env.cr.execute( + """ + WITH agg AS ( + SELECT move_id, sum(value) AS value, max(date) AS date, + min(id) AS keep_id, + string_agg(description, ' + ' ORDER BY id) AS description + FROM product_value + WHERE move_id IS NOT NULL + GROUP BY move_id + HAVING count(*) > 1 + ) + UPDATE product_value pv + SET value = agg.value, date = agg.date, description = agg.description + FROM agg + WHERE pv.id = agg.keep_id + """ + ) + env.cr.execute( + """ + DELETE FROM product_value pv + USING ( + SELECT move_id, min(id) AS keep_id + FROM product_value + WHERE move_id IS NOT NULL + GROUP BY move_id + HAVING count(*) > 1 + ) agg + WHERE pv.move_id = agg.move_id AND pv.id != agg.keep_id + """ + ) + + def stock_move_value(env): """ Set stock.move#value to sum of product.value#value for this move @@ -100,4 +142,5 @@ def migrate(env, version): stock_move_account_move_id(env) product_category_property_valuation(env) stock_location_valuation_account_id(env) + product_value_aggregate_move_rows(env) stock_move_value(env) From 4f20e5330268dcea8a444f823a6719e1325571f1 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 12 Jun 2026 16:53:31 -0400 Subject: [PATCH 3/5] [OU-FIX] stock_account: outbound move values are magnitudes in 19.0 18.0 layers carry a direction sign (deliveries negative); 19.0 stores the cost magnitude. A negative migrated value flips the anglo-saxon COGS entry when the delivery is invoiced post-migration (observed +400/-400 reversal: Cr COGS / Dr valuation on a migrated 4-unit delivery). --- .../stock_account/19.0.1.1/post-migration.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 7c13f522c2a7..31797661010b 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 @@ -116,6 +116,31 @@ def product_value_aggregate_move_rows(env): ) +def product_value_outbound_sign(env): + """ + 18.0 valuation layers carry a direction sign (outbound = negative); + 19.0 stores the magnitude of the cost moved (a 19-created delivery or + production consumption has a positive value). A negative migrated value + flips the anglo-saxon COGS journal entry when the delivery is invoiced + after the migration. Normalize outbound moves to magnitudes. + """ + env.cr.execute( + """ + UPDATE product_value pv + SET value = -pv.value + FROM stock_move sm, + stock_location src, + stock_location dst + WHERE sm.id = pv.move_id + AND src.id = sm.location_id + AND dst.id = sm.location_dest_id + AND pv.value < 0 + AND src.usage IN ('internal', 'transit') + AND dst.usage NOT IN ('internal', 'transit') + """ + ) + + def stock_move_value(env): """ Set stock.move#value to sum of product.value#value for this move @@ -143,4 +168,5 @@ def migrate(env, version): product_category_property_valuation(env) stock_location_valuation_account_id(env) product_value_aggregate_move_rows(env) + product_value_outbound_sign(env) stock_move_value(env) From 63c2cec83feb3a9fa913a4f8cea89a74531c6606 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 12 Jun 2026 19:07:19 -0400 Subject: [PATCH 4/5] [OU-FIX] stock_account: migration tests for layer aggregation + outbound sign Pre-migration data builds a FIFO receipt with two layers (base + adjustment, the landed-cost shape) and a partial delivery; the post-migration tests assert one summed product.value row per move (the recompute path agrees at $1,050) and a positive outbound magnitude. --- .../tests/data_stock_account_migration.py | 78 +++++++++++++++++++ .../tests/test_stock_account_migration.py | 33 ++++++++ 2 files changed, 111 insertions(+) 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/tests/data_stock_account_migration.py b/openupgrade_scripts/scripts/stock_account/tests/data_stock_account_migration.py new file mode 100644 index 000000000000..a7d0990a9ca1 --- /dev/null +++ b/openupgrade_scripts/scripts/stock_account/tests/data_stock_account_migration.py @@ -0,0 +1,78 @@ +env = locals().get("env") + +# A FIFO receipt carrying TWO valuation layers (base + an adjustment, the +# landed-cost shape) and a partial FIFO delivery (negative layer). The +# post-migration test asserts the layers collapse to one product.value row +# per move and that the outbound value migrates as a magnitude. +categ = env["product.category"].create( + {"name": "OU SVL agg test", "property_cost_method": "fifo"} +) +product = env["product.product"].create( + { + "name": "OU SVL agg part", + "type": "consu", + "is_storable": True, + "categ_id": categ.id, + } +) +Move = env["stock.move"] +in_move = Move.create( + { + "name": "OU test receipt", + "product_id": product.id, + "product_uom_qty": 10, + "product_uom": product.uom_id.id, + "location_id": env.ref("stock.stock_location_suppliers").id, + "location_dest_id": env.ref("stock.stock_location_stock").id, + "price_unit": 100.0, + } +) +in_move._action_confirm() +in_move.quantity = 10 +in_move.picked = True +in_move._action_done() +# second layer on the same move: the landed-cost/adjustment shape +env["stock.valuation.layer"].create( + { + "product_id": product.id, + "quantity": 0, + "value": 50.0, + "stock_move_id": in_move.id, + "company_id": in_move.company_id.id, + "description": "OU test adjustment", + } +) +out_move = Move.create( + { + "name": "OU test delivery", + "product_id": product.id, + "product_uom_qty": 4, + "product_uom": product.uom_id.id, + "location_id": env.ref("stock.stock_location_stock").id, + "location_dest_id": env.ref("stock.stock_location_customers").id, + } +) +out_move._action_confirm() +out_move._action_assign() +out_move.quantity = 4 +out_move.picked = True +out_move._action_done() +env["ir.model.data"].create( + [ + { + "module": "openupgrade_test", + "name": "stock_account_in_move", + "model": "stock.move", + "res_id": in_move.id, + "noupdate": True, + }, + { + "module": "openupgrade_test", + "name": "stock_account_out_move", + "model": "stock.move", + "res_id": out_move.id, + "noupdate": True, + }, + ] +) +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..eb5f78eb19c3 --- /dev/null +++ b/openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py @@ -0,0 +1,33 @@ +from odoo.tests import TransactionCase + +from odoo.addons.openupgrade_framework import openupgrade_test + + +@openupgrade_test +class TestStockAccountMigration(TransactionCase): + def test_product_value_aggregated_per_move(self): + """ + product.value is the manual-override history: _get_manual_value() + takes the single latest row per move as the move's entire value, so + the former layers must collapse to one summed row — otherwise the + first recompute (e.g. posting a vendor bill) replaces a $1,050 + receipt with its $50 adjustment layer. + """ + in_move = self.env.ref("openupgrade_test.stock_account_in_move") + rows = self.env["product.value"].search([("move_id", "=", in_move.id)]) + self.assertEqual( + len(rows), 1, "former valuation layers must collapse to one row" + ) + self.assertAlmostEqual(rows.value, 1050.0) + self.assertAlmostEqual(in_move.value, 1050.0) + # the recompute path must agree with the migrated value + self.assertAlmostEqual(in_move._get_value(), 1050.0) + + def test_outbound_value_is_magnitude(self): + """ + 18.0 layers carry a direction sign (deliveries negative); 19.0 + stores cost magnitudes. A negative migrated value flips the + anglo-saxon COGS entry when the delivery is invoiced on 19.0. + """ + out_move = self.env.ref("openupgrade_test.stock_account_out_move") + self.assertAlmostEqual(out_move.value, 420.0) From 6ca817a9e73de964ce9b48804c25df02892e8448 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 12 Jun 2026 19:19:56 -0400 Subject: [PATCH 5/5] [OU-FIX] stock_account: test expects the 18-recorded FIFO cost (400) --- .../stock_account/tests/test_stock_account_migration.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index eb5f78eb19c3..543ecc56ad56 100644 --- a/openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py +++ b/openupgrade_scripts/scripts/stock_account/tests/test_stock_account_migration.py @@ -30,4 +30,7 @@ def test_outbound_value_is_magnitude(self): anglo-saxon COGS entry when the delivery is invoiced on 19.0. """ out_move = self.env.ref("openupgrade_test.stock_account_out_move") - self.assertAlmostEqual(out_move.value, 420.0) + # 4 units consumed at the receipt's $100 FIFO cost (the raw adjustment + # layer doesn't feed remaining_value the way a validated landed cost + # does); 18.0 recorded the layer as -400 — 19.0 must hold +400. + self.assertAlmostEqual(out_move.value, 400.0)