├── tests ├── __init__.py ├── data_template │ ├── module_130 │ │ ├── __init__.py │ │ ├── __manifest__.py │ │ └── views │ │ │ └── sale_order.xml │ ├── module_120 │ │ ├── __init__.py │ │ ├── __manifest__.py │ │ └── models │ │ │ └── product_multi_price.py │ ├── module_080 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── sale_order.py │ │ ├── migrations │ │ │ └── 8.0.3.0.0 │ │ │ │ └── pre-migration.py │ │ ├── demo │ │ │ ├── demo_ir_values.xml │ │ │ └── demo_product_category.xml │ │ ├── __openerp__.py │ │ └── views │ │ │ └── sale_order.xml │ ├── module_150 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── account_account_type.py │ │ │ ├── stock_production_lot.py │ │ │ └── res_partner.py │ │ ├── security │ │ │ └── ir.model.access.csv │ │ ├── __manifest__.py │ │ └── views │ │ │ └── stock_report_views.xml │ ├── module_160 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── chart_template.py │ │ │ └── res_partner.py │ │ ├── views │ │ │ ├── template.xml │ │ │ └── res_partner.xml │ │ └── __manifest__.py │ ├── module_170 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── fetchmail.py │ │ │ └── res_partner.py │ │ ├── __manifest__.py │ │ └── views │ │ │ └── res_partner.xml │ └── module_180 │ │ ├── __init__.py │ │ ├── models │ │ ├── __init__.py │ │ ├── sale_order.py │ │ └── res_partner.py │ │ ├── __manifest__.py │ │ └── views │ │ └── res_partner.xml ├── data_result │ ├── module_130_140 │ │ ├── __init__.py │ │ ├── __manifest__.py │ │ └── views │ │ │ └── sale_order.xml │ ├── module_120_130 │ │ ├── __init__ .py │ │ ├── __manifest__.py │ │ └── models │ │ │ └── product_multi_price.py │ ├── module_080_130 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── sale_order.py │ │ ├── demo │ │ │ ├── demo_ir_values.xml │ │ │ └── demo_product_category.xml │ │ ├── __manifest__.py │ │ └── views │ │ │ └── sale_order.xml │ ├── module_150_160 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── account_account_type.py │ │ │ ├── stock_production_lot.py │ │ │ └── res_partner.py │ │ ├── security │ │ │ └── ir.model.access.csv │ │ ├── __manifest__.py │ │ └── views │ │ │ └── stock_report_views.xml │ ├── module_160_170 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── chart_template.py │ │ │ └── res_partner.py │ │ ├── views │ │ │ ├── template.xml │ │ │ └── res_partner.xml │ │ └── __manifest__.py │ ├── module_170_180 │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── fetchmail.py │ │ │ └── res_partner.py │ │ ├── __manifest__.py │ │ └── views │ │ │ └── res_partner.xml │ └── module_180_190 │ │ ├── __init__.py │ │ ├── models │ │ ├── __init__.py │ │ ├── sale_order.py │ │ └── res_partner.py │ │ ├── __manifest__.py │ │ └── views │ │ └── res_partner.xml └── test_migration.py ├── odoo_module_migrate ├── migration_scripts │ ├── __init__.py │ ├── text_replaces │ │ ├── migrate_120_130 │ │ │ ├── decorators.yaml │ │ │ └── digits_precision.yaml │ │ ├── migrate_180_190 │ │ │ ├── controllers.yaml │ │ │ ├── field_params.yaml │ │ │ ├── deprecated_properties.yaml │ │ │ └── rename_groups_id.yaml │ │ ├── migrate_allways │ │ │ └── string_attribute_in_tree_view.yaml │ │ └── migrate_150_160 │ │ │ └── orm_methods.yaml │ ├── deprecated_modules │ │ └── migrate_080_090 │ │ │ └── account_modules.yaml │ ├── renamed_models │ │ ├── migrate_150_160 │ │ │ └── renamed_models.yaml │ │ └── migrate_140_150 │ │ │ └── commission.yaml │ ├── removed_fields │ │ ├── migrate_130_140 │ │ │ └── mail.yaml │ │ ├── migrate_150_160 │ │ │ ├── product.yaml │ │ │ └── account.yaml │ │ └── migrate_160_170 │ │ │ └── hr.yaml │ ├── removed_models │ │ ├── migrate_150_160 │ │ │ └── removed_models.yaml │ │ └── migrate_160_170 │ │ │ └── removed_models.yaml │ ├── text_errors │ │ ├── migrate_100_110 │ │ │ └── unexisted_models.yaml │ │ ├── migrate_170_180 │ │ │ ├── name_search.yaml │ │ │ ├── ir_cron.yaml │ │ │ └── registry.yaml │ │ ├── migrate_160_170 │ │ │ └── orm_methods.yaml │ │ └── migrate_150_160 │ │ │ └── orm_methods.yaml │ ├── renamed_fields │ │ ├── migrate_150_160 │ │ │ ├── stock.yaml │ │ │ ├── hr.yaml │ │ │ └── account.yaml │ │ ├── migrate_160_170 │ │ │ ├── l10n_mx.yaml │ │ │ └── hr.yaml │ │ ├── migrate_170_180 │ │ │ └── account.yaml │ │ └── migrate_130_140 │ │ │ └── account.yaml │ ├── text_warnings │ │ ├── migrate_120_130 │ │ │ └── company.yaml │ │ ├── migrate_160_170 │ │ │ └── web_assets_common.yaml │ │ ├── migrate_170_180 │ │ │ └── related_fields.yaml │ │ ├── migrate_180_190 │ │ │ ├── decorators.yaml │ │ │ └── timezone.yaml │ │ └── migrate_150_160 │ │ │ └── orm_methods.yaml │ ├── migrate_140_150.py │ ├── migrate_150_160.py │ ├── migrate_100_allways.py │ ├── python_scripts │ │ └── migrate_allways │ │ │ └── bump_version.py │ ├── migrate_remove_migration_folder.py │ ├── migrate_allways.py │ ├── migrate_080_allways.py │ ├── migrate_150_allways.py │ ├── migrate_090_100.py │ ├── migrate_110_120.py │ ├── migrate_120_130.py │ ├── migrate_080_090.py │ ├── migrate_100_110.py │ ├── migrate_130_140.py │ ├── migrate_180_190.py │ ├── migrate_170_180.py │ └── migrate_160_170.py ├── __init__.py ├── exception.py ├── log.py ├── tools.py ├── config.py ├── module_migration.py ├── __main__.py ├── migration.py └── base_migration_script.py ├── test_requirements.txt ├── requirements.txt ├── MANIFEST.in ├── .pre-commit-config.yaml ├── .travis.yml ├── .github └── workflows │ ├── test.yml │ ├── release.yml │ └── pre-commit.yml ├── setup.py ├── .gitignore ├── DEVELOP.rst └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data_template/module_130/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data_result/module_130_140/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | coveralls 2 | flake8 3 | pytest 4 | -------------------------------------------------------------------------------- /tests/data_template/module_120/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models -------------------------------------------------------------------------------- /odoo_module_migrate/__init__.py: -------------------------------------------------------------------------------- 1 | from . import migration_scripts 2 | -------------------------------------------------------------------------------- /tests/data_result/module_120_130/__init__ .py: -------------------------------------------------------------------------------- 1 | from . import models -------------------------------------------------------------------------------- /tests/data_template/module_080/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_template/module_150/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_template/module_160/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_template/module_170/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_template/module_180/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_result/module_080_130/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_result/module_150_160/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_result/module_160_170/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_result/module_170_180/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /tests/data_result/module_180_190/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argcomplete 2 | colorama 3 | lxml 4 | pyyaml 5 | requests 6 | -------------------------------------------------------------------------------- /tests/data_template/module_080/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import sale_order 2 | -------------------------------------------------------------------------------- /tests/data_result/module_080_130/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import sale_order 2 | -------------------------------------------------------------------------------- /tests/data_result/module_150_160/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | -------------------------------------------------------------------------------- /tests/data_template/module_150/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | -------------------------------------------------------------------------------- /tests/data_template/module_080/migrations/8.0.3.0.0/pre-migration.py: -------------------------------------------------------------------------------- 1 | # Some script 2 | -------------------------------------------------------------------------------- /tests/data_template/module_170/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | from . import fetchmail 3 | -------------------------------------------------------------------------------- /tests/data_template/module_180/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | from . import sale_order 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include odoo_module_migrate/migration_scripts/ *.py *.yaml 3 | -------------------------------------------------------------------------------- /tests/data_result/module_170_180/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | from . import fetchmail 3 | -------------------------------------------------------------------------------- /tests/data_result/module_180_190/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | from . import sale_order 3 | -------------------------------------------------------------------------------- /tests/data_template/module_160/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | from . import chart_template 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_120_130/decorators.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | .*@api.multi.*\n: 3 | -------------------------------------------------------------------------------- /tests/data_result/module_160_170/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import res_partner 2 | from . import chart_template 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/deprecated_modules/migrate_080_090/account_modules.yaml: -------------------------------------------------------------------------------- 1 | - ["account_anglo_saxon", "removed"] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_models/migrate_150_160/renamed_models.yaml: -------------------------------------------------------------------------------- 1 | - ["stock.production.lot", "stock.lot", None] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_180_190/controllers.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | type=(['\"])json\1: type=\1jsonrpc\1 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_180_190/field_params.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | auto_join=: bypass_search_access= 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_180_190/deprecated_properties.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | \._(cr|uid|context)\b: .env.\1 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_allways/string_attribute_in_tree_view.yaml: -------------------------------------------------------------------------------- 1 | .xml: 2 | (]*?)\s+string="[^"]*": \1 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/removed_fields/migrate_130_140/mail.yaml: -------------------------------------------------------------------------------- 1 | - ['mail.template', 'user_signature', 'Commit https://github.com/odoo/odoo/commit/de1743ab'] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_150_160/orm_methods.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | \.get_xml_id\(: ".get_external_id(" 3 | \.fields_get_keys\(\): "._fields" 4 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/removed_fields/migrate_150_160/product.yaml: -------------------------------------------------------------------------------- 1 | - ['product.product', 'price', 'Commit https://github.com/odoo/odoo/commit/9e99a9df464d97a74ca320d'] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/removed_models/migrate_150_160/removed_models.yaml: -------------------------------------------------------------------------------- 1 | - ["account.account.type", "Commit https://github.com/odoo/odoo/commit/26b2472f4977ccedbb0b5ed5f"] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_errors/migrate_100_110/unexisted_models.yaml: -------------------------------------------------------------------------------- 1 | "*": 2 | "('|\")ir.values('|\")": "[V11] Reference to 'ir.values'. This model has been removed." 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/removed_fields/migrate_160_170/hr.yaml: -------------------------------------------------------------------------------- 1 | - ['hr.expense', 'reference', 'Commit https://github.com/odoo/odoo/commit/68fbdc964038ef6a1cf0d3df773db101ca81794a'] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_180_190/rename_groups_id.yaml: -------------------------------------------------------------------------------- 1 | .xml: 2 | (?<=name=[\"'])groups_id(?=[\"']): group_ids 3 | .py: 4 | (?<=[\.\"'])groups_id(?=[\.\s,:)\]'\"]): group_ids 5 | -------------------------------------------------------------------------------- /tests/data_result/module_080_130/demo/demo_ir_values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | My Demo Values 4 | 5 | 6 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_fields/migrate_150_160/stock.yaml: -------------------------------------------------------------------------------- 1 | - ['stock.move.line', 'product_qty', 'reserved_qty', 'Commit https://github.com/odoo/odoo/commit/56e7bcf0cb88acf3d60420569ab3eea9bdb19bdb'] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/removed_fields/migrate_150_160/account.yaml: -------------------------------------------------------------------------------- 1 | - ['account.move.line', 'exclude_from_invoice_tab', 'Commit https://github.com/odoo/odoo/commit/d8d47f9ff8554f4b39487fd2f13c153c7d6f958d'] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_fields/migrate_150_160/hr.yaml: -------------------------------------------------------------------------------- 1 | - ['hr.expense', 'analytic_account_id', 'analytic_distribution', 'Commit https://github.com/odoo/odoo/commit/7e3403068fc3fbc40182b3cfeb80e97a9300e8ff'] 2 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_fields/migrate_160_170/l10n_mx.yaml: -------------------------------------------------------------------------------- 1 | - ['account.tax', 'l10n_mx_tax_type', 'l10n_mx_factor_type', 'Commit https://github.com/odoo/odoo/commit/cdd29dfe98003270124110e6bbf13b64618e1f1f'] 2 | -------------------------------------------------------------------------------- /tests/data_result/module_160_170/views/template.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/data_template/module_160/views/template.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/data_result/module_080_130/demo/demo_product_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | My Demo Category 4 | 5 | 6 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_warnings/migrate_120_130/company.yaml: -------------------------------------------------------------------------------- 1 | 2 | .py: 3 | env.user.company_id: "[13] If the intended use is to get the current active company, you need to change '.env.user.company_id' to '.env.company'" 4 | -------------------------------------------------------------------------------- /tests/data_result/module_160_170/models/chart_template.py: -------------------------------------------------------------------------------- 1 | from odoo import models, fields 2 | 3 | 4 | class AccountAccountTemplate(models.Model): 5 | _inherit = "account.account.template" 6 | 7 | test_1 = fields.Char(required=True) 8 | -------------------------------------------------------------------------------- /tests/data_template/module_160/models/chart_template.py: -------------------------------------------------------------------------------- 1 | from odoo import models, fields 2 | 3 | 4 | class AccountAccountTemplate(models.Model): 5 | _inherit = "account.account.template" 6 | 7 | test_1 = fields.Char(required=True) 8 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_warnings/migrate_160_170/web_assets_common.yaml: -------------------------------------------------------------------------------- 1 | "*": 2 | web.assets_common: "[17] Reference to 'web.assets_common'. This bundle has been removed from odoo, see https://github.com/odoo/odoo/pull/132190" 3 | -------------------------------------------------------------------------------- /tests/data_result/module_150_160/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | access_stock_production_user,stock.production.lot,stock.model_stock_lot,quality.group_quality_user,1,0,0,0 3 | -------------------------------------------------------------------------------- /tests/data_template/module_150/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | access_stock_production_user,stock.production.lot,stock.model_stock_production_lot,quality.group_quality_user,1,0,0,0 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_140_150.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 4 | 5 | 6 | class MigrationScript(BaseMigrationScript): 7 | pass 8 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_150_160.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 4 | 5 | 6 | class MigrationScript(BaseMigrationScript): 7 | pass 8 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_errors/migrate_170_180/name_search.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | def\s+_name_search\(([^)]*)\): 3 | "[Error] The method '_name_search' is deprecated and replaced by '_search_display_name'. More details: https://github.com/odoo/odoo/pull/174967" 4 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_replaces/migrate_120_130/digits_precision.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | import\sodoo\.addons\.decimal_precision\b.*\n: 3 | from\sodoo\.addons\simport\sdecimal_precision\b.*\n: 4 | digits\=dp\.get_precision\((?P[^/)]+?)\): digits=\g 5 | -------------------------------------------------------------------------------- /tests/data_template/module_080/demo/demo_ir_values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Demo Values 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/data_template/module_120/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 3 | { 4 | "name": "Module name", 5 | "version": "12.0.1.0.0", 6 | "installable": False, 7 | "license": "AGPL-3", 8 | } 9 | -------------------------------------------------------------------------------- /tests/data_result/module_120_130/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 3 | { 4 | "name": "Module name", 5 | "version": "13.0.1.0.0", 6 | "installable": True, 7 | "license": "AGPL-3", 8 | } 9 | -------------------------------------------------------------------------------- /tests/data_template/module_080/demo/demo_product_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Demo Category 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_errors/migrate_160_170/orm_methods.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | \.message_post_with_view\(: "The method message_post_with_view has been replaced by message_post_with_source.\nMore details at https://github.com/odoo/odoo/commit/4775bd93a2bf9947236f4f7f06d6f8cb7734f7ef." 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/related_fields.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | related=.*store=True: "[18] It is no longer necessary to set store=True on related fields to group, aggregate, or sort them. Remove store=True unless it is strictly required. More details: https://github.com/odoo/odoo/pull/127353" 3 | -------------------------------------------------------------------------------- /tests/data_result/module_130_140/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 3 | { 4 | "name": "Module name", 5 | "version": "14.0.1.0.0", 6 | "installable": True, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'sale', 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /odoo_module_migrate/exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | 6 | class OdooMigrateException(Exception): 7 | pass 8 | 9 | 10 | class ConfigException(OdooMigrateException): 11 | pass 12 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_errors/migrate_170_180/ir_cron.yaml: -------------------------------------------------------------------------------- 1 | .xml: 2 | numbercall: "The 'numbercall' field has been removed. More details: https://github.com/odoo/odoo/commit/2700dd3fd4c45b5282d3803182dfffdfc4418ad8" 3 | doall: "The 'doall' field has been removed. More details: https://github.com/odoo/odoo/commit/2700dd3fd4c45b5282d3803182dfffdfc4418ad8" 4 | -------------------------------------------------------------------------------- /tests/data_template/module_130/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 3 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 4 | { 5 | "name": "Module name", 6 | "version": "8.0.3.0.0", 7 | "installable": False, 8 | 'license': 'AGPL-3', 9 | 'depends': [ 10 | 'sale', 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_warnings/migrate_180_190/decorators.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | ormcache_context\(: 3 | "[19] 'ormcache_context' is deprecated. Use 'ormcache' directly, context values are available as 'self.env.context.get'. More information: https://github.com/odoo/odoo/commit/755e9c3f2bad#diff-3d5ccf8623cd4456c775fef15f0cc973b6f88f05c0ce3b04ab113324e589b851" 4 | -------------------------------------------------------------------------------- /tests/data_template/module_170/__manifest__.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | { 4 | "name": "Module name", 5 | "version": "17.0.1.0.0", 6 | "installable": False, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'base', 10 | ], 11 | 'data': [ 12 | 'views/res_partner.xml', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /tests/data_template/module_180/__manifest__.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | { 4 | "name": "Module name", 5 | "version": "18.0.1.0.0", 6 | "installable": False, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'base', 10 | ], 11 | 'data': [ 12 | 'views/res_partner.xml', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /tests/data_result/module_170_180/__manifest__.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | { 4 | "name": "Module name", 5 | "version": "18.0.1.0.0", 6 | "installable": True, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'base', 10 | ], 11 | 'data': [ 12 | 'views/res_partner.xml', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /tests/data_result/module_180_190/__manifest__.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | { 4 | "name": "Module name", 5 | "version": "19.0.1.0.0", 6 | "installable": True, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'base', 10 | ], 11 | 'data': [ 12 | 'views/res_partner.xml', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_fields/migrate_170_180/account.yaml: -------------------------------------------------------------------------------- 1 | - ['res.company', 'period_lock_date', 'sale_lock_date and purchase_lock_date', 'Commit https://github.com/odoo/odoo/commit/c73d430080da602aa1528842b591040d0717987f'] 2 | - ['account.move', 'reversal_move_id', 'reversal_move_ids', 'Commit https://github.com/odoo/odoo/commit/26d57934f7a656fe5267d7931e0601c43a18d270'] 3 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_warnings/migrate_150_160/orm_methods.yaml: -------------------------------------------------------------------------------- 1 | 2 | .py: 3 | def _name_search: "[16] You are overriding name_search method in your module, you may make use now of new _rec_names_search class variable to expose the fields to search for without requiring the method override. More details at https://github.com/odoo/odoo/commit/3155c3e425581b71491844e7f9a3dd76a9f245a4." 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: | 2 | (?x) 3 | tests/data_template/| 4 | tests/data_result/ 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v2.3.0 9 | hooks: 10 | - id: check-yaml 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | - repo: https://github.com/psf/black 14 | rev: 22.10.0 15 | hooks: 16 | - id: black 17 | -------------------------------------------------------------------------------- /tests/data_template/module_130/views/sale_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/removed_models/migrate_160_170/removed_models.yaml: -------------------------------------------------------------------------------- 1 | - ["account.account.template", "Commit https://github.com/odoo/odoo/commit/512574861691f425ec6a17f20fe4b586bb88a299"] 2 | - ["account.tax.template", "Commit https://github.com/odoo/odoo/commit/512574861691f425ec6a17f20fe4b586bb88a299"] 3 | - ["account.group.template", "Commit https://github.com/odoo/odoo/commit/512574861691f425ec6a17f20fe4b586bb88a299"] 4 | -------------------------------------------------------------------------------- /tests/data_result/module_150_160/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Camptocamp SA (https://www.camptocamp.com). 2 | # @author Iván Todorovich 3 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 4 | 5 | { 6 | "name": "Module name", 7 | "version": "16.0.1.0.0", 8 | "installable": True, 9 | 'license': 'AGPL-3', 10 | 'depends': [ 11 | 'base', 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /tests/data_template/module_150/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Camptocamp SA (https://www.camptocamp.com). 2 | # @author Iván Todorovich 3 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 4 | 5 | { 6 | "name": "Module name", 7 | "version": "15.0.1.0.0", 8 | "installable": False, 9 | 'license': 'AGPL-3', 10 | 'depends': [ 11 | 'base', 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /tests/data_result/module_160_170/__manifest__.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | { 4 | "name": "Module name", 5 | "version": "17.0.1.0.0", 6 | "installable": True, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'base', 10 | ], 11 | 'data': [ 12 | 'views/res_partner.xml', 13 | ], 14 | "assets": { 15 | "web.assets_common", 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/data_template/module_160/__manifest__.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | { 4 | "name": "Module name", 5 | "version": "16.0.1.0.0", 6 | "installable": False, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'base', 10 | ], 11 | 'data': [ 12 | 'views/res_partner.xml', 13 | ], 14 | "assets": { 15 | "web.assets_common", 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/data_result/module_180_190/models/sale_order.py: -------------------------------------------------------------------------------- 1 | from odoo import models 2 | from odoo.fields import Domain 3 | 4 | 5 | class SaleOrder(models.Model): 6 | _inherit = "sale.order" 7 | 8 | def simple_search(self, base_domain): 9 | extra_domain = [('state', '=', 'sale')] 10 | result1 = Domain.AND([base_domain, extra_domain]) 11 | result2 = Domain.OR([result1, [('state', '=', 'draft')]]) 12 | return result2 13 | -------------------------------------------------------------------------------- /tests/data_template/module_180/models/sale_order.py: -------------------------------------------------------------------------------- 1 | from odoo import models 2 | from odoo.osv import expression 3 | 4 | 5 | class SaleOrder(models.Model): 6 | _inherit = "sale.order" 7 | 8 | def simple_search(self, base_domain): 9 | extra_domain = [('state', '=', 'sale')] 10 | result1 = expression.AND([base_domain, extra_domain]) 11 | result2 = expression.OR([result1, [('state', '=', 'draft')]]) 12 | return result2 13 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_fields/migrate_130_140/account.yaml: -------------------------------------------------------------------------------- 1 | - ['account.move', 'type', 'move_type', 'Commit https://github.com/odoo/odoo/commit/d675dbaa4c7174591e0e7c1a3caf3e76877312ce'] 2 | - ['account.move', 'invoice_payment_state', 'payment_state', 'Commit https://github.com/odoo/odoo/commit/8e4158af810bcf475214946fa64d3b0ce4d3b26d'] 3 | - ['account.move', 'invoice_sent', 'is_move_sent', 'Commit https://github.com/odoo/odoo/commit/caeb782841fc5a7ad71a196e2c9ee67644ef9074'] 4 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_fields/migrate_150_160/account.yaml: -------------------------------------------------------------------------------- 1 | - ['account.account', 'user_type_id', 'account_type', 'Commit https://github.com/odoo/odoo/commit/26b2472f4977ccedbb0b5ed5f'] 2 | - ['account.account', 'internal_type', 'account_type', 'Commit https://github.com/odoo/odoo/commit/26b2472f4977ccedbb0b5ed5f08be2c04313fd21'] 3 | - ['account.move.line', 'analytic_account_id', 'analytic_distribution', 'Commit https://github.com/odoo/odoo/commit/7064c95aa04e5138bb12ae97acfee04ebb67cc0e'] 4 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_100_allways.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 6 | 7 | _TEXT_REPLACES = {".py": {r"# (-\*- )?coding: utf-8( -\*-)?\n": ""}} 8 | 9 | 10 | class MigrationScript(BaseMigrationScript): 11 | 12 | _TEXT_REPLACES = _TEXT_REPLACES 13 | -------------------------------------------------------------------------------- /tests/data_result/module_080_130/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 3 | { 4 | "name": "Module name", 5 | "version": "13.0.1.0.0", 6 | "installable": True, 7 | 'license': 'AGPL-3', 8 | 'depends': [ 9 | 'sale', 10 | 'account_anglo_saxon', 11 | 'contract', 12 | 'account', 13 | 'account_check_printing', 14 | 'web_kanban_sparkline', 15 | 'web', 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/renamed_models/migrate_140_150/commission.yaml: -------------------------------------------------------------------------------- 1 | - ["sale.commission.settlement", "commission.settlement", "Commit https://github.com/OCA/commission/commit/49b0ff3daca47d133ec3bd9317cb95db81456bdd"] 2 | - ["sale.commission.analysis.report", "invoice.commission.analysis.report", " Commit https://github.com/OCA/commission/commit/05ec496a4e461a6e9e35a43ce4b9118eb1d1c1f4"] 3 | - ["sale.commision.make.settle", "commission.make.settle", "Commit https://github.com/OCA/commission/commit/05ec496a4e461a6e9e35a43ce4b9118eb1d1c1f4"] 4 | -------------------------------------------------------------------------------- /tests/data_template/module_080/__openerp__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 3 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 4 | { 5 | "name": "Module name", 6 | "version": "8.0.3.0.0", 7 | "installable": False, 8 | 'license': 'AGPL-3', 9 | 'depends': [ 10 | 'sale', 11 | 'account_anglo_saxon', 12 | 'account_analytic_analysis', 13 | 'account_chart', 14 | 'account_check_writing', 15 | 'web_kanban_sparkline', 16 | 'web', 17 | "base_suspend_security", 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | cache: pip 3 | 4 | language: python 5 | 6 | python: 7 | - "3.10" 8 | 9 | dist: "focal" 10 | 11 | before_install: 12 | # For tests running git command 13 | - git config --global user.name "test" 14 | - git config --global user.email "test.test@test.com" 15 | 16 | # command to install dependencies 17 | install: 18 | - pip install -r requirements.txt 19 | - pip install -r test_requirements.txt 20 | 21 | # command to run tests 22 | script: 23 | - git --version 24 | - coverage run --source odoo_module_migrate setup.py test 25 | 26 | after_success: 27 | - coveralls 28 | -------------------------------------------------------------------------------- /tests/data_result/module_130_140/views/sale_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | account.move.line 6 | 7 | tree 8 | 9 | 10 | Name 11 | String 12 | 13 | 14 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/python_scripts/migrate_allways/bump_version.py: -------------------------------------------------------------------------------- 1 | def bump_revision(**kwargs): 2 | tools = kwargs["tools"] 3 | manifest_path = kwargs["manifest_path"] 4 | migration_steps = kwargs["migration_steps"] 5 | target_version_name = migration_steps[-1]["target_version_name"] 6 | 7 | new_version = "%s.1.0.0" % target_version_name 8 | 9 | old_term = r"('|\")version('|\").*('|\").*('|\")" 10 | new_term = r'\1version\2: "{0}"'.format(new_version) 11 | tools._replace_in_file( 12 | manifest_path, {old_term: new_term}, "Bump version to %s" % new_version 13 | ) 14 | -------------------------------------------------------------------------------- /tests/data_template/module_150/models/account_account_type.py: -------------------------------------------------------------------------------- 1 | from odoo import fields, models 2 | 3 | 4 | class AccountAccountType(models.Model): 5 | _inherit = "account.account.type" 6 | 7 | analytic_account_required = fields.Boolean( 8 | string='Analytic Account Required?', 9 | help="If True, then an analytic account will be required when posting " 10 | "journal entries with this type of account.", 11 | ) 12 | analytic_tag_required = fields.Boolean( 13 | string='Analytic Tag Required?', 14 | help="If True, then analytic tags will be required when posting " 15 | "journal entries with this type of account.", 16 | ) 17 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_errors/migrate_150_160/orm_methods.yaml: -------------------------------------------------------------------------------- 1 | 2 | .py: 3 | (flush\(.*\)|recompute\(.*\)): "[16] The methods flush() and recompute() are deprecated. Use flush_model(), flush_recordset() or env.flush_all() instead depending on the needed granularity. More details at https://github.com/odoo/odoo/commit/32bc28aa665c479e31a40afffca2f03f6f2cc805." 4 | (refresh\(.*\)|invalidate_cache\(.*\)): "[16] The methods refresh() and invalidate_cache() are deprecated. Use invalidate_model(), invalidate_recordset() or env.invalidate_all() instead depending on the needed granularity. More details at https://github.com/odoo/odoo/commit/32bc28aa665c479e31a40afffca2f03f6f2cc805." 5 | -------------------------------------------------------------------------------- /tests/data_result/module_150_160/models/account_account_type.py: -------------------------------------------------------------------------------- 1 | from odoo import fields, models 2 | 3 | 4 | class AccountAccountType(models.Model): 5 | _inherit = "account.account.type" 6 | 7 | analytic_account_required = fields.Boolean( 8 | string='Analytic Account Required?', 9 | help="If True, then an analytic account will be required when posting " 10 | "journal entries with this type of account.", 11 | ) 12 | analytic_tag_required = fields.Boolean( 13 | string='Analytic Tag Required?', 14 | help="If True, then analytic tags will be required when posting " 15 | "journal entries with this type of account.", 16 | ) 17 | -------------------------------------------------------------------------------- /tests/data_result/module_120_130/models/product_multi_price.py: -------------------------------------------------------------------------------- 1 | from odoo import fields, models, api 2 | 3 | 4 | class ProductMultiPrice(models.Model): 5 | _name = 'product.multi.price' 6 | _description = "Product Multiple Prices" 7 | 8 | name = fields.Many2one( 9 | comodel_name='product.multi.price.name', 10 | required=True, 11 | translate=True, 12 | ) 13 | product_id = fields.Many2one( 14 | comodel_name='product.product', 15 | required=True, 16 | ondelete='cascade', 17 | ) 18 | price = fields.Float( 19 | digits='Product Price', 20 | ) 21 | 22 | def _some_method(self): 23 | return self.env.user.company_id 24 | -------------------------------------------------------------------------------- /tests/data_result/module_170_180/models/fetchmail.py: -------------------------------------------------------------------------------- 1 | from odoo import models, tools, _ 2 | from odoo.tools import misc 3 | from odoo.tools.misc import find_in_path 4 | from odoo.tools import config, consteq, file_path 5 | from odoo.tools.misc import lazy 6 | from odoo.tools import config, consteq, file_path 7 | from ssl import SSLError 8 | from odoo.exceptions import UserError 9 | 10 | 11 | class FetchMailServer(models.Model): 12 | _inherit = "fetchmail.server" 13 | 14 | def example_method_use_ustr(self): 15 | try: 16 | server_name = self.name 17 | description = self._description 18 | connection = self.connect(allow_archived=True) 19 | except SSLError as e: 20 | raise UserError(_(e)) 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "*" 7 | push: 8 | branches: 9 | - "master" 10 | - "master-ocabot-*" 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.13" 20 | - name: Install 21 | run: | 22 | pip install pre-commit 23 | pip install -r test_requirements.txt 24 | pip install . 25 | - name: Run tests 26 | run: coverage run --source odoo_module_migrate -m pytest -v tests 27 | - uses: codecov/codecov-action@v4 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_errors/migrate_170_180/registry.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | from\s+odoo\s+import\s+([^,\n]+,)*\s*registry\s*(,[^,\n]+)*: 3 | "The import of 'registry' should now be done as 'from odoo.modules.registry import Registry'. More details: https://github.com/odoo/odoo/pull/178784" 4 | 5 | \bodoo\.registry\(([^)]+)\): 6 | "'odoo.registry' should now be imported as 'from odoo.modules.registry import Registry' and replaced with 'Registry'. More details: https://github.com/odoo/odoo/pull/178784" 7 | 8 | \bregistry\(([^)]+)\): 9 | "The usage of 'registry' should now be replaced with 'Registry'. Make sure 'from odoo.modules.registry import Registry' is included at the beginning of your file. More details: https://github.com/odoo/odoo/pull/178784" 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - published 5 | 6 | name: release 7 | 8 | jobs: 9 | pypi: 10 | name: upload release to PyPI 11 | runs-on: ubuntu-latest 12 | environment: release 13 | 14 | permissions: 15 | # Used to authenticate to PyPI via OIDC. 16 | # Used to sign the release's artifacts with sigstore-python. 17 | id-token: write 18 | 19 | # Used to attach signing artifacts to the published release. 20 | contents: write 21 | 22 | steps: 23 | - uses: actions/checkout@v5 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.13" 28 | 29 | - name: build 30 | run: pipx run build 31 | 32 | - name: publish 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | -------------------------------------------------------------------------------- /tests/data_template/module_120/models/product_multi_price.py: -------------------------------------------------------------------------------- 1 | from odoo import fields, models, api 2 | from odoo.addons import decimal_precision as dp 3 | import odoo.addons.decimal_precision 4 | 5 | 6 | class ProductMultiPrice(models.Model): 7 | _name = 'product.multi.price' 8 | _description = "Product Multiple Prices" 9 | 10 | name = fields.Many2one( 11 | comodel_name='product.multi.price.name', 12 | required=True, 13 | translate=True, 14 | ) 15 | product_id = fields.Many2one( 16 | comodel_name='product.product', 17 | required=True, 18 | ondelete='cascade', 19 | ) 20 | price = fields.Float( 21 | digits=dp.get_precision('Product Price'), 22 | ) 23 | 24 | @api.one 25 | @api.multi 26 | def _some_method(self): 27 | return self.env.user.company_id 28 | -------------------------------------------------------------------------------- /tests/data_template/module_170/models/fetchmail.py: -------------------------------------------------------------------------------- 1 | from odoo import models, tools, _ 2 | from odoo.tools import misc 3 | from odoo.tools import ustr 4 | from odoo.tools.misc import ustr 5 | from odoo.tools.misc import ustr, find_in_path 6 | from odoo.tools import ustr, config, consteq, file_path 7 | from odoo.tools.misc import lazy, ustr 8 | from odoo.tools import config, consteq, ustr, file_path 9 | from ssl import SSLError 10 | from odoo.exceptions import UserError 11 | 12 | 13 | class FetchMailServer(models.Model): 14 | _inherit = "fetchmail.server" 15 | 16 | def example_method_use_ustr(self): 17 | try: 18 | server_name = ustr(self.name) 19 | description = misc.ustr(self._description) 20 | connection = self.connect(allow_archived=True) 21 | except SSLError as e: 22 | raise UserError(_(tools.ustr(e))) 23 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_remove_migration_folder.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import os 6 | import subprocess 7 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 8 | 9 | 10 | def remove_migration_folder(**kwargs): 11 | logger = kwargs["logger"] 12 | module_path = kwargs["module_path"] 13 | migration_path_folder = os.path.join(module_path, "migrations") 14 | if os.path.exists(migration_path_folder): 15 | logger.info("Removing 'migrations' folder") 16 | subprocess.check_output("rm -r %s" % migration_path_folder, shell=True) 17 | 18 | 19 | class MigrationScript(BaseMigrationScript): 20 | _GLOBAL_FUNCTIONS = [ 21 | remove_migration_folder, 22 | ] 23 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/text_warnings/migrate_180_190/timezone.yaml: -------------------------------------------------------------------------------- 1 | .py: 2 | pytz\.timezone\(\s*self\.env\.(context\.get\(\s*['"']tz['"']|user\.tz): 3 | "[19] Replace manual timezone handling with 'self.env.tz'. This new property provides automatic timezone resolution with proper fallbacks (context -> user -> UTC). More info: https://github.com/odoo/odoo/pull/221541" 4 | timezone\(\s*self\.env\.(context\.get\(\s*['"']tz['"']|user\.tz): 5 | "[19] Replace manual timezone handling with 'self.env.tz'. This new property provides automatic timezone resolution with proper fallbacks (context -> user -> UTC). More info: https://github.com/odoo/odoo/pull/221541" 6 | (\w+)\.env\.context\.get\(\s*['"']tz['"']: 7 | "[19] Replace manual timezone context access with 'env.tz'. This new property provides automatic timezone resolution with proper fallbacks (context -> user -> UTC). More info: https://github.com/odoo/odoo/pull/221541" 8 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_allways.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 6 | 7 | _TEXT_REPLACES = { 8 | ".xml": { 9 | r"": "", 10 | } 11 | } 12 | 13 | 14 | def set_module_installable(**kwargs): 15 | tools = kwargs["tools"] 16 | manifest_path = kwargs["manifest_path"] 17 | old_term = r"('|\")installable('|\").*(False)" 18 | new_term = r"\1installable\2: True" 19 | tools._replace_in_file( 20 | manifest_path, {old_term: new_term}, "Set module installable" 21 | ) 22 | 23 | 24 | class MigrationScript(BaseMigrationScript): 25 | _TEXT_REPLACES = _TEXT_REPLACES 26 | _GLOBAL_FUNCTIONS = [ 27 | set_module_installable, 28 | ] 29 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_080_allways.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 6 | 7 | _FILE_RENAMES = {"__openerp__.py": "__manifest__.py"} 8 | 9 | _TEXT_ERRORS = { 10 | ".xml": { 11 | r' 2 | 3 | 4 | res.partner.simplified.form 5 | res.partner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 60 | """, 61 | re.VERBOSE, 62 | ): """ 63 | 64 | 70 | """, 71 | }, 72 | } 73 | 74 | 75 | class MigrationScript(BaseMigrationScript): 76 | _TEXT_ERRORS = _TEXT_ERRORS 77 | _TEXT_REPLACES = _TEXT_REPLACES 78 | -------------------------------------------------------------------------------- /tests/data_result/module_080_130/views/sale_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Custom field A 5 | sale.report 6 | tree,form 7 | [('custom_field','=','a')] 8 | 9 | 10 | 11 | Custom field B 12 | sale.report 13 | tree,form 14 | [('custom_field','=','b')] 15 | 16 | 17 | 18 | Custom field C 19 | sale.report 20 | tree,form 21 | [('custom_field','=','c')] 22 | 23 | 24 | 25 | Custom field D 26 | sale.report 27 | tree,form 28 | [('custom_field','=','d')] 29 | 30 | 31 | 37 | 38 | 39 | Test Form 40 | account.move 41 | 42 |
43 | 44 |
45 | 46 | 47 | 53 | 54 |
55 |
56 |
61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_080_090.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 6 | 7 | _TEXT_REPLACES = { 8 | ".py": {"select=True": "index=True"}, 9 | } 10 | 11 | _DEPRECATED_MODULES = [ 12 | ("account_analytic_analysis", "oca_moved", "contract", "Moved to OCA/contract"), 13 | ( 14 | "account_analytic_plans", 15 | "oca_moved", 16 | "account_analytic_distribution", 17 | "Moved to OCA/account_analytic", 18 | ), 19 | ("account_bank_statement_extensions", "removed"), 20 | ("account_chart", "merged", "account"), 21 | ("account_check_writing", "renamed", "account_check_printing"), 22 | ("account_followup", "removed"), 23 | ( 24 | "account_payment", 25 | "oca_moved", 26 | "account_payment_order", 27 | "Moved to OCA/bank-payment", 28 | ), 29 | ("account_sequence", "removed"), 30 | ("analytic_contract_hr_expense", "removed"), 31 | ("analytic_user_function", "removed"), 32 | ("anglo_saxon_dropshipping", "removed"), 33 | ("auth_openid", "removed"), 34 | ("base_report_designer", "removed"), 35 | ("contacts", "merged", "mail"), 36 | ("crm_helpdesk", "removed"), 37 | ("crm_mass_mailing", "removed"), 38 | ("crm_profiling", "removed"), 39 | ("edi", "removed"), 40 | ("email_template", "merged", "mail_template"), 41 | ("hr_applicant_document", "removed"), 42 | ("hr_timesheet_invoice", "removed"), 43 | ("im_chat", "merged", "mail"), 44 | ("knowledge", "oca_moved", "knowledge", "Moved to OCA/knowledge"), 45 | ("l10n_be_coda", "removed"), 46 | ("l10n_fr_rib", "removed"), 47 | ("marketing_crm", "merged", "crm"), 48 | ("multi_company", "removed"), 49 | ("portal_claim", "renamed", "website_crm_claim"), 50 | ("portal_project", "merged", "project"), 51 | ("portal_project_issue", "merged", "project_issue"), 52 | ("procurement_jit_stock", "merged", "procurement_jit"), 53 | ( 54 | "purchase_analytic_plans", 55 | "oca_moved", 56 | "purchase_analytic_distribution", 57 | "Moved to OCA/account-analytic", 58 | ), 59 | ("purchase_double_validation", "removed"), 60 | ( 61 | "sale_analytic_plans", 62 | "oca_moved", 63 | "sale_analytic_distribution", 64 | "Moved to OCA/account-analytic", 65 | ), 66 | ("sale_journal", "removed"), 67 | ("share", "removed"), 68 | ("stock_invoice_directly", "removed"), 69 | ("web_api", "removed"), 70 | ("web_gantt", "merged", "web"), 71 | ("web_graph", "merged", "web"), 72 | ("web_kanban_sparkline", "merged", "web"), 73 | ("web_tests", "merged", "web"), 74 | ("web_tests_demo", "removed"), 75 | ("website_certification", "removed"), 76 | ("website_instantclick", "removed"), 77 | ("website_mail_group", "renamed", "website_mail_channel"), 78 | ("website_report", "merged", "report"), 79 | ] 80 | 81 | 82 | class MigrationScript(BaseMigrationScript): 83 | 84 | _TEXT_REPLACES = _TEXT_REPLACES 85 | _DEPRECATED_MODULES = _DEPRECATED_MODULES 86 | -------------------------------------------------------------------------------- /tests/data_template/module_080/views/sale_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Custom field A 6 | sale.report 7 | form 8 | tree,form 9 | [('custom_field','=','a')] 10 | 11 | 12 | 13 | Custom field B 14 | sale.report 15 | tree 16 | tree,form 17 | [('custom_field','=','b')] 18 | 19 | 20 | 21 | Custom field C 22 | sale.report 23 | form 24 | tree,form 25 | [('custom_field','=','c')] 26 | 27 | 28 | 29 | Custom field D 30 | sale.report 31 | tree 32 | tree,form 33 | [('custom_field','=','d')] 34 | 35 | 36 | 42 | 43 | 44 | Test Form 45 | account.move 46 | 47 |
48 | 49 |
50 | 62 |
63 |
64 |
69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |
77 |
78 |
79 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_100_110.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 6 | 7 | # TODO: Call 2to3 8 | 9 | _TEXT_REPLACES = { 10 | "*": { 11 | r"ir.actions.report.xml": "ir.actions.report", 12 | r"report.external_layout": "web.external_layout", 13 | r"report.html_container": "web.html_container", 14 | r"report.layout": "web.report_layout", 15 | r"report.minimal_layout": "web.minimal_layout", 16 | }, 17 | ".xml": { 18 | r"kanban_state_selection": "state_selection", 19 | }, 20 | } 21 | 22 | _TEXT_ERRORS = { 23 | "*": { 24 | "('|\")workflow('|\")": "[V11] Reference to 'workflow'." 25 | " This model has been removed.", 26 | "('|\")workflow.activity('|\")": "[V11] Reference to 'workflow.activity'." 27 | " This model has been removed.", 28 | "('|\")workflow.instance('|\")": "[V11] Reference to'workflow.instance'." 29 | " This model has been removed.", 30 | "('|\")workflow.transition('|\")": "[V11] Reference to 'workflow.transition'." 31 | " This model has been removed.", 32 | "('|\")workflow.triggers('|\")": "[V11] Reference to 'workflow.triggers'." 33 | " This model has been removed.", 34 | "('|\")workflow.workitem('|\")": "[V11] Reference to 'workflow.workitem'." 35 | " This model has been removed.", 36 | "report.external_layout_header": "report.external_layout_header is obsolete.", 37 | "report.external_layout_footer": "report.external_layout_footer is obsolete.", 38 | }, 39 | ".xml": { 40 | r"]*colors=": "colors attribute is deprecated in tree view." 41 | " Use decoration- instead.", 42 | r"]*fonts=": "fonts attribute is deprecated in tree view." 43 | " Use decoration- instead.", 44 | }, 45 | } 46 | 47 | _DEPRECATED_MODULES = [ 48 | ("account_accountant", "removed"), 49 | ("account_tax_cash_basis", "removed"), 50 | ("base_action_rule", "renamed", "base_automation"), 51 | ("crm_project_issue", "renamed", "crm_project_issue"), 52 | ( 53 | "hr_timesheet_sheet", 54 | "oca_moved", 55 | "hr_timesheet_sheet", 56 | "Moved to OCA/hr-timesheet", 57 | ), 58 | ("marketing_campaign", "removed"), 59 | ("marketing_campaign_crm_demo", "removed"), 60 | ("portal_gamification", "merged", "gamification"), 61 | ("portal_sale", "merged", "sale"), 62 | ("portal_stock", "merged", "portal"), 63 | ("procurement", "merged", "stock"), 64 | ("project_issue", "merged", "project"), 65 | ("project_issue_sheet", "merged", "hr_timesheet"), 66 | ("rating_project_issue", "removed"), 67 | ("report", "merged", "base"), 68 | ("stock_calendar", "removed"), 69 | ("stock_picking_wave", "renamed", "stock_picking_batch"), 70 | ("subscription", "removed"), 71 | ("web_calendar", "merged", "web"), 72 | ("web_kanban", "merged", "web"), 73 | ("website_issue", "renamed", "website_form_project"), 74 | ("website_portal", "merged", "website"), 75 | ("website_project", "merged", "project"), 76 | ("website_project_issue", "merged", "project"), 77 | ("website_project_timesheet", "merged", "hr_timesheet"), 78 | ("website_rating_project_issue", "renamed", "website_rating_project"), 79 | ] 80 | 81 | 82 | class MigrationScript(BaseMigrationScript): 83 | 84 | _TEXT_REPLACES = _TEXT_REPLACES 85 | _TEXT_ERRORS = _TEXT_ERRORS 86 | _DEPRECATED_MODULES = _DEPRECATED_MODULES 87 | -------------------------------------------------------------------------------- /odoo_module_migrate/module_migration.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import os 6 | 7 | from .log import logger 8 | 9 | from .config import _MANIFEST_NAMES 10 | from .tools import _execute_shell 11 | 12 | 13 | class ModuleMigration: 14 | 15 | _migration = False 16 | _module_name = False 17 | _module_path = False 18 | 19 | def __init__(self, migration, module_name): 20 | self._migration = migration 21 | self._module_name = module_name 22 | self._module_path = self._migration._directory_path / module_name 23 | 24 | def run(self): 25 | logger.info( 26 | "[%s] Running migration from %s to %s" 27 | % ( 28 | self._module_name, 29 | self._migration._migration_steps[0]["init_version_name"], 30 | self._migration._migration_steps[-1]["target_version_name"], 31 | ) 32 | ) 33 | 34 | # Apply migration script 35 | for migration_script in self._migration._migration_scripts: 36 | migration_script.run( 37 | self._module_path, 38 | self._get_manifest_path(), 39 | self._module_name, 40 | self._migration._migration_steps, 41 | self._migration._directory_path, 42 | self._migration._commit_enabled, 43 | ) 44 | 45 | # Run pre-commit before final commit to format any changes made during migration scripts execution 46 | if os.path.exists(".pre-commit-config.yaml") and self._migration._pre_commit: 47 | _execute_shell( 48 | "pre-commit run -a", 49 | path=self._migration._directory_path, 50 | raise_error=False, 51 | ) 52 | 53 | self._commit_changes( 54 | "[MIG] %s: Migration to %s" 55 | % ( 56 | self._module_name, 57 | self._migration._migration_steps[-1]["target_version_name"], 58 | ) 59 | ) 60 | 61 | def _get_manifest_path(self): 62 | for manifest_name in _MANIFEST_NAMES: 63 | manifest_path = self._module_path / manifest_name 64 | if manifest_path.exists(): 65 | return manifest_path 66 | 67 | def _rename_file(self, module_path, old_file_path, new_file_path): 68 | """ 69 | Rename a file. try to execute 'git mv', to avoid huge diff. 70 | 71 | if 'git mv' fails, make a classical rename 72 | """ 73 | logger.info( 74 | "Renaming file: '%s' by '%s' " 75 | % ( 76 | old_file_path.replace(str(module_path.resolve()), ""), 77 | new_file_path.replace(str(module_path.resolve()), ""), 78 | ) 79 | ) 80 | if self._migration._commit_enabled: 81 | _execute_shell( 82 | "git mv %s %s" % (old_file_path, new_file_path), path=module_path 83 | ) 84 | else: 85 | _execute_shell( 86 | "mv %s %s" % (old_file_path, new_file_path), path=module_path 87 | ) 88 | 89 | def _commit_changes(self, commit_name): 90 | if not self._migration._commit_enabled: 91 | return 92 | 93 | if _execute_shell("git diff", path=self._migration._directory_path): 94 | logger.info( 95 | "Commit changes for %s. commit name '%s'" 96 | % (self._module_name, commit_name) 97 | ) 98 | 99 | _execute_shell( 100 | " git add . --all && git commit --no-verify -m '%s'" % (commit_name), 101 | path=self._migration._directory_path, 102 | ) 103 | -------------------------------------------------------------------------------- /tests/data_result/module_170_180/views/res_partner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | res.partner.simplified.form 5 | res.partner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | res.partner.form 17 | res.partner 18 | 19 | list,form 20 | 21 | 22 | 23 | 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | res.partner.complex.view 33 | res.partner 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | boolean_toggle 42 | 43 | 44 | boolean_toggle 45 | 46 | 47 | boolean_toggle 48 | 49 | 50 | 51 | 52 | 53 | list_view_ref 54 | 55 | 56 | 57 | res.partner.chatter.form 58 | res.partner 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 |
68 |
69 | 70 | 71 | 72 | 73 |
74 |
75 | 76 | 77 | res.partner.view.kanban 78 | res.partner 79 | 80 | 81 | 82 | 83 | 84 | 85 | Image 87 | 88 | 89 | 90 | Delete 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | res.partner.view.kanban 99 | res.partner 100 | 101 | 102 | 103 | 104 | 105 | 106 | Image 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 | -------------------------------------------------------------------------------- /odoo_module_migrate/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import argparse 6 | import argcomplete 7 | import sys 8 | 9 | from . import tools 10 | from .log import setup_logger 11 | from .migration import Migration 12 | 13 | 14 | def get_parser(): 15 | 16 | main_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 17 | 18 | main_parser.add_argument( 19 | "-d", 20 | "--directory", 21 | dest="directory", 22 | default="./", 23 | type=str, 24 | help="Target Modules directory. Set here a folder path" 25 | " that contains Odoo modules you want to migrate from a version" 26 | " to another.", 27 | ) 28 | 29 | main_parser.add_argument( 30 | "-m", 31 | "--modules", 32 | dest="modules", 33 | type=str, 34 | help="Target Modules to migrate." 35 | " If not set, all the modules present in the directory will be" 36 | " migrated.", 37 | ) 38 | 39 | main_parser.add_argument( 40 | "-i", 41 | "--init-version-name", 42 | choices=tools._get_available_init_version_names(), 43 | dest="init_version_name", 44 | required=True, 45 | type=str, 46 | ) 47 | 48 | main_parser.add_argument( 49 | "-t", 50 | "--target-version-name", 51 | dest="target_version_name", 52 | type=str, 53 | choices=tools._get_available_target_version_names(), 54 | default=tools._get_latest_version_name(), 55 | help="Target version of the Odoo module you want to migrate." 56 | " If 'latest' is set, the tool will try to migrate to the latest" 57 | " Odoo version.", 58 | ) 59 | 60 | main_parser.add_argument( 61 | "-fp", 62 | "--format-patch", 63 | action="store_true", 64 | help="Enable this option, if you want to get the code from the" 65 | " previous branch.", 66 | ) 67 | 68 | main_parser.add_argument( 69 | "-rn", 70 | "--remote-name", 71 | dest="remote_name", 72 | default="origin", 73 | type=str, 74 | ) 75 | 76 | main_parser.add_argument( 77 | "-ll", 78 | "--log-level", 79 | choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 80 | dest="log_level", 81 | default="INFO", 82 | type=str, 83 | ) 84 | 85 | main_parser.add_argument( 86 | "-lp", 87 | "--log-path", 88 | dest="log_path", 89 | default=False, 90 | type=str, 91 | ) 92 | 93 | main_parser.add_argument( 94 | "-nc", 95 | "--no-commit", 96 | action="store_true", 97 | default=False, 98 | help="Enable this option, if you don't want that the library commits" 99 | " the changes. (using git add and git commit command)", 100 | ) 101 | 102 | # TODO: Move to `argparse.BooleanOptionalAction` once in Python 3.9+ 103 | main_parser.add_argument( 104 | "-npc", 105 | "--no-pre-commit", 106 | dest="pre_commit", 107 | action="store_false", 108 | help="Skip pre-commit execution", 109 | ) 110 | 111 | # TODO: Move to `argparse.BooleanOptionalAction` once in Python 3.9+ 112 | main_parser.add_argument( 113 | "-nrmf", 114 | "--no-remove-migration-folder", 115 | dest="remove_migration_folder", 116 | action="store_false", 117 | help="Skip removing migration folder", 118 | ) 119 | 120 | return main_parser 121 | 122 | 123 | def main(args=False): 124 | # Parse Arguments 125 | parser = get_parser() 126 | argcomplete.autocomplete(parser, always_complete_options=False) 127 | if args: 128 | args = parser.parse_args(args) 129 | else: 130 | args = parser.parse_args() 131 | 132 | # Set log level 133 | setup_logger(args.log_level, args.log_path) 134 | 135 | try: 136 | # Create a new Migration Object 137 | module_names = ( 138 | args.modules 139 | and [x.strip() for x in args.modules.split(",") if x.strip()] 140 | or [] 141 | ) 142 | 143 | migration = Migration( 144 | args.directory, 145 | args.init_version_name, 146 | args.target_version_name, 147 | module_names, 148 | args.format_patch, 149 | args.remote_name, 150 | not args.no_commit, 151 | args.pre_commit, 152 | args.remove_migration_folder, 153 | ) 154 | 155 | # run Migration 156 | migration.run() 157 | 158 | except KeyboardInterrupt: 159 | pass 160 | 161 | 162 | if __name__ == "__main__": 163 | main(sys.argv[1:]) 164 | -------------------------------------------------------------------------------- /tests/data_template/module_170/views/res_partner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | res.partner.simplified.form 5 | res.partner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | res.partner.form 17 | res.partner 18 | 19 | tree,form 20 | 21 | 22 | 23 | 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | res.partner.complex.view 33 | res.partner 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | toggle_button 42 | 43 | 44 | toggle_button 45 | 46 | 47 | 48 | toggle_button 49 | 50 | 51 | 52 | 53 | 54 | 55 | tree_view_ref 56 | 57 | 58 | 59 | res.partner.chatter.form 60 | res.partner 61 | 62 |
63 | 64 | 65 | 66 |
67 | 68 | 69 | 70 | 71 |
72 | 73 |
74 |
75 | 76 |
77 | 78 |
79 | 80 | 81 |
82 | 83 | 84 | 85 | 86 | res.partner.view.kanban 87 | res.partner 88 | 89 | 90 | 91 | 92 | 93 | 94 | Image 96 | 97 | 98 | 99 | Delete 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | res.partner.view.kanban 108 | res.partner 109 | 110 | 111 | 112 | 113 | 114 | 115 | Image 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_130_140.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 - Today: Iván Todorovich 2 | # Copyright (C) 2020 - Today: Simone Rubino 3 | # @author: Iván Todorovich (https://twitter.com/ivantodorovich) 4 | # @author: Simone Rubino (https://github.com/Daemo00) 5 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 6 | import re 7 | from pathlib import Path 8 | import lxml.etree as et 9 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 10 | 11 | 12 | def src_model_new_value(field_elem, model_dot_name): 13 | """ 14 | Particular behavior for new binding_model_id. 15 | 16 | Apply some heuristic to change 17 | old attribute `src_model=account.move` 18 | to ``. 19 | """ 20 | module_name = model_dot_name.split(".")[0] 21 | model_und_name = model_dot_name.replace(".", "_") 22 | ref_value = f"{module_name}.model_{model_und_name}" 23 | field_elem.set("ref", ref_value) 24 | 25 | 26 | def value_to_text(field_elem, attr_value): 27 | """Standard behavior: an attribute value is the new fields's text.""" 28 | field_elem.text = attr_value 29 | 30 | 31 | TAG_ATTR_RENAMING = { 32 | "report": { 33 | "name": ("report_name", value_to_text), 34 | "string": ("name", value_to_text), 35 | }, 36 | "act_window": { 37 | "src_model": ("binding_model_id", src_model_new_value), 38 | }, 39 | } 40 | """ 41 | Configuration for renaming particular attributes. 42 | 43 | The dictionary maps old tag names to their attributes' names. 44 | Each old attribute name is then mapped a tuple containing: 45 | 46 | - name of the corresponding new field 47 | - a function that applies the old attribute's value to the new field 48 | """ 49 | 50 | 51 | def _reformat_file(file_path: Path): 52 | """Reformat `file_path`. 53 | 54 | Substitute `act_window` and `report` tag with `record` tag. 55 | Note that: 56 | - `id` attribute is kept in the `record` tag; 57 | - in `report` tag: 58 | - `string` has been renamed to `name`; 59 | - `name` has been renamed to `report_name`; 60 | - other attributes are assigned to respective fields 61 | """ 62 | parser = et.XMLParser(remove_blank_text=True) 63 | tree = et.parse(str(file_path.resolve()), parser) 64 | root = tree.getroot() 65 | reformat_tags = (*root.findall("act_window"), *root.findall("report")) 66 | if not reformat_tags: 67 | return None 68 | 69 | regexp = r"(?P[ \t]*)" r"(?P<{tag_type}[^/]*id=\"{tag_id}\"[^/]*/>)" 70 | 71 | new_tags_dict = dict() 72 | for tag in reformat_tags: 73 | tag_regex = regexp.format( 74 | tag_type=tag.tag, 75 | tag_id=tag.attrib["id"], 76 | ) 77 | for attrib, value in tag.attrib.items(): 78 | if attrib == "id": 79 | continue 80 | tag.attrib.pop(attrib) 81 | 82 | attr_renames_dict = TAG_ATTR_RENAMING.get(tag.tag, dict()) 83 | new_value_func = value_to_text 84 | if attrib in attr_renames_dict: 85 | new_attr_name, new_value_func = attr_renames_dict.get(attrib) 86 | attrib = new_attr_name 87 | 88 | field_elem = et.SubElement(tag, "field", {"name": attrib}) 89 | new_value_func(field_elem, value) 90 | 91 | tag.attrib["model"] = "ir.actions." + tag.tag 92 | tag.tag = "record" 93 | 94 | new_tags_dict[tag_regex] = tag 95 | 96 | # Read in the file 97 | xml_file = file_path.read_text() 98 | 99 | # Replace the target string 100 | for tag_regex, tag in new_tags_dict.items(): 101 | match = re.search(tag_regex, xml_file) 102 | if match: 103 | indent = match.group("indent") 104 | tag_match = match.group("tag") 105 | et.indent(tag, space=indent, level=1) 106 | # Remove trailing newline 107 | tag_string = et.tostring(tag, pretty_print=True)[:-1] 108 | xml_file = re.sub(tag_match, tag_string.decode(), xml_file) 109 | 110 | # Write the file out again 111 | file_path.write_text(xml_file) 112 | return file_path 113 | 114 | 115 | def _get_files(module_path, reformat_file_ext): 116 | """Get files to be reformatted.""" 117 | file_paths = list() 118 | if not module_path.is_dir(): 119 | raise Exception(f"'{module_path}' is not a directory") 120 | file_paths.extend(module_path.rglob("*" + reformat_file_ext)) 121 | return file_paths 122 | 123 | 124 | def reformat_deprecated_tags( 125 | logger, module_path, module_name, manifest_path, migration_steps, tools 126 | ): 127 | """Reformat deprecated tags in XML files. 128 | 129 | Deprecated tags are `act_window` and `report`: 130 | they have to be substituted by the `record` tag. 131 | """ 132 | 133 | reformat_file_ext = ".xml" 134 | file_paths = _get_files(module_path, reformat_file_ext) 135 | logger.debug(f"{reformat_file_ext} files found:\n" f"{list(map(str, file_paths))}") 136 | 137 | reformatted_files = list() 138 | for file_path in file_paths: 139 | reformatted_file = _reformat_file(file_path) 140 | if reformatted_file: 141 | reformatted_files.append(reformatted_file) 142 | logger.debug("Reformatted files:\n" f"{list(reformatted_files)}") 143 | 144 | 145 | _TEXT_REPLACES = { 146 | ".js": { 147 | r"tour\.STEPS\.SHOW_APPS_MENU_ITEM": "tour.stepUtils.showAppsMenuItem()", 148 | r"tour\.STEPS\.TOGGLE_HOME_MENU": "tour.stepUtils.toggleHomeMenu()", 149 | }, 150 | ".py": { 151 | r"\.phantom_js\(": ".browser_js(", 152 | }, 153 | } 154 | 155 | 156 | class MigrationScript(BaseMigrationScript): 157 | 158 | _GLOBAL_FUNCTIONS = [reformat_deprecated_tags] 159 | _TEXT_REPLACES = _TEXT_REPLACES 160 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_180_190.py: -------------------------------------------------------------------------------- 1 | # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). 2 | 3 | import ast 4 | import json 5 | import re 6 | from io import BytesIO 7 | 8 | from lxml import etree 9 | 10 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 11 | 12 | 13 | def migrate_expression_to_domain( 14 | logger, module_path, module_name, manifest_path, migration_steps, tools 15 | ): 16 | """Convert odoo.osv.expression usage to odoo.fields.Domain""" 17 | files_to_process = tools.get_files(module_path, (".py",)) 18 | 19 | for file in files_to_process: 20 | try: 21 | content = tools._read_content(file) 22 | original_content = content 23 | 24 | content = re.sub( 25 | r"from odoo\.osv import expression", 26 | "from odoo.fields import Domain", 27 | content, 28 | ) 29 | 30 | content = re.sub( 31 | r"from odoo\.osv\.expression import (AND|OR|AND, OR|OR, AND)", 32 | "from odoo.fields import Domain", 33 | content, 34 | ) 35 | 36 | content = re.sub(r"expression\.AND\(", "Domain.AND(", content) 37 | content = re.sub(r"expression\.OR\(", "Domain.OR(", content) 38 | 39 | content = re.sub(r"(? 60: 92 | args = f"\n{ind * 2}{definition!r}" 93 | else: 94 | args = repr(definition) 95 | result.append(f"_{name} = models.{constructor}({args})") 96 | return f"\n{ind}".join(result) 97 | 98 | # Process each file 99 | for file in files_to_process: 100 | content = tools._read_content(file) 101 | content = sql_expression_re.sub(build_sql_object, content) 102 | if sql_expression_re.search(content): 103 | logger.warning("Failed to replace sql_constraints") 104 | tools._write_content(file, content) 105 | 106 | 107 | def _remove_group_attrs_in_search_views( 108 | logger, module_path, module_name, manifest_path, migration_steps, tools 109 | ): 110 | """Remove `expand` and `string` attributes from tags when they 111 | are inside a view. 112 | """ 113 | 114 | files_to_process = tools.get_files(module_path, (".xml",)) 115 | 116 | for file_path in files_to_process: 117 | try: 118 | content = tools._read_content(file_path) 119 | parser = etree.XMLParser(recover=True) 120 | try: 121 | # lxml does not accept unicode strings with XML declaration, 122 | # so parse from bytes to be safe. 123 | tree = etree.parse(BytesIO(content.encode("utf-8")), parser) 124 | root = tree.getroot() 125 | except Exception: 126 | # If full-parse fails, skip this file 127 | continue 128 | 129 | changed = False 130 | 131 | # Find all elements and remove expand/string from children 132 | for search in root.findall(".//search"): 133 | for group in search.findall(".//group"): 134 | for attr in ("expand", "string"): 135 | if attr in group.attrib: 136 | del group.attrib[attr] 137 | changed = True 138 | 139 | if changed: 140 | # Write back modified tree 141 | new_content = etree.tostring( 142 | root, encoding="utf-8", xml_declaration=True 143 | ).decode("utf-8") 144 | new_content = new_content.replace( 145 | "", 146 | '', 147 | ) 148 | if not new_content.endswith("\n"): 149 | new_content += "\n" 150 | tools._write_content(file_path, new_content) 151 | logger.info( 152 | f"Removed expand/string attrs from in search views: {file_path}" 153 | ) 154 | 155 | except Exception as e: 156 | logger.error(f"Error processing XML file {file_path}: {e}") 157 | 158 | 159 | class MigrationScript(BaseMigrationScript): 160 | _GLOBAL_FUNCTIONS = [ 161 | upgrade_sql_constraints, 162 | migrate_expression_to_domain, 163 | _remove_group_attrs_in_search_views, 164 | ] 165 | -------------------------------------------------------------------------------- /tests/test_migration.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import os 6 | import re 7 | from filecmp import dircmp 8 | import pathlib 9 | import shutil 10 | import unittest 11 | 12 | from odoo_module_migrate.__main__ import main 13 | from odoo_module_migrate.tools import _read_content 14 | 15 | 16 | class TestMigration(unittest.TestCase): 17 | 18 | _template_path = pathlib.Path("./tests/data_template").resolve() 19 | _working_path = pathlib.Path("./tests/data_tmp").resolve() 20 | _expected_path = pathlib.Path("./tests/data_result").resolve() 21 | 22 | def _migrate_module( 23 | self, module_name, result_name, init_version_name, target_version_name 24 | ): 25 | shutil.rmtree(self._working_path, ignore_errors=True) 26 | shutil.copytree(self._template_path, self._working_path) 27 | 28 | main( 29 | [ 30 | "--directory", 31 | str(self._working_path), 32 | "--init-version-name", 33 | init_version_name, 34 | "--target-version-name", 35 | target_version_name, 36 | "--modules", 37 | module_name, 38 | "--log-path", 39 | str(self._working_path / "test_log.log"), 40 | "--no-commit", 41 | ] 42 | ) 43 | 44 | def _get_comparison(self, module_name, result_name): 45 | comparison = dircmp( 46 | str(self._expected_path / result_name), 47 | str(self._working_path / module_name), 48 | ) 49 | return comparison 50 | 51 | def _get_diff_files(self, comparison, folder): 52 | res = [os.path.join(folder, x) for x in comparison.diff_files] 53 | for subfolder, subcomparison in comparison.subdirs.items(): 54 | res += self._get_diff_files(subcomparison, os.path.join(folder, subfolder)) 55 | return res 56 | 57 | def test_migration_080_130(self): 58 | self._migrate_module("module_080", "module_080_130", "8.0", "13.0") 59 | comparison = self._get_comparison("module_080", "module_080_130") 60 | diff_files = self._get_diff_files(comparison, "./") 61 | self.assertEqual( 62 | len(diff_files), 63 | 0, 64 | "Differences found in the following files\n- %s" 65 | % ("\n- ".join(diff_files)), 66 | ) 67 | 68 | log_content = _read_content(str(self._working_path / "test_log.log")) 69 | 70 | required_logs = [ 71 | ("ERROR", "web_kanban_sparkline.*should remove the dependency"), 72 | ("WARNING", "Replaced.*account_analytic_analysis.*contract'"), 73 | ("ERROR", "deprecated decorator.*@api.cr"), 74 | ("ERROR", "ir.values.*removed"), 75 | ("ERROR", "removed module.*account_anglo_saxon"), 76 | ] 77 | 78 | for required_log in required_logs: 79 | level, message = required_log 80 | pattern = "{0}.*{1}".format(level, message) 81 | self.assertNotEqual( 82 | len(re.findall(pattern, log_content)), 83 | 0, 84 | "%s not found in the log" % pattern, 85 | ) 86 | 87 | def test_migration_120_130(self): 88 | self._migrate_module("module_120", "module_120_130", "12.0", "13.0") 89 | comparison = self._get_comparison("module_120", "module_120_130") 90 | diff_files = self._get_diff_files(comparison, "./") 91 | log_content = _read_content(str(self._working_path / "test_log.log")) 92 | self.assertEqual( 93 | len(diff_files), 94 | 0, 95 | "Differences found in the following files\n- %s" 96 | % ("\n- ".join(diff_files)), 97 | ) 98 | 99 | required_logs = [ 100 | ( 101 | "WARNING", 102 | "[13].*change.*env\\.user\\.company_id.*to.*\\.env\\.company", 103 | ), 104 | ] 105 | for required_log in required_logs: 106 | level, message = required_log 107 | pattern = "{0}.*{1}".format(level, message) 108 | self.assertNotEqual( 109 | len(re.findall(pattern, log_content)), 110 | 0, 111 | "%s not found in the log" % pattern, 112 | ) 113 | 114 | def test_migration_130_140(self): 115 | self._migrate_module("module_130", "module_130_140", "13.0", "14.0") 116 | comparison = self._get_comparison("module_130", "module_130_140") 117 | diff_files = self._get_diff_files(comparison, "./") 118 | self.assertEqual( 119 | len(diff_files), 120 | 0, 121 | "Differences found in the following files\n- %s" 122 | % ("\n- ".join(diff_files)), 123 | ) 124 | 125 | def test_migration_150_160(self): 126 | self._migrate_module("module_150", "module_150_160", "15.0", "16.0") 127 | comparison = self._get_comparison("module_150", "module_150_160") 128 | diff_files = self._get_diff_files(comparison, "./") 129 | self.assertEqual( 130 | len(diff_files), 131 | 0, 132 | "Differences found in the following files\n- %s" 133 | % ("\n- ".join(diff_files)), 134 | ) 135 | 136 | def test_migration_160_170(self): 137 | self._migrate_module("module_160", "module_160_170", "16.0", "17.0") 138 | comparison = self._get_comparison("module_160", "module_160_170") 139 | diff_files = self._get_diff_files(comparison, "./") 140 | self.assertEqual( 141 | len(diff_files), 142 | 0, 143 | "Differences found in the following files\n- %s" 144 | % ("\n- ".join(diff_files)), 145 | ) 146 | log_content = _read_content(str(self._working_path / "test_log.log")) 147 | required_logs = [ 148 | ( 149 | "WARNING", 150 | "[17].*'web.assets_common'.*This bundle has been removed.*", 151 | ), 152 | ] 153 | for required_log in required_logs: 154 | level, message = required_log 155 | pattern = "{0}.*{1}".format(level, message) 156 | self.assertNotEqual( 157 | len(re.findall(pattern, log_content)), 158 | 0, 159 | "%s not found in the log" % pattern, 160 | ) 161 | 162 | def test_migration_170_180(self): 163 | self._migrate_module("module_170", "module_170_180", "17.0", "18.0") 164 | comparison = self._get_comparison("module_170", "module_170_180") 165 | diff_files = self._get_diff_files(comparison, "./") 166 | self.assertEqual( 167 | len(diff_files), 168 | 0, 169 | "Differences found in the following files\n- %s" 170 | % ("\n- ".join(diff_files)), 171 | ) 172 | 173 | def test_migration_180_190(self): 174 | self._migrate_module("module_180", "module_180_190", "18.0", "19.0") 175 | comparison = self._get_comparison("module_180", "module_180_190") 176 | diff_files = self._get_diff_files(comparison, "./") 177 | self.assertEqual( 178 | len(diff_files), 179 | 0, 180 | "Differences found in the following files\n- %s" 181 | % ("\n- ".join(diff_files)), 182 | ) 183 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_170_180.py: -------------------------------------------------------------------------------- 1 | # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). 2 | # This script is based on the original code from: 3 | # https://github.com/odoo/odoo/blob/master/odoo/upgrade_code/17.5-00-tree-to-list.py 4 | 5 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 6 | import re 7 | 8 | 9 | def replace_tree_with_list_in_views( 10 | logger, module_path, module_name, manifest_path, migration_steps, tools 11 | ): 12 | files_to_process = tools.get_files(module_path, (".xml", ".js", ".py")) 13 | 14 | reg_tree_to_list_xml_mode = re.compile( 15 | r"""(]* name=["'](view_mode|name|binding_view_types)["'][^>]*>([^<>]+[,.])?\s*)tree(\s*([,.][^<>]+)?)""" 16 | ) 17 | reg_tree_to_list_tag = re.compile(r"([<,/])tree([ \n\r,>/])") 18 | reg_tree_to_list_xpath = re.compile( 19 | r"""(]* expr=['"])([^<>]*/)?tree(/|[\['"])""" 20 | ) 21 | reg_tree_to_list_ref = re.compile(r"tree_view_ref") 22 | reg_tree_to_list_mode = re.compile(r"""(mode=['"][^'"]*)tree([^'"]*['"])""") 23 | reg_tree_to_list_view_mode = re.compile( 24 | r"""(['"]view_mode['"][^'":=]*[:=].*['"]([^'"]+,)?\s*)tree(\s*(,[^'"]+)?['"])""" 25 | ) 26 | reg_tree_to_list_view = re.compile( 27 | r"""(['"]views['"][^'":]*[:=].*['"])tree(['"])""" 28 | ) 29 | reg_tree_to_list_string = re.compile(r"""([ '">)])tree( [vV]iews?[ '"<.)])""") 30 | reg_tree_to_list_String = re.compile(r"""([ '">)])Tree( [vV]iews?[ '"<.)])""") 31 | reg_tree_to_list_env_ref = re.compile(r"""(self\.env\.ref\(.*['"])tree(['"])""") 32 | 33 | for file in files_to_process: 34 | try: 35 | content = tools._read_content(file) 36 | content = content.replace(" tree view ", " list view ") 37 | content = reg_tree_to_list_xml_mode.sub(r"\1list\4", content) 38 | content = reg_tree_to_list_tag.sub(r"\1list\2", content) 39 | content = reg_tree_to_list_xpath.sub(r"\1\2list\3", content) 40 | content = reg_tree_to_list_ref.sub("list_view_ref", content) 41 | content = reg_tree_to_list_mode.sub(r"\1list\2", content) 42 | content = reg_tree_to_list_view_mode.sub(r"\1list\3", content) 43 | content = reg_tree_to_list_view.sub(r"\1list\2", content) 44 | content = reg_tree_to_list_string.sub(r"\1list\2", content) 45 | content = reg_tree_to_list_String.sub(r"\1List\2", content) 46 | content = reg_tree_to_list_env_ref.sub(r"\1list\2", content) 47 | 48 | tools._write_content(file, content) 49 | 50 | except Exception as e: 51 | logger.error(f"Error processing file {file}: {str(e)}") 52 | 53 | 54 | def replace_chatter_blocks( 55 | logger, module_path, module_name, manifest_path, migration_steps, tools 56 | ): 57 | files_to_process = tools.get_files(module_path, (".xml",)) 58 | 59 | reg_chatter_block = r"""
]*position=["'][^"']+["'])[^>]*>[\s\S]*?
""" 60 | reg_xpath_chatter = r"""//div\[hasclass\(['"]oe_chatter['"]\)\]""" 61 | reg_chatter_with_position_self_closing = ( 62 | r"""
""" 63 | ) 64 | 65 | replacement_div = "" 66 | replacement_xpath = "//chatter" 67 | 68 | def replace_chatter_self_closing(match): 69 | position = match.group(1) 70 | return f"" 71 | 72 | replaces = { 73 | reg_chatter_block: replacement_div, 74 | reg_xpath_chatter: replacement_xpath, 75 | reg_chatter_with_position_self_closing: replace_chatter_self_closing, 76 | } 77 | 78 | for file in files_to_process: 79 | try: 80 | tools._replace_in_file( 81 | file, replaces, log_message=f"Updated chatter blocks in file: {file}" 82 | ) 83 | except Exception as e: 84 | logger.error(f"Error processing file {file}: {str(e)}") 85 | 86 | 87 | def replace_deprecated_kanban_box_card_menu( 88 | logger, module_path, module_name, manifest_path, migration_steps, tools 89 | ): 90 | files_to_process = tools.get_files(module_path, (".xml", ".js", ".py")) 91 | replaces = { 92 | "kanban-card": "card", 93 | "kanban-box": "card", 94 | "kanban-menu": "menu", 95 | } 96 | for file in files_to_process: 97 | try: 98 | tools._replace_in_file( 99 | file, 100 | replaces, 101 | log_message=f"""Replace kanban-card and kanban-box with card, also change kanban-menu with menu" in file: {file}""", 102 | ) 103 | except Exception as e: 104 | logger.error(f"Error processing file {file}: {str(e)}") 105 | 106 | 107 | def replace_user_has_groups( 108 | logger, module_path, module_name, manifest_path, migration_steps, tools 109 | ): 110 | files_to_process = tools.get_files(module_path, (".py",)) 111 | replaces = { 112 | r"self\.user_has_groups\(\s*(['\"])([\w\.]+)\1\s*\)": r"self.env.user.has_group(\1\2\1)", 113 | r"self\.user_has_groups\(\s*(['\"])([^'\"]*[,!][^'\"]*?)\1\s*\)": r"self.env.user.has_groups(\1\2\1)", 114 | } 115 | 116 | for file in files_to_process: 117 | try: 118 | tools._replace_in_file(file, replaces) 119 | except Exception as e: 120 | logger.error(f"Error processing file {file}: {str(e)}") 121 | 122 | 123 | def replace_unaccent_parameter( 124 | logger, module_path, module_name, manifest_path, migration_steps, tools 125 | ): 126 | files_to_process = tools.get_files(module_path, (".py",)) 127 | replaces = { 128 | # Handle multiline with unaccent=False or unaccent=True 129 | r"(?s)fields\.(Char|Text|Html|Properties)\(\s*unaccent\s*=\s*(False|True)\s*,?\s*\)": r"fields.\1()", 130 | # Handle when unaccent=False or unaccent=True is the first parameter 131 | r"(?s)fields\.(Char|Text|Html|Properties)\(\s*unaccent\s*=\s*(False|True)\s*,\s*([^)]+?)\)": r"fields.\1(\3)", 132 | # Handle when unaccent=False or unaccent=True is between other parameters 133 | r"(?s)fields\.(Char|Text|Html|Properties)\(([^)]+?),\s*unaccent\s*=\s*(False|True)\s*,\s*([^)]+?)\)": r"fields.\1(\2, \4)", 134 | # Handle when unaccent=False or unaccent=True is the last parameter 135 | r"(?s)fields\.(Char|Text|Html|Properties)\(([^)]+?),\s*unaccent\s*=\s*(False|True)\s*\)": r"fields.\1(\2)", 136 | } 137 | 138 | for file in files_to_process: 139 | try: 140 | tools._replace_in_file( 141 | file, 142 | replaces, 143 | log_message=f"[18.0] Removed deprecated unaccent=False parameter in file: {file}", 144 | ) 145 | except Exception as e: 146 | logger.error(f"Error processing file {file}: {str(e)}") 147 | 148 | 149 | def replace_ustr( 150 | logger, module_path, module_name, manifest_path, migration_steps, tools 151 | ): 152 | files_to_process = tools.get_files(module_path, (".py",)) 153 | replaces = { 154 | r"from\s+odoo\.tools\s+import\s+ustr\s*\n": "", 155 | r"from\s+odoo\.tools\.misc\s+import\s+ustr\s*\n": "", 156 | r"from\s+odoo\.tools\s+import\s+([^,\n]*,\s*)?ustr,\s*([^,\n]*)": r"from odoo.tools import \1\2", 157 | r"from\s+odoo\.tools\.misc\s+import\s+([^,\n]*,\s*)?ustr,\s*([^,\n]*)": r"from odoo.tools.misc import \1\2", 158 | r",\s*ustr(\s*,)?": r"\1", 159 | r"tools\.ustr\(([^)]+)\)": r"\1", 160 | r"misc\.ustr\(([^)]+)\)": r"\1", 161 | r"=\s*ustr\(([^)]+)\)": r"= \1", 162 | } 163 | for file in files_to_process: 164 | try: 165 | tools._replace_in_file( 166 | file, replaces, log_message=f"Deprecate ustr in: {file}" 167 | ) 168 | except Exception as e: 169 | logger.error(f"Error processing file {file}: {str(e)}") 170 | 171 | 172 | class MigrationScript(BaseMigrationScript): 173 | _GLOBAL_FUNCTIONS = [ 174 | replace_unaccent_parameter, 175 | replace_deprecated_kanban_box_card_menu, 176 | replace_tree_with_list_in_views, 177 | replace_chatter_blocks, 178 | replace_user_has_groups, 179 | replace_ustr, 180 | ] 181 | -------------------------------------------------------------------------------- /DEVELOP.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. code-block:: shell 5 | 6 | # Pull Code 7 | git clone https://github.com/grap/odoo-module-migrator 8 | cd odoo-module-migrator 9 | 10 | # Create virtual env and activate it 11 | virtualenv env --python=python3 12 | . ./env/bin/activate 13 | 14 | # Install dependencies 15 | pip3 install -r requirements.txt 16 | 17 | # Run the script 18 | python -m odoo_module_migrate COMMAND OPTIONS 19 | 20 | You can also install from test source via pip: 21 | 22 | .. code-block:: shell 23 | 24 | pip3 install odoo-module-migrator\ 25 | --upgrade\ 26 | --index-url https://test.pypi.org/simple\ 27 | --extra-index-url https://pypi.org/simple 28 | 29 | Run tests 30 | ========= 31 | 32 | .. code-block:: shell 33 | 34 | # Activate virtual env 35 | . ./env/bin/activate 36 | 37 | # Install extra dependencies required for tests 38 | pip3 install -r test_requirements.txt 39 | 40 | # Run Tests 41 | coverage run --source odoo_module_migrate setup.py test 42 | 43 | Structure of the Project 44 | ======================== 45 | 46 | Framework 47 | --------- 48 | 49 | In the ``odoo_module_migrate`` folder 50 | 51 | * ``__main__.py``: the entry point of this module. It mainly 52 | handles arguments, and launch the script. 53 | 54 | * ``config.py``: configuration file, to updated if new features are available 55 | in this library. (For exemple, a new migration available from X to X+1) 56 | 57 | * ``log.py``: Handle logs of this library. 58 | 59 | * ``tools.py``: bundle of generic functions used by the framework. 60 | 61 | * ``migration.py``: Define the class ``Migration`` that handle a migration 62 | from a version X.0 to a version Y.0 for any module. 63 | 64 | * ``module_migration.py``: Define the class ``ModuleMigration`` that handle 65 | a migration for a given module. 66 | 67 | 68 | Migration Scripts 69 | ----------------- 70 | 71 | The list of the operations are written in the subfolder 72 | ``__: 73 | 74 | * each operaion has specification when it has to be applied: ``FROM_TO`` part of the path. 75 | 76 | * `Possible values are `__ ``090``, ``010``, ``011``, etc. 77 | * The ``TO`` part may have value ``always``, which means is should be applied in every version since ``FROM`` version. 78 | * Special value for ``FROM_TO`` is just a single ``allways`` (yes, it's mispealing) which means it will be applied whatever the init and target odoo version. 79 | 80 | * ``migrate_FROM_TO.py`` — old way to specify operations. Normally, these files should not be changed. 81 | 82 | * ``file_renames/migrate_FROM_TO/NAME.yaml`` — file renaming rules. For 83 | example, for migration from version 8.0 to more recent version: 84 | 85 | .. code-block:: yaml 86 | 87 | __openerp__.py: __manifest__.py 88 | 89 | * ``text_replaces/migrate_FROM_TO/NAME.yaml`` — replace pattern text by 90 | another. for example, for migration from version 8.0 to version 9.0: 91 | 92 | .. code-block:: yaml 93 | 94 | .py: 95 | select=True: index=True 96 | 97 | 98 | * ``text_errors/migrate_FROM_TO/NAME.yaml`` — display errors if files contains a 99 | given partern. For example, for migration from version 10.0 to version 11.0: 100 | 101 | .. code-block:: yaml 102 | 103 | "*": 104 | ir.values: "ir.values table does not exist anymore" 105 | 106 | * ``text_warnings/migrate_FROM_TO/NAME.yaml`` — display errors if files contains a 107 | given partern. For example, for migration from version 12.0 to version 13.0: 108 | 109 | .. code-block:: yaml 110 | 111 | "*": 112 | "@api.returns": "decorator @api.returns is deprecated" 113 | 114 | * ``deprecated_modules/migrate_FROM_TO/NAME.yaml`` — dependencies to obsoletes modules. There are following possibilities: 115 | 116 | * the module has been fully removed. 117 | * the module has been renamed. 118 | * the module features has been merged into another module. 119 | * the module has been moved under OCA umbrella. (w/o another name) 120 | 121 | .. code-block:: yaml 122 | 123 | - ["account_anglo_saxon", "removed"] 124 | - ["account_check_writing", "renamed", "account_check_printing"] 125 | - ["account_chart", "merged", "account"] 126 | - ["account_analytic_analysis", "oca_moved", "contract", "Moved to OCA/contract"] 127 | 128 | * ``python_scripts/migrate_FROM_TO/NAME.py`` — for complex updates/checks. Must contain one or few functions that don't start with underscore symbol. The functions take following keyword arguments: 129 | 130 | * ``logger`` 131 | * ``module_path`` 132 | * ``module_name`` 133 | * ``manifest_path`` 134 | * ``migration_steps`` — list of steps. See ``_AVAILABLE_MIGRATION_STEPS`` in ``__ 135 | * ``tools`` — python module with some functions. See ``__ 136 | 137 | .. code-block:: py 138 | 139 | def set_module_installable(**kwargs): 140 | tools = kwargs['tools'] 141 | manifest_path = kwargs['manifest_path'] 142 | old_term = r"('|\")installable('|\").*(False)" 143 | new_term = r"\1installable\2: True" 144 | tools._replace_in_file( 145 | manifest_path, {old_term: new_term}, "Set module installable") 146 | 147 | * ``removed_fields/migrate_FROM_TO/NAME.yaml`` — removed fields rule. Give warnings if field name is found on the code. 148 | To minimize two many false positives we search for field name on this situations: 149 | * with simple/double quotes 150 | * prefixed with dot and with space, comma or equal after the string 151 | 152 | For example, for migration from version 15.0 to 16.0: 153 | .. code-block:: yaml 154 | - ['product.product', 'price', 'Commit https://github.com/odoo/odoo/commit/9e99a9df464d97a74ca320d'] 155 | 156 | * ``renamed_fields/migrate_FROM_TO/NAME.yaml`` — renamed fields rule. Give warnings if old field name is found on the code. 157 | To minimize two many false positives we search for field name on this situations: 158 | * with simple/double quotes 159 | * prefixed with dot and with space, comma or equal after the string 160 | 161 | For example, for migration from version 15.0 to 16.0: 162 | .. code-block:: yaml 163 | - ['account.account', 'user_type_id', 'account_type', 'Commit https://github.com/odoo/odoo/commit/26b2472f4977ccedbb0b5ed5f'] 164 | 165 | * ``removed_models/migrate_FROM_TO/NAME.yaml`` — removed models rule. Display errors / warnings if files contains a given partern: 166 | * errors: "old_model_name", 'old_model_name', old_table_name["',] 167 | * warnings: old.model.name, old_model_name 168 | 169 | For example, for migration from version 15.0 to 16.0: 170 | .. code-block:: yaml 171 | - ["account.account.type", "Commit https://github.com/odoo/odoo/commit/26b2472f4977ccedbb0b5ed5f"] 172 | 173 | * ``renamed_models/migrate_FROM_TO/NAME.yaml`` — renamed models rule. Display errors / warnings if files contains a given partern: 174 | * errors: "old_model_name", 'old_model_name', old_table_name["',] 175 | * warnings: old.model.name, old_model_name 176 | 177 | For example, for migration from version 15.0 to 16.0: 178 | .. code-block:: yaml 179 | - ["stock.production.lot", "stock.lot", None] 180 | 181 | How to improve the library 182 | ========================== 183 | 184 | * Read (or complete !) the migration advices of the OCA. 185 | https://github.com/OCA/maintainer-tools/wiki#migration 186 | 187 | * Read the complementary pages 188 | https://odoo-development.readthedocs.io/en/latest/migration/ 189 | 190 | * Discover what changed between two revisions, reading OpenUpgrade 191 | documentation, specially the modules changes, for exemple: 192 | https://github.com/OCA/OpenUpgrade/blob/12.0/odoo/openupgrade/doc/source/modules110-120.rst 193 | 194 | * Create or complete the according migration file. 195 | 196 | * Add tests. 197 | 198 | * Make a Pull request. 199 | 200 | Package deployment 201 | ================== 202 | 203 | .. code-block:: shell 204 | 205 | pip3 install --upgrade setuptools wheel 206 | pip3 install --upgrade twine 207 | 208 | # Generate wheel and package 209 | python3 setup.py sdist bdist_wheel 210 | 211 | # Push on pyPi Test 212 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 213 | 214 | # Push on pyPi Production 215 | twine upload dist/* 216 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg 2 | :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html 3 | :alt: License: AGPL-3 4 | .. image:: https://img.shields.io/badge/python-3.6-blue.svg 5 | :alt: Python support: 3.6 6 | .. image:: https://app.travis-ci.com/OCA/odoo-module-migrator.svg?branch=master 7 | :target: https://app.travis-ci.com/OCA/odoo-module-migrator 8 | 9 | ==================== 10 | odoo-module-migrator 11 | ==================== 12 | 13 | ``odoo-module-migrator`` is a python3 library that allows you to automatically migrate 14 | module code to make it compatible with newer Odoo versions. 15 | For example: 16 | 17 | * renaming ``__openerp__.py`` file to ``__manifest__.py`` 18 | * removing ``# -*- encoding: utf-8 -*-`` since V11.0 19 | * replacing ``openerp`` import with ``odoo`` import 20 | * removing ``migrations`` folders 21 | * changing to 22 | * ... 23 | 24 | This library will: 25 | 26 | * (optionally) get commits from the old branch (if format-patch is enabled) 27 | * automatically apply changes (renaming, replacing, etc.) 28 | * commit your changes 29 | * display warnings or errors in the log if your code contains obsolete code patterns 30 | 31 | This project is about migrating code. If you're looking for database data migration 32 | between Odoo versions, check out the https://github.com/OCA/OpenUpgrade project. 33 | 34 | **INFO log** 35 | 36 | Indicates that the library automatically changed something. 37 | *A priori* you have nothing to do. For example, for a migration from 8.0 to 38 | a more recent version: 39 | 40 | .. code-block:: shell 41 | 42 | 12:38:54 INFO Renaming file: '/my_module/__openerp__.py' to '/my_module/__manifest__.py' 43 | 44 | **WARNING log** 45 | 46 | Indicates that you should check something. There may be something to do 47 | to make the module work. For example: 48 | 49 | .. code-block:: shell 50 | 51 | 19:37:55 WARNING Replaced dependency of 'account_analytic_analysis' with 'contract' (Moved to OCA/contract) 52 | 53 | **ERROR log** 54 | 55 | Indicates that you must change something in your code. If not, the module 56 | will not work *for sure* (not installable or generating errors during 57 | execution). 58 | 59 | For example, if you have an 8.0 module that depends on 'account_anglo_saxon', 60 | which disappeared in more recent versions, the following log will be displayed: 61 | 62 | .. code-block:: shell 63 | 64 | 12:38:54 ERROR Depends on removed module 'account_anglo_saxon' 65 | 66 | Development and Improvement 67 | =========================== 68 | 69 | If you want to improve or contribute to this library, please read the 70 | ``DEVELOP.rst`` file and the 'Roadmap / Known Issues' sections. 71 | 72 | Installation 73 | ============ 74 | 75 | .. code-block:: shell 76 | 77 | pip3 install odoo-module-migrator 78 | 79 | Usage 80 | ===== 81 | 82 | Using Format Patch command 83 | -------------------------- 84 | 85 | (Recommended by the OCA) 86 | 87 | If you want to migrate an Odoo module from version 8.0 to 12.0, for example, 88 | the module ``pos_order_pricelist_change`` in the OCA "pos" repository: 89 | 90 | .. code-block:: shell 91 | 92 | git clone https://github.com/OCA/pos -b 12.0 93 | cd pos 94 | odoo-module-migrate 95 | --directory ./ 96 | --modules pos_order_pricelist_change 97 | --init-version-name 8.0 98 | --target-version-name 12.0 99 | --format-patch 100 | 101 | Without format Patch command 102 | ---------------------------- 103 | 104 | (Mainly for your custom modules) 105 | 106 | If you have created a new branch (for example 12.0) based on your 10.0 branch, 107 | you can run the following command: 108 | 109 | .. code-block:: shell 110 | 111 | odoo-module-migrate 112 | --directory /path/to/repository 113 | --modules module_1,module_2,module_3 114 | --init-version-name 10.0 115 | --target-version-name 12.0 116 | 117 | This tool will operate the changes for each module. 118 | 119 | Available Arguments 120 | ------------------- 121 | 122 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 123 | | Name | Shortcut | Options | Description | 124 | +==========================+==========+=================+=======================================================+ 125 | |``--directory`` | ``-d`` | Default: | Local folder that contains the module(s) to migrate. | 126 | | | | ``./`` | | 127 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 128 | |``--init-version-name`` | ``-i`` | Required | Initial version of your module(s) you want to migrate.| 129 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 130 | |``--target-version-name`` | ``-t`` | Default: | Final version you want to migrate to. | 131 | | | | the latest Odoo | | 132 | | | | version | | 133 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 134 | |``--modules`` | ``-m`` | Default: | Module(s) to migrate. Note: if the format-patch option| 135 | | | | All modules | is enabled, you have to provide only one module. | 136 | | | | in the | | 137 | | | | directory | | 138 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 139 | |``--format-patch`` | ``-fp`` | Disabled | Recover code using the git format-patch command. | 140 | | | | by default | | 141 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 142 | |``--remote-name`` | ``-rn`` | Default: | Name of the main remote used by the format-patch | 143 | | | | ``origin`` | command. | 144 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 145 | |``--log-level`` | ``-ll`` | Default: | Possible values: ``DEBUG``, ``INFO``, ``WARNING``, | 146 | | | | ``INFO`` | etc. | 147 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 148 | |``--log-path`` | ``-lp`` | Default: | If set, logs will not be displayed on screen, | 149 | | | | False | but stored in a file. | 150 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 151 | |``--no-commit`` | ``-nc`` | Default: | If set, the library will not git add and git commit | 152 | | | | commit | changes. | 153 | +--------------------------+----------+-----------------+-------------------------------------------------------+ 154 | 155 | Roadmap / Known Issues 156 | ====================== 157 | 158 | * The replacement of the tag with will fail in cases 159 | where there are many occurrences. 160 | We could fix that by using the ``lxml`` library instead of regular expressions. 161 | 162 | * Add a call to ``pre-commit run -a``, if pre-commit is present in the 163 | repository. 164 | 165 | Changes 166 | ======= 167 | 168 | 0.3.2 (December 04, 2019) 169 | ------------------------- 170 | * [REM] Remove black call (add call to a more generic tool pre-commit 171 | in the roadmap). 172 | * [IMP] Add --no-verify option in ``git commit`` to avoid failure if pre-commit 173 | is present. 174 | * [REF] Refactor ``_execute_shell`` function. 175 | 176 | 0.2.0 (October 13, 2019) 177 | ------------------------ 178 | * Second release. 179 | 180 | 0.1.4 (October 12, 2019) 181 | ------------------------ 182 | * Test 183 | 184 | [ADD] test. 185 | 186 | * Framework 187 | 188 | [ADD] ``--file-path`` option. 189 | [ADD] ``_DEPRECATED_MODULES`` syntax. 190 | 191 | * Migration script 192 | 193 | [FIX] Incorrect syntax of regular expression to remove python 2 header. 194 | [IMP] First release of all the steps from 8.0 to 13.0. 195 | 196 | 0.1.3 (October 11, 2019) 197 | ------------------------ 198 | 199 | * Framework 200 | 201 | [ADD] ``--no-commit`` option that disables git add and git commit calls. 202 | [FIX] Do not commit many times if migration has many steps. 203 | [REF] Remove useless commented code. 204 | [REF] Create ``_commit_changes()`` and ``_replace_in_file()`` functions. 205 | 206 | * Meta 207 | 208 | [FIX] GitHub URL of the project in setup.py. 209 | [ADD] Travis file + links to coveralls. 210 | [ADD] test_requirements.txt. 211 | 212 | * Migration script 213 | 214 | [ADD] Migration from 12.0 to 13.0 and add a warning if references to web_settings_dashboard are found, courtesy of @yelizariev. 215 | [ADD] Bump version in manifest file. 216 | [ADD] Set installable to True. 217 | 218 | 0.1.2 (October 10, 2019) 219 | ------------------------ 220 | 221 | * First release. 222 | 223 | Credits 224 | ======= 225 | 226 | Authors 227 | ------- 228 | 229 | * GRAP, Groupement Régional Alimentaire de Proximité (http://www.grap.coop) 230 | 231 | Contributors 232 | ------------ 233 | 234 | * Sylvain LE GAL (https://www.twitter.com/legalsylvain) 235 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) 2 | # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import importlib 6 | import os 7 | import pathlib 8 | import pkgutil 9 | import inspect 10 | 11 | from .config import _AVAILABLE_MIGRATION_STEPS, _MANIFEST_NAMES 12 | from .exception import ConfigException 13 | from .log import logger 14 | from .tools import _execute_shell, _get_latest_version_code 15 | from .module_migration import ModuleMigration 16 | from .base_migration_script import BaseMigrationScript 17 | 18 | 19 | class Migration: 20 | def __init__( 21 | self, 22 | relative_directory_path, 23 | init_version_name, 24 | target_version_name, 25 | module_names=None, 26 | format_patch=False, 27 | remote_name="origin", 28 | commit_enabled=True, 29 | pre_commit=True, 30 | remove_migration_folder=True, 31 | ): 32 | if not module_names: 33 | module_names = [] 34 | self._commit_enabled = commit_enabled 35 | self._pre_commit = pre_commit 36 | self._remove_migration_folder = remove_migration_folder 37 | self._migration_steps = [] 38 | self._migration_scripts = [] 39 | self._module_migrations = [] 40 | self._directory_path = False 41 | 42 | # Get migration steps that will be runned 43 | found = False 44 | for item in _AVAILABLE_MIGRATION_STEPS: 45 | if not found and item["init_version_name"] != init_version_name: 46 | continue 47 | else: 48 | found = True 49 | self._migration_steps.append(item) 50 | if item["target_version_name"] == target_version_name: 51 | # This is the last step, exiting 52 | break 53 | 54 | # Check consistency between format patch and module_names args 55 | if format_patch and len(module_names) != 1: 56 | raise ConfigException( 57 | "Format patch option can only be used for a single module" 58 | ) 59 | logger.debug("Module list: %s" % module_names) 60 | logger.debug("format patch option : %s" % format_patch) 61 | 62 | # convert relative or absolute directory into Path Object 63 | if not os.path.exists(relative_directory_path): 64 | raise ConfigException( 65 | "Unable to find directory: %s" % relative_directory_path 66 | ) 67 | 68 | root_path = pathlib.Path(relative_directory_path) 69 | self._directory_path = pathlib.Path(root_path.resolve(strict=True)) 70 | 71 | # format-patch, if required 72 | if format_patch: 73 | if not (root_path / module_names[0]).is_dir(): 74 | self._get_code_from_previous_branch(module_names[0], remote_name) 75 | else: 76 | logger.warning( 77 | "Ignoring format-patch argument, as the module %s" 78 | " is still present in the repository" % (module_names[0]) 79 | ) 80 | 81 | # Guess modules if not provided, and check validity 82 | if not module_names: 83 | module_names = [] 84 | # Recover all submodules, if no modules list is provided 85 | child_paths = [x for x in root_path.iterdir() if x.is_dir()] 86 | for child_path in child_paths: 87 | if self._is_module_path(child_path): 88 | module_names.append(child_path.name) 89 | else: 90 | child_paths = [root_path / x for x in module_names] 91 | for child_path in child_paths: 92 | if not self._is_module_path(child_path): 93 | module_names.remove(child_path.name) 94 | logger.warning( 95 | "No valid module found for '%s' in the directory '%s'" 96 | % (child_path.name, root_path.resolve()) 97 | ) 98 | 99 | if not module_names: 100 | raise ConfigException("No modules found to migrate. Exiting.") 101 | 102 | for module_name in module_names: 103 | self._module_migrations.append(ModuleMigration(self, module_name)) 104 | 105 | if os.path.exists(".pre-commit-config.yaml") and self._pre_commit: 106 | self._run_pre_commit(module_names) 107 | 108 | # get migration scripts, depending to the migration list 109 | self._get_migration_scripts() 110 | 111 | def _run_pre_commit(self, module_names): 112 | logger.info("Run pre-commit") 113 | _execute_shell( 114 | "pre-commit run -a", path=self._directory_path, raise_error=False 115 | ) 116 | if self._commit_enabled: 117 | logger.info("Stage and commit changes done by pre-commit") 118 | _execute_shell("git add -A", path=self._directory_path) 119 | _execute_shell( 120 | "git commit -m '[IMP] %s: pre-commit execution' --no-verify" 121 | % ", ".join(module_names), 122 | path=self._directory_path, 123 | raise_error=False, # Don't fail if there is nothing to commit 124 | ) 125 | 126 | def _is_module_path(self, module_path): 127 | return any([(module_path / x).exists() for x in _MANIFEST_NAMES]) 128 | 129 | def _get_code_from_previous_branch(self, module_name, remote_name): 130 | init_version = self._migration_steps[0]["init_version_name"] 131 | target_version = self._migration_steps[-1]["target_version_name"] 132 | branch_name = "%(version)s-mig-%(module_name)s" % { 133 | "version": target_version, 134 | "module_name": module_name, 135 | } 136 | 137 | logger.info("Creating new branch '%s' ..." % (branch_name)) 138 | _execute_shell( 139 | "git checkout --no-track -b %(branch)s %(remote)s/%(version)s" 140 | % { 141 | "branch": branch_name, 142 | "remote": remote_name, 143 | "version": target_version, 144 | }, 145 | path=self._directory_path, 146 | ) 147 | 148 | logger.info("Getting latest changes from old branch") 149 | # Depth is added just in case you had a shallow git history 150 | _execute_shell( 151 | "git fetch --depth 9999999 %(remote)s %(init)s" 152 | % { 153 | "remote": remote_name, 154 | "init": init_version, 155 | }, 156 | path=self._directory_path, 157 | ) 158 | 159 | _execute_shell( 160 | "git format-patch --keep-subject " 161 | "--stdout %(remote)s/%(target)s..%(remote)s/%(init)s " 162 | "-- %(module)s | git am -3 --keep" 163 | % { 164 | "remote": remote_name, 165 | "init": init_version, 166 | "target": target_version, 167 | "module": module_name, 168 | }, 169 | path=self._directory_path, 170 | ) 171 | 172 | def _load_migration_script(self, full_name): 173 | module = importlib.import_module(full_name) 174 | result = [ 175 | x[1]() 176 | for x in inspect.getmembers(module, inspect.isclass) 177 | if x[0] != "BaseMigrationScript" and issubclass(x[1], BaseMigrationScript) 178 | ] 179 | return result 180 | 181 | def _get_migration_scripts(self): 182 | # Add the script that will be allways executed 183 | self._migration_scripts.extend( 184 | self._load_migration_script( 185 | "odoo_module_migrate.migration_scripts.migrate_allways" 186 | ) 187 | ) 188 | if self._remove_migration_folder: 189 | self._migration_scripts.extend( 190 | self._load_migration_script( 191 | "odoo_module_migrate.migration_scripts." 192 | "migrate_remove_migration_folder" 193 | ) 194 | ) 195 | all_packages = importlib.import_module("odoo_module_migrate.migration_scripts") 196 | 197 | migration_start = float(self._migration_steps[0]["init_version_code"]) 198 | migration_end = float(self._migration_steps[-1]["target_version_code"]) 199 | 200 | for loader, name, is_pkg in pkgutil.walk_packages(all_packages.__path__): 201 | # Ignore script that will be allways executed. 202 | # this script will be added at the end. 203 | if name in ("migrate_allways", "migrate_remove_migration_folder"): 204 | continue 205 | 206 | # Filter migration scripts, depending of the configuration 207 | full_name = all_packages.__name__ + "." + name 208 | if "allways" in name: 209 | # replace allways by the most recent version 210 | real_name = name.replace("allways", _get_latest_version_code()) 211 | else: 212 | real_name = name 213 | splitted_name = real_name.split("_") 214 | 215 | script_start = float(splitted_name[1]) 216 | script_end = float(splitted_name[2]) 217 | 218 | # Exclude scripts 219 | if script_start >= migration_end or script_end <= migration_start: 220 | continue 221 | 222 | self._migration_scripts.extend(self._load_migration_script(full_name)) 223 | 224 | logger.debug( 225 | "The following migration script will be" 226 | " executed:\n- %s" 227 | % "\n- ".join( 228 | [ 229 | inspect.getfile(x.__class__).split("/")[-1] 230 | for x in self._migration_scripts 231 | ] 232 | ) 233 | ) 234 | 235 | def run(self): 236 | logger.debug( 237 | "Running migration from: %s to: %s in '%s'" 238 | % ( 239 | self._migration_steps[0]["init_version_name"], 240 | self._migration_steps[-1]["target_version_name"], 241 | self._directory_path.resolve(), 242 | ) 243 | ) 244 | for module_migration in self._module_migrations: 245 | module_migration.run() 246 | -------------------------------------------------------------------------------- /odoo_module_migrate/migration_scripts/migrate_160_170.py: -------------------------------------------------------------------------------- 1 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 2 | 3 | from odoo_module_migrate.base_migration_script import BaseMigrationScript 4 | import lxml.etree as et 5 | from pathlib import Path 6 | import sys 7 | import os 8 | import ast 9 | from typing import Any 10 | 11 | empty_list = ast.parse("[]").body[0].value 12 | 13 | 14 | class AbstractVisitor(ast.NodeVisitor): 15 | def __init__(self) -> None: 16 | # ((line, line_end, col_offset, end_col_offset), replace_by) NO OVERLAPS 17 | self.change_todo = [] 18 | 19 | def post_process(self, all_code: str, file: str) -> str: 20 | all_lines = all_code.split("\n") 21 | for (lineno, line_end, col_offset, end_col_offset), new_substring in sorted( 22 | self.change_todo, reverse=True 23 | ): 24 | if lineno == line_end: 25 | line = all_lines[lineno - 1] 26 | all_lines[lineno - 1] = ( 27 | line[:col_offset] + new_substring + line[end_col_offset:] 28 | ) 29 | else: 30 | print( 31 | f"Ignore replacement {file}: {(lineno, line_end, col_offset, end_col_offset), new_substring}" 32 | ) 33 | return "\n".join(all_lines) 34 | 35 | def add_change(self, old_node: ast.AST, new_node: ast.AST | str): 36 | position = ( 37 | old_node.lineno, 38 | old_node.end_lineno, 39 | old_node.col_offset, 40 | old_node.end_col_offset, 41 | ) 42 | if isinstance(new_node, str): 43 | self.change_todo.append((position, new_node)) 44 | else: 45 | self.change_todo.append((position, ast.unparse(new_node))) 46 | 47 | 48 | class VisitorToPrivateReadGroup(AbstractVisitor): 49 | def post_process(self, all_code: str, file: str) -> str: 50 | all_lines = all_code.split("\n") 51 | for i, line in enumerate(all_lines): 52 | if "super(" not in line: 53 | all_lines[i] = line.replace(".read_group(", "._read_group(") 54 | return "\n".join(all_lines) 55 | 56 | 57 | class VisitorInverseGroupbyFields(AbstractVisitor): 58 | def visit_Call(self, node: ast.Call) -> Any: 59 | if isinstance(node.func, ast.Attribute) and node.func.attr == "_read_group": 60 | # Should have the same number of args/keywords 61 | # Inverse fields/groupby order 62 | keywords_by_key = {keyword.arg: keyword.value for keyword in node.keywords} 63 | key_i_by_key = {keyword.arg: i for i, keyword in enumerate(node.keywords)} 64 | if len(node.args) >= 3: 65 | self.add_change(node.args[2], node.args[1]) 66 | self.add_change(node.args[1], node.args[2]) 67 | elif len(node.args) == 2: 68 | new_args_value = keywords_by_key.get("groupby", empty_list) 69 | if "groupby" in keywords_by_key: 70 | fields_args = ast.keyword("fields", node.args[1]) 71 | self.add_change(node.args[1], new_args_value) 72 | self.add_change(node.keywords[key_i_by_key["groupby"]], fields_args) 73 | else: 74 | self.add_change( 75 | node.args[1], 76 | f"{ast.unparse(new_args_value)}, {ast.unparse(node.args[1])}", 77 | ) 78 | else: # len(node.args) <= 2 79 | if ( 80 | "groupby" in key_i_by_key 81 | and "fields" in key_i_by_key 82 | and key_i_by_key["groupby"] > key_i_by_key["fields"] 83 | ): 84 | self.add_change( 85 | node.keywords[key_i_by_key["groupby"]], 86 | node.keywords[key_i_by_key["fields"]], 87 | ) 88 | self.add_change( 89 | node.keywords[key_i_by_key["fields"]], 90 | node.keywords[key_i_by_key["groupby"]], 91 | ) 92 | else: 93 | raise ValueError(f"{key_i_by_key}, {keywords_by_key}, {node.args}") 94 | self.generic_visit(node) 95 | 96 | 97 | class VisitorRenameKeywords(AbstractVisitor): 98 | def visit_Call(self, node: ast.Call) -> Any: 99 | if isinstance(node.func, ast.Attribute) and node.func.attr == "_read_group": 100 | # Replace fields by aggregate and orderby by order 101 | for keyword in node.keywords: 102 | if keyword.arg == "fields": 103 | new_keyword = ast.keyword("aggregates", keyword.value) 104 | self.add_change(keyword, new_keyword) 105 | if keyword.arg == "orderby": 106 | new_keyword = ast.keyword("order", keyword.value) 107 | self.add_change(keyword, new_keyword) 108 | self.generic_visit(node) 109 | 110 | 111 | class VisitorRemoveLazy(AbstractVisitor): 112 | def post_process(self, all_code: str, file: str) -> str: 113 | # remove extra comma ',' and extra line if possible 114 | all_code = super().post_process(all_code, file) 115 | all_lines = all_code.split("\n") 116 | for (lineno, __, col_offset, __), __ in sorted(self.change_todo, reverse=True): 117 | comma_find = False 118 | line = all_lines[lineno - 1] 119 | remaining = line[col_offset:] 120 | line = line[:col_offset] 121 | while not comma_find: 122 | if "," not in line: 123 | all_lines.pop(lineno - 1) 124 | lineno -= 1 125 | line = all_lines[lineno - 1] 126 | else: 127 | comma_find = True 128 | last_index_comma = -(line[::-1].index(",") + 1) 129 | all_lines[lineno - 1] = line[:last_index_comma] + remaining 130 | 131 | return "\n".join(all_lines) 132 | 133 | def visit_Call(self, node: ast.Call) -> Any: 134 | if isinstance(node.func, ast.Attribute) and node.func.attr == "_read_group": 135 | # Replace fields by aggregate and orderby by order 136 | if len(node.args) == 7: 137 | self.add_change(node.args[6], "") 138 | else: 139 | for keyword in node.keywords: 140 | if keyword.arg == "lazy": 141 | self.add_change(keyword, "") 142 | self.generic_visit(node) 143 | 144 | 145 | class VisitorAggregatesSpec(AbstractVisitor): 146 | def visit_Call(self, node: ast.Call) -> Any: 147 | if isinstance(node.func, ast.Attribute) and node.func.attr == "_read_group": 148 | 149 | keywords_by_key = {keyword.arg: keyword.value for keyword in node.keywords} 150 | aggregate_values = None 151 | if len(node.args) >= 3: 152 | aggregate_values = node.args[2] 153 | elif "aggregates" in keywords_by_key: 154 | aggregate_values = keywords_by_key["aggregates"] 155 | 156 | groupby_values = empty_list 157 | if len(node.args) >= 2: 158 | groupby_values = node.args[1] 159 | elif "groupby" in keywords_by_key: 160 | groupby_values = keywords_by_key["groupby"] 161 | 162 | if aggregate_values: 163 | aggregates = None 164 | try: 165 | aggregates = ast.literal_eval(ast.unparse(aggregate_values)) 166 | if not isinstance(aggregates, (list, tuple)): 167 | raise ValueError( 168 | f"{aggregate_values} is not a list but literal ?" 169 | ) 170 | 171 | aggregates = [ 172 | f"{field_spec.split('(')[1][:-1]}:{field_spec.split(':')[1].split('(')[0]}" 173 | if "(" in field_spec 174 | else field_spec 175 | for field_spec in aggregates 176 | ] 177 | aggregates = [ 178 | "__count" 179 | if field_spec in ("id:count", "id:count_distinct") 180 | else field_spec 181 | for field_spec in aggregates 182 | ] 183 | 184 | groupby = ast.literal_eval(ast.unparse(groupby_values)) 185 | if isinstance(groupby, str): 186 | groupby = [groupby] 187 | 188 | aggregates = [ 189 | f"{field}:sum" 190 | if (":" not in field and field != "__count") 191 | else field 192 | for field in aggregates 193 | if field not in groupby 194 | ] 195 | if not aggregates: 196 | aggregates = ["__count"] 197 | except SyntaxError: 198 | pass 199 | except ValueError: 200 | pass 201 | 202 | if aggregates is not None: 203 | self.add_change(aggregate_values, repr(aggregates)) 204 | self.generic_visit(node) 205 | 206 | 207 | Steps_visitor: list[AbstractVisitor] = [ 208 | VisitorToPrivateReadGroup, 209 | VisitorInverseGroupbyFields, 210 | VisitorRenameKeywords, 211 | VisitorAggregatesSpec, 212 | VisitorRemoveLazy, 213 | ] 214 | 215 | 216 | def replace_read_group_signature(logger, filename): 217 | with open(filename, mode="rt") as file: 218 | new_all = all_code = file.read() 219 | if ".read_group(" in all_code or "._read_group(" in all_code: 220 | for Step in Steps_visitor: 221 | visitor = Step() 222 | try: 223 | visitor.visit(ast.parse(new_all)) 224 | except Exception: 225 | logger.info( 226 | f"ERROR in {filename} at step {visitor.__class__}: \n{new_all}" 227 | ) 228 | raise 229 | new_all = visitor.post_process(new_all, filename) 230 | if new_all == all_code: 231 | logger.info("read_group detected but not changed in file %s" % filename) 232 | 233 | if new_all != all_code: 234 | logger.info("Script read_group replace applied in file %s" % filename) 235 | with open(filename, mode="wt") as file: 236 | file.write(new_all) 237 | 238 | 239 | def _get_files(module_path, reformat_file_ext): 240 | """Get files to be reformatted.""" 241 | file_paths = list() 242 | if not module_path.is_dir(): 243 | raise Exception(f"'{module_path}' is not a directory") 244 | file_paths.extend(module_path.rglob("*" + reformat_file_ext)) 245 | return file_paths 246 | 247 | 248 | def _check_open_form_view(logger, file_path: Path): 249 | """Check if the view has a button to open a form reg in a tree view `file_path`.""" 250 | parser = et.XMLParser(remove_blank_text=True) 251 | tree = et.parse(str(file_path.resolve()), parser) 252 | record_node = tree.getroot()[0] 253 | f_arch = record_node.find('field[@name="arch"]') 254 | root = f_arch if f_arch is not None else record_node 255 | for button in root.findall(".//button[@name='get_formview_action']"): 256 | logger.warning( 257 | ( 258 | "Button to open a form reg form a tree view detected in file %s line %s, probably should be changed by open_form_view='True'. More info here https://github.com/odoo/odoo/commit/258e6a019a21042bf4f6cf70fcce386d37afd50c" 259 | ) 260 | % (file_path.name, button.sourceline) 261 | ) 262 | 263 | 264 | def _check_open_form( 265 | logger, module_path, module_name, manifest_path, migration_steps, tools 266 | ): 267 | reformat_file_ext = ".xml" 268 | file_paths = _get_files(module_path, reformat_file_ext) 269 | logger.debug(f"{reformat_file_ext} files found:\n" f"{list(map(str, file_paths))}") 270 | 271 | for file_path in file_paths: 272 | _check_open_form_view(logger, file_path) 273 | 274 | 275 | def _reformat_read_group( 276 | logger, module_path, module_name, manifest_path, migration_steps, tools 277 | ): 278 | """Reformat read_group method in py files.""" 279 | 280 | reformat_file_ext = ".py" 281 | file_paths = _get_files(module_path, reformat_file_ext) 282 | logger.debug(f"{reformat_file_ext} files found:\n" f"{list(map(str, file_paths))}") 283 | 284 | reformatted_files = list() 285 | for file_path in file_paths: 286 | reformatted_file = replace_read_group_signature(logger, file_path) 287 | if reformatted_file: 288 | reformatted_files.append(reformatted_file) 289 | logger.debug("Reformatted files:\n" f"{list(reformatted_files)}") 290 | 291 | 292 | class MigrationScript(BaseMigrationScript): 293 | 294 | _GLOBAL_FUNCTIONS = [_check_open_form, _reformat_read_group] 295 | -------------------------------------------------------------------------------- /odoo_module_migrate/base_migration_script.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .config import _ALLOWED_EXTENSIONS 3 | from .tools import _execute_shell 4 | from .log import logger 5 | from . import tools 6 | import re 7 | import pathlib 8 | import traceback 9 | import inspect 10 | import glob 11 | import yaml 12 | import importlib 13 | 14 | 15 | class BaseMigrationScript(object): 16 | _TEXT_REPLACES = {} 17 | _TEXT_ERRORS = {} 18 | _TEXT_WARNINGS = {} 19 | _DEPRECATED_MODULES = [] 20 | _FILE_RENAMES = {} 21 | _REMOVED_FIELDS = [] 22 | _RENAMED_FIELDS = [] 23 | _RENAMED_MODELS = [] 24 | _REMOVED_MODELS = [] 25 | _GLOBAL_FUNCTIONS = [] # [function_object] 26 | _module_path = "" 27 | 28 | def parse_rules(self): 29 | script_parts = inspect.getfile(self.__class__).split("/") 30 | migrate_from_to = script_parts[-1].split(".")[0] 31 | migration_scripts_dir = "/".join(script_parts[:-1]) 32 | 33 | TYPE_ARRAY = "TYPE_ARRAY" 34 | TYPE_DICT = "TYPE_DICT" 35 | TYPE_DICT_OF_DICT = "TYPE_DICT_OF_DICT" 36 | rules = { 37 | # {filetype: {regex: replacement}} 38 | "_TEXT_REPLACES": { 39 | "type": TYPE_DICT_OF_DICT, 40 | "doc": {}, 41 | }, 42 | # {filetype: {regex: message}} 43 | "_TEXT_ERRORS": { 44 | "type": TYPE_DICT_OF_DICT, 45 | "doc": {}, 46 | }, 47 | # {filetype: {regex: message}} 48 | "_TEXT_WARNINGS": { 49 | "type": TYPE_DICT_OF_DICT, 50 | "doc": {}, 51 | }, 52 | # [(module, why, ...)] 53 | "_DEPRECATED_MODULES": { 54 | "type": TYPE_ARRAY, 55 | "doc": [], 56 | }, 57 | # {old_name: new_name} 58 | "_FILE_RENAMES": { 59 | "type": TYPE_DICT, 60 | "doc": {}, 61 | }, 62 | # [(model_name, field_name, more_info), ...)] 63 | "_REMOVED_FIELDS": { 64 | "type": TYPE_ARRAY, 65 | "doc": [], 66 | }, 67 | # [(model_name, old_field_name, new_field_name, more_info), ...)] 68 | "_RENAMED_FIELDS": { 69 | "type": TYPE_ARRAY, 70 | "doc": [], 71 | }, 72 | # [(old.model.name, new.model.name, more_info)] 73 | "_RENAMED_MODELS": { 74 | "type": TYPE_ARRAY, 75 | "doc": [], 76 | }, 77 | # [(old.model.name, more_info)] 78 | "_REMOVED_MODELS": { 79 | "type": TYPE_ARRAY, 80 | "doc": [], 81 | }, 82 | } 83 | # read 84 | for rule in rules.keys(): 85 | rule_folder = rule[1:].lower() 86 | file_pattern = "%s/%s/%s/*.yaml" % ( 87 | migration_scripts_dir, 88 | rule_folder, 89 | migrate_from_to, 90 | ) 91 | for filename in glob.glob(file_pattern): 92 | with open(filename) as f: 93 | new_rules = yaml.safe_load(f) 94 | if rules[rule]["type"] == TYPE_DICT_OF_DICT: 95 | for f_type, data in new_rules.items(): 96 | if f_type not in rules[rule]["doc"]: 97 | rules[rule]["doc"][f_type] = {} 98 | rules[rule]["doc"][f_type].update(data) 99 | elif rules[rule]["type"] == TYPE_DICT: 100 | rules[rule]["doc"].update(new_rules) 101 | elif rules[rule]["type"] == TYPE_ARRAY: 102 | rules[rule]["doc"].extend(new_rules) 103 | # extend 104 | for rule, data in rules.items(): 105 | rtype = data["type"] 106 | doc = data.get("doc") 107 | if not doc: 108 | continue 109 | 110 | rvalues = getattr(self, rule) 111 | if rtype == TYPE_ARRAY: 112 | rvalues.extend(doc) 113 | elif rtype == TYPE_DICT: 114 | rvalues.update(doc) 115 | else: 116 | # TYPE_DICT_OF_DICT 117 | for filetype, values in doc.items(): 118 | rvalues.setdefault(filetype, {}) 119 | rvalues[filetype].update(values or {}) 120 | 121 | file_pattern = "%s/python_scripts/%s/*.py" % ( 122 | migration_scripts_dir, 123 | migrate_from_to, 124 | ) 125 | for path in glob.glob(file_pattern): 126 | module_name = path.split("/")[-1].split(".")[0] 127 | module_name = ".".join( 128 | [ 129 | "odoo_module_migrate.migration_scripts.python_scripts", 130 | migrate_from_to, 131 | module_name, 132 | ] 133 | ) 134 | module = importlib.import_module(module_name) 135 | for name, value in inspect.getmembers(module, inspect.isfunction): 136 | if not name.startswith("_"): 137 | self._GLOBAL_FUNCTIONS.append(value) 138 | 139 | def run( 140 | self, 141 | module_path, 142 | manifest_path, 143 | module_name, 144 | migration_steps, 145 | directory_path, 146 | commit_enabled, 147 | ): 148 | logger.debug( 149 | "Running %s script" % inspect.getfile(self.__class__).split("/")[-1] 150 | ) 151 | self.parse_rules() 152 | manifest_path = self._get_correct_manifest_path( 153 | manifest_path, self._FILE_RENAMES 154 | ) 155 | for root, directories, filenames in os.walk(module_path.resolve()): 156 | for filename in filenames: 157 | extension = os.path.splitext(filename)[1] 158 | if extension not in _ALLOWED_EXTENSIONS: 159 | continue 160 | self.process_file( 161 | root, 162 | filename, 163 | extension, 164 | self._FILE_RENAMES, 165 | directory_path, 166 | commit_enabled, 167 | ) 168 | 169 | self.handle_deprecated_modules(manifest_path, self._DEPRECATED_MODULES) 170 | 171 | if self._GLOBAL_FUNCTIONS: 172 | for function in self._GLOBAL_FUNCTIONS: 173 | function( 174 | logger=logger, 175 | module_path=module_path, 176 | module_name=module_name, 177 | manifest_path=manifest_path, 178 | migration_steps=migration_steps, 179 | tools=tools, 180 | ) 181 | 182 | def process_file( 183 | self, root, filename, extension, file_renames, directory_path, commit_enabled 184 | ): 185 | # Skip useless file 186 | # TODO, skip files present in some folders. (for exemple 'lib') 187 | absolute_file_path = os.path.join(root, filename) 188 | logger.debug("Migrate '%s' file" % absolute_file_path) 189 | 190 | # Rename file, if required 191 | new_name = file_renames.get(filename) 192 | if new_name: 193 | self._rename_file( 194 | directory_path, 195 | absolute_file_path, 196 | os.path.join(root, new_name), 197 | commit_enabled, 198 | ) 199 | absolute_file_path = os.path.join(root, new_name) 200 | 201 | removed_fields = self.handle_removed_fields(self._REMOVED_FIELDS) 202 | renamed_fields = self.handle_renamed_fields(self._RENAMED_FIELDS) 203 | renamed_models = self.handle_renamed_models(self._RENAMED_MODELS) 204 | removed_models = self.handle_removed_models(self._REMOVED_MODELS) 205 | 206 | # Operate changes in the file (replacements, removals) 207 | replaces = self._TEXT_REPLACES.get("*", {}) 208 | replaces.update(self._TEXT_REPLACES.get(extension, {})) 209 | replaces.update(renamed_models.get("replaces")) 210 | replaces.update(removed_models.get("replaces")) 211 | 212 | new_text = tools._replace_in_file( 213 | absolute_file_path, replaces, "Change file content of %s" % filename 214 | ) 215 | 216 | # Display errors if the new content contains some obsolete 217 | # pattern 218 | errors = self._TEXT_ERRORS.get("*", {}) 219 | errors.update(self._TEXT_ERRORS.get(extension, {})) 220 | errors.update(renamed_models.get("errors")) 221 | errors.update(removed_models.get("errors")) 222 | for pattern, error_message in errors.items(): 223 | if re.findall(pattern, new_text): 224 | logger.error(error_message + "\nFile " + os.path.join(root, filename)) 225 | 226 | warnings = self._TEXT_WARNINGS.get("*", {}) 227 | warnings.update(self._TEXT_WARNINGS.get(extension, {})) 228 | warnings.update(removed_fields.get("warnings")) 229 | warnings.update(renamed_fields.get("warnings")) 230 | warnings.update(renamed_models.get("warnings")) 231 | warnings.update(removed_models.get("warnings")) 232 | for pattern, warning_message in warnings.items(): 233 | if re.findall(pattern, new_text): 234 | logger.warning(warning_message + ". File " + root + os.sep + filename) 235 | 236 | def handle_removed_fields(self, removed_fields): 237 | """Give warnings if field_name is found on the code. To minimize two 238 | many false positives we search for field name on this situations: 239 | * with simple/double quotes 240 | * prefixed with dot and with space, comma or equal after the string 241 | For now this handler is simple but the idea would be to improve it 242 | with deeper analysis and direct replaces if it is possible and secure. 243 | For that analysis model_name could be used 244 | """ 245 | res = {} 246 | for model_name, field_name, more_info in removed_fields: 247 | msg = "On the model %s, the field %s was deprecated.%s" % ( 248 | model_name, 249 | field_name, 250 | " %s" % more_info if more_info else "", 251 | ) 252 | res[r"""(['"]{0}['"]|\.{0}[\s,=])""".format(field_name)] = msg 253 | return {"warnings": res} 254 | 255 | def handle_renamed_fields(self, removed_fields): 256 | """Give warnings if old_field_name is found on the code. To minimize 257 | two many false positives we search for field name on this situations: 258 | * with simple/double quotes 259 | * prefixed with dot and with space, comma or equal after the string 260 | For now this handler is simple but the idea would be to improve it 261 | with deeper analysis and direct replaces if it is possible and secure. 262 | For that analysis model_name could be used 263 | """ 264 | res = {} 265 | for model_name, old_field_name, new_field_name, more_info in removed_fields: 266 | msg = "On the model %s, the field %s was renamed to %s.%s" % ( 267 | model_name, 268 | old_field_name, 269 | new_field_name, 270 | " %s" % more_info if more_info else "", 271 | ) 272 | res[r"""(['"]{0}['"]|\.{0}[\s,=])""".format(old_field_name)] = msg 273 | return {"warnings": res} 274 | 275 | def handle_deprecated_modules(self, manifest_path, deprecated_modules): 276 | current_manifest_text = tools._read_content(manifest_path) 277 | new_manifest_text = current_manifest_text 278 | for items in deprecated_modules: 279 | old_module, action = items[0:2] 280 | new_module = len(items) > 2 and items[2] 281 | old_module_pattern = r"('|\"){0}('|\")".format(old_module) 282 | if new_module: 283 | new_module_pattern = r"('|\"){0}('|\")".format(new_module) 284 | replace_pattern = r"\1{0}\2".format(new_module) 285 | 286 | if not re.findall(old_module_pattern, new_manifest_text): 287 | continue 288 | 289 | if action == "removed": 290 | # The module has been removed, just log an error. 291 | logger.error("Depends on removed module '%s'" % (old_module)) 292 | 293 | elif action == "renamed": 294 | new_manifest_text = re.sub( 295 | old_module_pattern, replace_pattern, new_manifest_text 296 | ) 297 | logger.info( 298 | "Replaced dependency of '%s' by '%s'." % (old_module, new_module) 299 | ) 300 | 301 | elif action == "oca_moved": 302 | new_manifest_text = re.sub( 303 | old_module_pattern, replace_pattern, new_manifest_text 304 | ) 305 | logger.warning( 306 | "Replaced dependency of '%s' by '%s' (%s)\n" 307 | "Check that '%s' is available on your system." 308 | % (old_module, new_module, items[3], new_module) 309 | ) 310 | 311 | elif action == "merged": 312 | if not re.findall(new_module_pattern, new_manifest_text): 313 | # adding dependency of the merged module 314 | new_manifest_text = re.sub( 315 | old_module_pattern, replace_pattern, new_manifest_text 316 | ) 317 | logger.info( 318 | "'%s' merged in '%s'. Replacing dependency." 319 | % (old_module, new_module) 320 | ) 321 | else: 322 | # TODO, improve me. we should remove the dependency 323 | # but it could generate coma trouble. 324 | # maybe handling this treatment by ast lib could fix 325 | # the problem. 326 | logger.error( 327 | "'%s' merged in '%s'. You should remove the" 328 | " dependency to '%s' manually." 329 | % (old_module, new_module, old_module) 330 | ) 331 | if current_manifest_text != new_manifest_text: 332 | tools._write_content(manifest_path, new_manifest_text) 333 | 334 | def handle_renamed_models(self, renamed_models): 335 | """renamed_models = [(old.model, new.model, msg)] 336 | returns dictionary of all replaces / warnings / errors produced 337 | by a model renamed 338 | { 339 | 'replaces': 340 | { 341 | "old_model_name", 'old_model_name': new_model_name 342 | old_table_name["',]: new_table_name["',] 343 | }, 344 | 'warnings': 345 | { 346 | old.model.name: warning msg 347 | old_model_name: warning msg 348 | } 349 | } 350 | """ 351 | res = {"replaces": {}, "warnings": {}, "errors": {}} 352 | for old_model_name, new_model_name, more_info in renamed_models: 353 | old_table_name = old_model_name.replace(".", "_") 354 | new_table_name = new_model_name.replace(".", "_") 355 | old_name_esc = re.escape(old_model_name) 356 | res["replaces"].update( 357 | { 358 | r"\"%s\"" % old_name_esc: '"%s"' % new_model_name, 359 | r"\'%s\'" % old_name_esc: "'%s'" % new_model_name, 360 | r"\"%s\"" % old_table_name: '"%s"' % new_table_name, 361 | r"\'%s\'" % old_table_name: "'%s'" % new_table_name, 362 | r"model_%s\"" % old_table_name: 'model_%s"' % new_table_name, 363 | r"model_%s\'" % old_table_name: "model_%s'" % new_table_name, 364 | r"model_%s," % old_table_name: "model_%s," % new_table_name, 365 | } 366 | ) 367 | msg = "The model %s has been renamed to %s.%s" % ( 368 | old_model_name, 369 | new_model_name, 370 | (" %s" % more_info) or "", 371 | ) 372 | res["warnings"].update( 373 | { 374 | old_name_esc: msg, 375 | old_table_name: msg, 376 | } 377 | ) 378 | return res 379 | 380 | def handle_removed_models(self, removed_models): 381 | """removed_models = [(old.model, msg)] 382 | returns dictionary of all replaces / warnings / errors produced 383 | by a model renamed 384 | { 385 | 'error': 386 | { 387 | "old_model_name", 'old_model_name': new_model_name 388 | old_table_name["',]: new_table_name["',] 389 | }, 390 | 'warnings': 391 | { 392 | old.model.name: warning msg 393 | old_model_name: warning msg 394 | } 395 | } 396 | """ 397 | res = {"replaces": {}, "warnings": {}, "errors": {}} 398 | for model_name, more_info in removed_models: 399 | table_name = model_name.replace(".", "_") 400 | model_name_esc = re.escape(model_name) 401 | 402 | msg = "The model %s has been deprecated.%s" % ( 403 | model_name, 404 | (" %s" % more_info) or "", 405 | ) 406 | 407 | res["errors"].update( 408 | { 409 | r"\"%s\"" % model_name_esc: msg, 410 | r"\'%s\'" % model_name_esc: msg, 411 | r"\"%s\"" % table_name: msg, 412 | r"\'%s\'" % table_name: msg, 413 | r"model_%s\"" % table_name: msg, 414 | r"model_%s\'" % table_name: msg, 415 | r"model_%s," % table_name: msg, 416 | } 417 | ) 418 | res["warnings"].update( 419 | { 420 | model_name_esc: msg, 421 | table_name: msg, 422 | } 423 | ) 424 | return res 425 | 426 | def _get_correct_manifest_path(self, manifest_path, file_renames): 427 | current_manifest_file_name = manifest_path.as_posix().split("/")[-1] 428 | if current_manifest_file_name in file_renames: 429 | new_manifest_file_name = manifest_path.as_posix().replace( 430 | current_manifest_file_name, file_renames[current_manifest_file_name] 431 | ) 432 | manifest_path = pathlib.Path(new_manifest_file_name) 433 | return manifest_path 434 | 435 | def _rename_file(self, module_path, old_file_path, new_file_path, commit_enabled): 436 | """ 437 | Rename a file. try to execute 'git mv', to avoid huge diff. 438 | 439 | if 'git mv' fails, make a classical rename 440 | """ 441 | logger.info( 442 | "Renaming file: '%s' by '%s' " 443 | % ( 444 | old_file_path.replace(str(module_path.resolve()), ""), 445 | new_file_path.replace(str(module_path.resolve()), ""), 446 | ) 447 | ) 448 | try: 449 | if commit_enabled: 450 | _execute_shell( 451 | "git mv %s %s" % (old_file_path, new_file_path), path=module_path 452 | ) 453 | else: 454 | _execute_shell( 455 | "mv %s %s" % (old_file_path, new_file_path), path=module_path 456 | ) 457 | except BaseException: 458 | logger.error(traceback.format_exc()) 459 | --------------------------------------------------------------------------------