diff --git a/docsource/modules180-190.rst b/docsource/modules180-190.rst index 0c5dcfe4e0f..95929a3c260 100644 --- a/docsource/modules180-190.rst +++ b/docsource/modules180-190.rst @@ -908,7 +908,7 @@ Module coverage 18.0 -> 19.0 +---------------------------------------------------+----------------------+-------------------------------------------------+ | privacy_lookup | |No DB layout changes. | +---------------------------------------------------+----------------------+-------------------------------------------------+ -| product | | | +| product |Done | | +---------------------------------------------------+----------------------+-------------------------------------------------+ | product_email_template | |No DB layout changes. | +---------------------------------------------------+----------------------+-------------------------------------------------+ diff --git a/openupgrade_scripts/scripts/product/19.0.1.2/post-migration.py b/openupgrade_scripts/scripts/product/19.0.1.2/post-migration.py new file mode 100644 index 00000000000..c6026b4ca68 --- /dev/null +++ b/openupgrade_scripts/scripts/product/19.0.1.2/post-migration.py @@ -0,0 +1,42 @@ +# Copyright 2025 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def _product_packaging(env): + """ + Create UOMs corresponding to the previous packaging's qty field, + reuse existing UOMs + """ + env.cr.execute("SELECT id, name, qty FROM product_uom") + for _id, name, qty in env.cr.fetchall(): + product_uom = env["product.uom"].browse(_id) + uom = product_uom.product_id.uom_id + + all_uoms = uom + while all_uoms.related_uom_ids - all_uoms: + all_uoms += all_uoms.related_uom_ids + while all_uoms.relative_uom_id - all_uoms: + all_uoms += all_uoms.relative_uom_id + existing_uom = env["uom.uom"] + for possible_uom in all_uoms: + if uom._compute_quantity(qty, possible_uom) == 1: + existing_uom = possible_uom + break + + if not existing_uom: + existing_uom = env["uom.uom"].create( + { + "name": name, + "relative_uom_id": uom.id, + "relative_factor": qty, + } + ) + + product_uom.uom_id = existing_uom + + +@openupgrade.migrate() +def migrate(env, version): + _product_packaging(env) diff --git a/openupgrade_scripts/scripts/product/19.0.1.2/pre-migration.py b/openupgrade_scripts/scripts/product/19.0.1.2/pre-migration.py new file mode 100644 index 00000000000..91fe80dec8c --- /dev/null +++ b/openupgrade_scripts/scripts/product/19.0.1.2/pre-migration.py @@ -0,0 +1,121 @@ +# Copyright 2025 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def _product_product_is_favorite(env): + """ + Precreate product.product#is_favorite and set with sql + """ + openupgrade.add_columns( + env, [("product.product", "is_favorite", "boolean", False, "product_product")] + ) + env.cr.execute( + """ + UPDATE product_product + SET is_favorite=product_template.is_favorite + FROM product_template + WHERE + product_product.product_tmpl_id=product_template.id + AND product_template.is_favorite + """ + ) + + +def _product_supplierinfo_product_tmpl_id(env): + """ + Precreate product.supplierinfo#{product_tmpl_id,product_uom_id} and set with sql + """ + openupgrade.add_columns( + env, + [ + ( + "product.supplierinfo", + "product_tmpl_id", + "many2one", + None, + "product_supplierinfo", + ), + ( + "product.supplierinfo", + "product_uom_id", + "many2one", + None, + "product_supplierinfo", + ), + ], + ) + env.cr.execute( + """ + UPDATE product_supplierinfo + SET product_tmpl_id=product_product.product_tmpl_id + FROM product_product + WHERE + product_supplierinfo.product_id=product_product.id + AND product_supplierinfo.product_tmpl_id IS NULL + """ + ) + env.cr.execute( + """ + UPDATE product_supplierinfo + SET product_uom_id=product_template.uom_id + FROM product_template + WHERE + product_supplierinfo.product_tmpl_id=product_template.id + AND product_supplierinfo.product_id IS NULL + """ + ) + env.cr.execute( + """ + UPDATE product_supplierinfo + SET product_uom_id=product_template.uom_id + FROM product_product, product_template + WHERE + product_supplierinfo.product_id=product_product.id + AND product_product.product_tmpl_id=product_template.id + AND product_supplierinfo.product_uom_id IS NULL + """ + ) + + +def _prepare_product_packaging_migration(env): + """ + product.packaging is replaced by UOMs being pointed at by records of product.uom + Rename product_packaging here to product_uom, and amend with data in post-migration + Set uom_id to dummy UOM to have the ORM set up the not null constraint correctly + """ + openupgrade.rename_tables(env.cr, [("product_packaging", "product_uom")]) + openupgrade.add_columns( + env, + [ + ( + "product.uom", + "uom_id", + "many2one", + env.ref("uom.product_uom_unit").id, + "product_uom", + ), + ], + ) + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.delete_records_safely_by_xml_id( + env, + [ + "product.constraint_product_packaging_positive_qty", + "product.product_packaging_comp_rule", + ], + ) + openupgrade.rename_xmlids( + env.cr, + [ + ("product.cat_expense", "product.product_category_expenses"), + ("product.product_category_all", "product.product_category_goods"), + ], + ) + _product_product_is_favorite(env) + _product_supplierinfo_product_tmpl_id(env) + _prepare_product_packaging_migration(env) diff --git a/openupgrade_scripts/scripts/product/19.0.1.2/upgrade_analysis_work.txt b/openupgrade_scripts/scripts/product/19.0.1.2/upgrade_analysis_work.txt new file mode 100644 index 00000000000..d65758c6587 --- /dev/null +++ b/openupgrade_scripts/scripts/product/19.0.1.2/upgrade_analysis_work.txt @@ -0,0 +1,127 @@ +---Models in module 'product'--- +obsolete model product.packaging (renamed to product.uom) +new model product.uom (renamed from product.packaging) + +# DONE: packagings are folded into UOMs, with product.uom linking products to the UOM that used to be a packaging + +---Fields in module 'product'--- +product / product.attribute / display_type (selection) : selection_keys added: [image] (most likely nothing to do) + +# NOTHING TO DO + +product / product.packaging / _order : _order is now 'id' ('product_id, sequence, id') +product / product.packaging / barcode (char) : now required +product / product.packaging / name (char) : DEL required +product / product.packaging / qty (float) : DEL +product / product.packaging / sequence (integer) : DEL + +# DONE: table is renamed to product_uom in pre-migration and matching UOMs created in post-migration + +product / product.pricelist.item / company_id (many2one) : not related anymore +product / product.pricelist.item / company_id (many2one) : now a function +product / product.pricelist.item / currency_id (many2one) : not related anymore +product / product.pricelist.item / currency_id (many2one) : now a function + +# NOTHING TO DO: related field was stored before + +product / product.product / _order : _order is now 'default_code, name, id' ('is_favorite desc, default_code, name, id') + +# NOTHING TO DO + +product / product.product / is_favorite (boolean) : is now stored + +# DONE: pre-created in pre-migration and filled from product_template + +product / product.product / is_in_selected_section_of_order (boolean): NEW + +# NOTHING TO DO: virtual field only used for searching + +product / product.product / packaging_ids (one2many) : DEL relation: product.packaging +product / product.product / product_uom_ids (one2many) : NEW relation: product.uom + +# DONE: see above at product.packaging + +product / product.supplierinfo / product_tmpl_id (many2one) : now required +product / product.supplierinfo / product_uom_id (many2one) : NEW relation: uom.uom, required, hasdefault: compute + +# DONE: filled from product_id + +product / product.tag / image (binary) : previously in module website_sale + +# NOTHING TO DO + +product / product.tag / visible_to_customers (boolean): NEW hasdefault: default + +# NOTHING TO DO + +product / product.template / pricelist_rule_ids (one2many) : NEW relation: product.pricelist.item + +# NOTHING TO DO + +product / product.template / uom_ids (many2many) : NEW relation: uom.uom +product / product.template / uom_po_id (many2one) : DEL relation: uom.uom, required + +# NOTHING TO DO + +product / product.uom / uom_id (many2one) : NEW relation: uom.uom, required +product / uom.uom / product_uom_ids (one2many) : NEW relation: product.uom + +# DONE: see above at product.packaging + +---XML records in module 'product'--- +DEL decimal.precision: product.decimal_product_uom [renamed to uom module] (noupdate) + +# DONE: in uom migration + +DEL ir.actions.act_window: product.action_packaging_view +NEW ir.actions.server: product.action_product_print_labels +NEW ir.actions.server: product.action_product_template_print_labels +NEW ir.model.access: product.access_product_document_manager +NEW ir.model.access: product.access_product_uom_manager +NEW ir.model.access: product.access_product_uom_user +NEW ir.model.access: product.access_uom_uom_product_manager +DEL ir.model.access: product.access_product_packaging_manager +DEL ir.model.access: product.access_product_packaging_user + +# NOTHING TO DO + +NEW ir.model.constraint: product.constraint_product_product_combination_unique +NEW ir.model.constraint: product.constraint_product_product_is_favorite_index +NEW ir.model.constraint: product.constraint_product_template_is_favorite_index +NEW ir.model.constraint: product.constraint_product_uom_barcode_uniq +DEL ir.model.constraint: product.constraint_product_packaging_barcode_uniq + +# NOTHING TO DO + +DEL ir.model.constraint: product.constraint_product_packaging_positive_qty +DEL ir.rule: product.product_packaging_comp_rule (noupdate) + +# DONE: deleted in pre-migration + +NEW ir.ui.view: product.product_pricelist_item_product_product_form_view +NEW ir.ui.view: product.product_pricelist_item_product_template_form_view +NEW ir.ui.view: product.product_template_list_view_purchasable +NEW ir.ui.view: product.product_template_list_view_sellable +NEW ir.ui.view: product.product_uom_list_view +NEW ir.ui.view: product.uom_uom_form_view_inherit +DEL ir.ui.view: product.product_packaging_form_view +DEL ir.ui.view: product.product_packaging_form_view2 +DEL ir.ui.view: product.product_packaging_search_view +DEL ir.ui.view: product.product_packaging_tree_view +DEL ir.ui.view: product.product_packaging_tree_view2 + +# NOTHING TO DO + +NEW product.category: product.product_category_expenses (noupdate) +NEW product.category: product.product_category_goods (noupdate) +NEW product.category: product.product_category_services (noupdate) +DEL product.category: product.cat_expense (noupdate) +DEL product.category: product.product_category_1 (noupdate) +DEL product.category: product.product_category_all (noupdate) + +# DONE: renamed in pre-migration + +DEL res.groups: product.group_stock_packaging +NEW res.groups.privilege: product.res_groups_privilege_product + +# NOTHING TO DO diff --git a/openupgrade_scripts/scripts/product/tests/data_product_migration.py b/openupgrade_scripts/scripts/product/tests/data_product_migration.py new file mode 100644 index 00000000000..bb9afc0e2a8 --- /dev/null +++ b/openupgrade_scripts/scripts/product/tests/data_product_migration.py @@ -0,0 +1,32 @@ +env = locals().get("env") +# packaging that should be assigned existing UOM +product = env.ref("product.product_delivery_01") +packaging = env["product.packaging"].create( + { + "name": "Sixpack", + "barcode": "42 42", + "product_id": product.id, + "qty": 6, + } +) +# packaging that should have a new UOM created +product = env.ref("product.product_delivery_01") +packaging = env["product.packaging"].create( + { + "name": "Thirteenpack", + "barcode": "42 43", + "product_id": product.id, + "qty": 13, + } +) +# packaging that should reuse UOM created for the one above +product = env.ref("product.product_delivery_02") +packaging = env["product.packaging"].create( + { + "name": "Thirteenpack", + "barcode": "42 44", + "product_id": product.id, + "qty": 13, + } +) +env.cr.commit() diff --git a/openupgrade_scripts/scripts/product/tests/test_product_migration.py b/openupgrade_scripts/scripts/product/tests/test_product_migration.py new file mode 100644 index 00000000000..a40753f3582 --- /dev/null +++ b/openupgrade_scripts/scripts/product/tests/test_product_migration.py @@ -0,0 +1,24 @@ +from odoo.tests import TransactionCase + +from odoo.addons.openupgrade_framework import openupgrade_test + + +@openupgrade_test +class TestProductMigration(TransactionCase): + def test_product_uom(self): + """ + Test that packages have been correctly migrated to uoms + """ + product = self.env.ref("product.product_delivery_01") + self.assertIn( + self.env.ref("uom.product_uom_pack_6"), product.product_uom_ids.uom_id + ) + new_uom = product.product_uom_ids.uom_id - self.env.ref( + "uom.product_uom_pack_6" + ) + self.assertEqual(new_uom.relative_uom_id, self.env.ref("uom.product_uom_unit")) + self.assertEqual(new_uom.relative_factor, 13) + self.assertEqual( + self.env.ref("product.product_delivery_02").product_uom_ids.uom_id, + new_uom, + )