[IMP] module_auto_update: black, isort, prettier

pull/1832/head
Eric Antones 2020-05-18 13:12:59 +02:00
parent e93659fb03
commit 8592e0e285
10 changed files with 184 additions and 199 deletions

View File

@ -3,24 +3,20 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
'name': 'Module Auto Update',
'summary': 'Automatically update Odoo modules',
'version': '12.0.2.0.5',
'category': 'Extra Tools',
'website': 'https://github.com/OCA/server-tools',
'author': 'LasLabs, '
'Juan José Scarafía, '
'Tecnativa, '
'ACSONE SA/NV, '
'Odoo Community Association (OCA)',
'license': 'LGPL-3',
'installable': True,
'uninstall_hook': 'uninstall_hook',
'depends': [
'base',
],
'data': [
'views/ir_module_module.xml',
],
'development_status': 'Production/Stable',
"name": "Module Auto Update",
"summary": "Automatically update Odoo modules",
"version": "12.0.2.0.5",
"category": "Extra Tools",
"website": "https://github.com/OCA/server-tools",
"author": "LasLabs, "
"Juan José Scarafía, "
"Tecnativa, "
"ACSONE SA/NV, "
"Odoo Community Association (OCA)",
"license": "LGPL-3",
"installable": True,
"uninstall_hook": "uninstall_hook",
"depends": ["base"],
"data": ["views/ir_module_module.xml"],
"development_status": "Production/Stable",
}

View File

@ -1,9 +1,9 @@
# Copyright 2018 ACSONE SA/NV.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from fnmatch import fnmatch
import hashlib
import os
from fnmatch import fnmatch
def _fnmatch(filename, patterns):
@ -14,20 +14,20 @@ def _fnmatch(filename, patterns):
def _walk(top, exclude_patterns, keep_langs):
keep_langs = {l.split('_')[0] for l in keep_langs}
keep_langs = {l.split("_")[0] for l in keep_langs}
for dirpath, dirnames, filenames in os.walk(top):
dirnames.sort()
reldir = os.path.relpath(dirpath, top)
if reldir == '.':
reldir = ''
if reldir == ".":
reldir = ""
for filename in sorted(filenames):
filepath = os.path.join(reldir, filename)
if _fnmatch(filepath, exclude_patterns):
continue
if keep_langs and reldir in {'i18n', 'i18n_extra'}:
if keep_langs and reldir in {"i18n", "i18n_extra"}:
basename, ext = os.path.splitext(filename)
if ext == '.po':
if basename.split('_')[0] not in keep_langs:
if ext == ".po":
if basename.split("_")[0] not in keep_langs:
continue
yield filepath
@ -37,8 +37,8 @@ def addon_hash(top, exclude_patterns, keep_langs):
m = hashlib.sha1()
for filepath in _walk(top, exclude_patterns, keep_langs):
# hash filename so empty files influence the hash
m.update(filepath.encode('utf-8'))
m.update(filepath.encode("utf-8"))
# hash file content
with open(os.path.join(top, filepath), 'rb') as f:
with open(os.path.join(top, filepath), "rb") as f:
m.update(f.read())
return m.hexdigest()

View File

@ -6,7 +6,5 @@ from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
openupgrade.delete_records_safely_by_xml_id(
env, [
'module_auto_update.module_check_upgrades_cron',
],
env, ["module_auto_update.module_check_upgrades_cron"],
)

View File

@ -11,12 +11,9 @@ from odoo.modules.module import get_module_path
from ..addon_hash import addon_hash
PARAM_INSTALLED_CHECKSUMS = \
'module_auto_update.installed_checksums'
PARAM_EXCLUDE_PATTERNS = \
'module_auto_update.exclude_patterns'
DEFAULT_EXCLUDE_PATTERNS = \
'*.pyc,*.pyo,i18n/*.pot,i18n_extra/*.pot,static/*'
PARAM_INSTALLED_CHECKSUMS = "module_auto_update.installed_checksums"
PARAM_EXCLUDE_PATTERNS = "module_auto_update.exclude_patterns"
DEFAULT_EXCLUDE_PATTERNS = "*.pyc,*.pyo,i18n/*.pot,i18n_extra/*.pot,static/*"
_logger = logging.getLogger(__name__)
@ -34,40 +31,33 @@ def ensure_module_state(env, modules, state):
if not modules:
return
env.cr.execute(
"SELECT name FROM ir_module_module "
"WHERE id IN %s AND state != %s",
"SELECT name FROM ir_module_module " "WHERE id IN %s AND state != %s",
(tuple(modules.ids), state),
)
names = [r[0] for r in env.cr.fetchall()]
if names:
raise FailedUpgradeError(
"The following modules should be in state '%s' "
"at this stage: %s. Bailing out for safety." %
(state, ','.join(names), ),
"at this stage: %s. Bailing out for safety." % (state, ",".join(names),),
)
class Module(models.Model):
_inherit = 'ir.module.module'
_inherit = "ir.module.module"
@api.multi
def _get_checksum_dir(self):
self.ensure_one()
exclude_patterns = self.env["ir.config_parameter"].get_param(
PARAM_EXCLUDE_PATTERNS,
DEFAULT_EXCLUDE_PATTERNS,
PARAM_EXCLUDE_PATTERNS, DEFAULT_EXCLUDE_PATTERNS,
)
exclude_patterns = [p.strip() for p in exclude_patterns.split(',')]
keep_langs = self.env['res.lang'].search([]).mapped('code')
exclude_patterns = [p.strip() for p in exclude_patterns.split(",")]
keep_langs = self.env["res.lang"].search([]).mapped("code")
module_path = get_module_path(self.name)
if module_path and os.path.isdir(module_path):
checksum_dir = addon_hash(
module_path,
exclude_patterns,
keep_langs,
)
checksum_dir = addon_hash(module_path, exclude_patterns, keep_langs,)
else:
checksum_dir = False
@ -75,32 +65,30 @@ class Module(models.Model):
@api.model
def _get_saved_checksums(self):
Icp = self.env['ir.config_parameter']
return json.loads(Icp.get_param(PARAM_INSTALLED_CHECKSUMS, '{}'))
Icp = self.env["ir.config_parameter"]
return json.loads(Icp.get_param(PARAM_INSTALLED_CHECKSUMS, "{}"))
@api.model
def _save_checksums(self, checksums):
Icp = self.env['ir.config_parameter']
Icp = self.env["ir.config_parameter"]
Icp.set_param(PARAM_INSTALLED_CHECKSUMS, json.dumps(checksums))
@api.model
def _save_installed_checksums(self):
checksums = {}
installed_modules = self.search([('state', '=', 'installed')])
installed_modules = self.search([("state", "=", "installed")])
for module in installed_modules:
checksums[module.name] = module._get_checksum_dir()
self._save_checksums(checksums)
@api.model
def _get_modules_partially_installed(self):
return self.search([
('state', 'in', ('to install', 'to remove', 'to upgrade')),
])
return self.search([("state", "in", ("to install", "to remove", "to upgrade"))])
@api.model
def _get_modules_with_changed_checksum(self):
saved_checksums = self._get_saved_checksums()
installed_modules = self.search([('state', '=', 'installed')])
installed_modules = self.search([("state", "=", "installed")])
return installed_modules.filtered(
lambda r: r._get_checksum_dir() != saved_checksums.get(r.name),
)
@ -131,32 +119,37 @@ class Module(models.Model):
"""
_logger.info(
"Checksum upgrade starting (i18n-overwrite=%s)...",
overwrite_existing_translations
overwrite_existing_translations,
)
tools.config['overwrite_existing_translations'] = \
overwrite_existing_translations
tools.config[
"overwrite_existing_translations"
] = overwrite_existing_translations
_logger.info("Updating modules list...")
self.update_list()
changed_modules = self._get_modules_with_changed_checksum()
if not changed_modules and not self._get_modules_partially_installed():
_logger.info("No checksum change detected in installed modules "
"and all modules installed, nothing to do.")
_logger.info(
"No checksum change detected in installed modules "
"and all modules installed, nothing to do."
)
return
_logger.info("Marking the following modules to upgrade, "
"for their checksums changed: %s...",
','.join(changed_modules.mapped('name')))
_logger.info(
"Marking the following modules to upgrade, "
"for their checksums changed: %s...",
",".join(changed_modules.mapped("name")),
)
changed_modules.button_upgrade()
self.env.cr.commit() # pylint: disable=invalid-commit
# in rare situations, button_upgrade may fail without
# exception, this would lead to corruption because
# no upgrade would be performed and save_installed_checksums
# would update cheksums for modules that have not been upgraded
ensure_module_state(self.env, changed_modules, 'to upgrade')
ensure_module_state(self.env, changed_modules, "to upgrade")
_logger.info("Upgrading...")
self.env['base.module.upgrade'].upgrade_module()
self.env["base.module.upgrade"].upgrade_module()
self.env.cr.commit() # pylint: disable=invalid-commit
_logger.info("Upgrade successful, updating checksums...")
@ -167,8 +160,8 @@ class Module(models.Model):
if partial_modules:
raise IncompleteUpgradeError(
"Checksum upgrade successful "
"but incomplete for the following modules: %s" %
','.join(partial_modules.mapped('name'))
"but incomplete for the following modules: %s"
% ",".join(partial_modules.mapped("name"))
)
_logger.info("Checksum upgrade complete.")

View File

@ -1 +1 @@
<odoo/>
<odoo />

View File

@ -1 +1 @@
<odoo/>
<odoo />

View File

@ -1 +1 @@
1+1
_ = 1 + 1

View File

@ -9,59 +9,63 @@ from ..models.module import DEFAULT_EXCLUDE_PATTERNS
class TestAddonHash(unittest.TestCase):
def setUp(self):
super(TestAddonHash, self).setUp()
self.sample_dir = os.path.join(
os.path.dirname(__file__),
'sample_module',
)
self.sample_dir = os.path.join(os.path.dirname(__file__), "sample_module",)
def test_basic(self):
files = list(addon_hash._walk(
self.sample_dir,
exclude_patterns=["*/__pycache__/*"],
keep_langs=[],
))
self.assertEqual(files, [
'README.rst',
'data/f1.xml',
'data/f2.xml',
'i18n/en.po',
'i18n/en_US.po',
'i18n/fr.po',
'i18n/fr_BE.po',
'i18n/test.pot',
'i18n_extra/en.po',
'i18n_extra/fr.po',
'i18n_extra/nl_NL.po',
'models/stuff.py',
'models/stuff.pyc',
'models/stuff.pyo',
'static/src/some.js',
])
files = list(
addon_hash._walk(
self.sample_dir, exclude_patterns=["*/__pycache__/*"], keep_langs=[],
)
)
self.assertEqual(
files,
[
"README.rst",
"data/f1.xml",
"data/f2.xml",
"i18n/en.po",
"i18n/en_US.po",
"i18n/fr.po",
"i18n/fr_BE.po",
"i18n/test.pot",
"i18n_extra/en.po",
"i18n_extra/fr.po",
"i18n_extra/nl_NL.po",
"models/stuff.py",
"models/stuff.pyc",
"models/stuff.pyo",
"static/src/some.js",
],
)
def test_exclude(self):
files = list(addon_hash._walk(
self.sample_dir,
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS.split(','),
keep_langs=['fr_FR', 'nl'],
))
self.assertEqual(files, [
'README.rst',
'data/f1.xml',
'data/f2.xml',
'i18n/fr.po',
'i18n/fr_BE.po',
'i18n_extra/fr.po',
'i18n_extra/nl_NL.po',
'models/stuff.py',
])
files = list(
addon_hash._walk(
self.sample_dir,
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS.split(","),
keep_langs=["fr_FR", "nl"],
)
)
self.assertEqual(
files,
[
"README.rst",
"data/f1.xml",
"data/f2.xml",
"i18n/fr.po",
"i18n/fr_BE.po",
"i18n_extra/fr.po",
"i18n_extra/nl_NL.po",
"models/stuff.py",
],
)
def test2(self):
checksum = addon_hash.addon_hash(
self.sample_dir,
exclude_patterns=['*.pyc', '*.pyo', '*.pot', 'static/*'],
keep_langs=['fr_FR', 'nl'],
exclude_patterns=["*.pyc", "*.pyo", "*.pot", "static/*"],
keep_langs=["fr_FR", "nl"],
)
self.assertEqual(checksum, 'fecb89486c8a29d1f760cbd01c1950f6e8421b14')
self.assertEqual(checksum, "fecb89486c8a29d1f760cbd01c1950f6e8421b14")

View File

@ -12,23 +12,22 @@ from odoo.modules import get_module_path
from odoo.tests import TransactionCase
from ..addon_hash import addon_hash
from ..models.module import IncompleteUpgradeError, DEFAULT_EXCLUDE_PATTERNS
from ..models.module import DEFAULT_EXCLUDE_PATTERNS, IncompleteUpgradeError
MODULE_NAME = 'module_auto_update'
MODULE_NAME = "module_auto_update"
class TestModule(TransactionCase):
def setUp(self):
super(TestModule, self).setUp()
self.own_module = self.env['ir.module.module'].search([
('name', '=', MODULE_NAME),
])
self.own_module = self.env["ir.module.module"].search(
[("name", "=", MODULE_NAME)]
)
self.own_dir_path = get_module_path(MODULE_NAME)
keep_langs = self.env['res.lang'].search([]).mapped('code')
keep_langs = self.env["res.lang"].search([]).mapped("code")
self.own_checksum = addon_hash(
self.own_dir_path,
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS.split(','),
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS.split(","),
keep_langs=keep_langs,
)
self.own_writeable = os.access(self.own_dir_path, os.W_OK)
@ -36,8 +35,9 @@ class TestModule(TransactionCase):
def test_compute_checksum_dir(self):
"""It should compute the directory's SHA-1 hash"""
self.assertEqual(
self.own_module._get_checksum_dir(), self.own_checksum,
'Module directory checksum not computed properly',
self.own_module._get_checksum_dir(),
self.own_checksum,
"Module directory checksum not computed properly",
)
def test_compute_checksum_dir_ignore_excluded(self):
@ -45,10 +45,11 @@ class TestModule(TransactionCase):
calculations"""
if not self.own_writeable:
self.skipTest("Own directory not writeable")
with tempfile.NamedTemporaryFile(suffix='.pyc', dir=self.own_dir_path):
with tempfile.NamedTemporaryFile(suffix=".pyc", dir=self.own_dir_path):
self.assertEqual(
self.own_module._get_checksum_dir(), self.own_checksum,
'SHA1 checksum does not ignore excluded extensions',
self.own_module._get_checksum_dir(),
self.own_checksum,
"SHA1 checksum does not ignore excluded extensions",
)
def test_compute_checksum_dir_recomputes_when_file_added(self):
@ -56,62 +57,59 @@ class TestModule(TransactionCase):
added to the module directory"""
if not self.own_writeable:
self.skipTest("Own directory not writeable")
with tempfile.NamedTemporaryFile(suffix='.py', dir=self.own_dir_path):
with tempfile.NamedTemporaryFile(suffix=".py", dir=self.own_dir_path):
self.assertNotEqual(
self.own_module._get_checksum_dir(), self.own_checksum,
'SHA1 checksum not recomputed',
self.own_module._get_checksum_dir(),
self.own_checksum,
"SHA1 checksum not recomputed",
)
def test_saved_checksums(self):
Imm = self.env['ir.module.module']
base_module = Imm.search([('name', '=', 'base')])
self.assertEqual(base_module.state, 'installed')
Imm = self.env["ir.module.module"]
base_module = Imm.search([("name", "=", "base")])
self.assertEqual(base_module.state, "installed")
self.assertFalse(Imm._get_saved_checksums())
Imm._save_installed_checksums()
saved_checksums = Imm._get_saved_checksums()
self.assertTrue(saved_checksums)
self.assertTrue(saved_checksums['base'])
self.assertTrue(saved_checksums["base"])
def test_get_modules_with_changed_checksum(self):
Imm = self.env['ir.module.module']
Imm = self.env["ir.module.module"]
self.assertTrue(Imm._get_modules_with_changed_checksum())
Imm._save_installed_checksums()
self.assertFalse(Imm._get_modules_with_changed_checksum())
@odoo.tests.tagged('post_install', '-at_install')
@odoo.tests.tagged("post_install", "-at_install")
class TestModuleAfterInstall(TransactionCase):
def setUp(self):
super(TestModuleAfterInstall, self).setUp()
Imm = self.env['ir.module.module']
self.own_module = Imm.search([('name', '=', MODULE_NAME)])
self.base_module = Imm.search([('name', '=', 'base')])
Imm = self.env["ir.module.module"]
self.own_module = Imm.search([("name", "=", MODULE_NAME)])
self.base_module = Imm.search([("name", "=", "base")])
def test_get_modules_partially_installed(self):
Imm = self.env['ir.module.module']
self.assertTrue(
self.own_module not in Imm._get_modules_partially_installed())
Imm = self.env["ir.module.module"]
self.assertTrue(self.own_module not in Imm._get_modules_partially_installed())
self.own_module.button_upgrade()
self.assertTrue(
self.own_module in Imm._get_modules_partially_installed())
self.assertTrue(self.own_module in Imm._get_modules_partially_installed())
self.own_module.button_upgrade_cancel()
self.assertTrue(
self.own_module not in Imm._get_modules_partially_installed())
self.assertTrue(self.own_module not in Imm._get_modules_partially_installed())
def test_upgrade_changed_checksum(self):
Imm = self.env['ir.module.module']
Bmu = self.env['base.module.upgrade']
Imm = self.env["ir.module.module"]
Bmu = self.env["base.module.upgrade"]
# check modules are in installed state
installed_modules = Imm.search([('state', '=', 'installed')])
installed_modules = Imm.search([("state", "=", "installed")])
self.assertTrue(self.own_module in installed_modules)
self.assertTrue(self.base_module in installed_modules)
self.assertTrue(len(installed_modules) > 2)
# change the checksum of 'base'
Imm._save_installed_checksums()
saved_checksums = Imm._get_saved_checksums()
saved_checksums['base'] = False
saved_checksums["base"] = False
Imm._save_checksums(saved_checksums)
changed_modules = Imm._get_modules_with_changed_checksum()
self.assertEqual(len(changed_modules), 1)
@ -121,102 +119,100 @@ class TestModuleAfterInstall(TransactionCase):
upgrade_module_mock.call_count += 1
# since we are upgrading base, all installed module
# must have been marked to upgrade at this stage
self.assertEqual(self.base_module.state, 'to upgrade')
self.assertEqual(self.own_module.state, 'to upgrade')
installed_modules.write({'state': 'installed'})
self.assertEqual(self.base_module.state, "to upgrade")
self.assertEqual(self.own_module.state, "to upgrade")
installed_modules.write({"state": "installed"})
upgrade_module_mock.call_count = 0
# upgrade_changed_checksum commits, so mock that
with mock.patch.object(self.env.cr, 'commit'):
with mock.patch.object(self.env.cr, "commit"):
# we simulate an install by setting module states
Bmu._patch_method('upgrade_module', upgrade_module_mock)
Bmu._patch_method("upgrade_module", upgrade_module_mock)
try:
Imm.upgrade_changed_checksum()
self.assertEqual(upgrade_module_mock.call_count, 1)
self.assertEqual(self.base_module.state, 'installed')
self.assertEqual(self.own_module.state, 'installed')
self.assertEqual(self.base_module.state, "installed")
self.assertEqual(self.own_module.state, "installed")
saved_checksums = Imm._get_saved_checksums()
self.assertTrue(saved_checksums['base'])
self.assertTrue(saved_checksums["base"])
self.assertTrue(saved_checksums[MODULE_NAME])
finally:
Bmu._revert_method('upgrade_module')
Bmu._revert_method("upgrade_module")
def test_incomplete_upgrade(self):
Imm = self.env['ir.module.module']
Bmu = self.env['base.module.upgrade']
Imm = self.env["ir.module.module"]
Bmu = self.env["base.module.upgrade"]
installed_modules = Imm.search([('state', '=', 'installed')])
installed_modules = Imm.search([("state", "=", "installed")])
# change the checksum of 'base'
Imm._save_installed_checksums()
saved_checksums = Imm._get_saved_checksums()
saved_checksums['base'] = False
saved_checksums["base"] = False
Imm._save_checksums(saved_checksums)
def upgrade_module_mock(self_model):
upgrade_module_mock.call_count += 1
# since we are upgrading base, all installed module
# must have been marked to upgrade at this stage
self.assertEqual(self.base_module.state, 'to upgrade')
self.assertEqual(self.own_module.state, 'to upgrade')
installed_modules.write({'state': 'installed'})
self.assertEqual(self.base_module.state, "to upgrade")
self.assertEqual(self.own_module.state, "to upgrade")
installed_modules.write({"state": "installed"})
# simulate partial upgrade
self.own_module.write({'state': 'to upgrade'})
self.own_module.write({"state": "to upgrade"})
upgrade_module_mock.call_count = 0
# upgrade_changed_checksum commits, so mock that
with mock.patch.object(self.env.cr, 'commit'):
with mock.patch.object(self.env.cr, "commit"):
# we simulate an install by setting module states
Bmu._patch_method('upgrade_module', upgrade_module_mock)
Bmu._patch_method("upgrade_module", upgrade_module_mock)
try:
with self.assertRaises(IncompleteUpgradeError):
Imm.upgrade_changed_checksum()
self.assertEqual(upgrade_module_mock.call_count, 1)
finally:
Bmu._revert_method('upgrade_module')
Bmu._revert_method("upgrade_module")
def test_incomplete_upgrade_no_checkusm(self):
Imm = self.env['ir.module.module']
Bmu = self.env['base.module.upgrade']
Imm = self.env["ir.module.module"]
Bmu = self.env["base.module.upgrade"]
installed_modules = Imm.search(
[('state', '=', 'installed')])
installed_modules = Imm.search([("state", "=", "installed")])
# change the checksum of 'base'
Imm._save_installed_checksums()
saved_checksums = Imm._get_saved_checksums()
Imm._save_checksums(saved_checksums)
self.base_module.write({'state': 'to upgrade'})
self.base_module.write({"state": "to upgrade"})
def upgrade_module_mock(self_model):
upgrade_module_mock.call_count += 1
# since we are upgrading base, all installed module
# must have been marked to upgrade at this stage
self.assertEqual(self.base_module.state, 'to upgrade')
self.assertEqual(self.own_module.state, 'installed')
installed_modules.write({'state': 'installed'})
self.assertEqual(self.base_module.state, "to upgrade")
self.assertEqual(self.own_module.state, "installed")
installed_modules.write({"state": "installed"})
upgrade_module_mock.call_count = 0
# upgrade_changed_checksum commits, so mock that
with mock.patch.object(self.env.cr, 'commit'):
with mock.patch.object(self.env.cr, "commit"):
# we simulate an install by setting module states
Bmu._patch_method('upgrade_module',
upgrade_module_mock)
Bmu._patch_method("upgrade_module", upgrade_module_mock)
# got just other modules to_upgrade and no checksum ones
try:
Imm.upgrade_changed_checksum()
self.assertEqual(upgrade_module_mock.call_count, 1)
finally:
Bmu._revert_method('upgrade_module')
Bmu._revert_method("upgrade_module")
def test_nothing_to_upgrade(self):
Imm = self.env['ir.module.module']
Bmu = self.env['base.module.upgrade']
Imm = self.env["ir.module.module"]
Bmu = self.env["base.module.upgrade"]
Imm._save_installed_checksums()
@ -226,12 +222,12 @@ class TestModuleAfterInstall(TransactionCase):
upgrade_module_mock.call_count = 0
# upgrade_changed_checksum commits, so mock that
with mock.patch.object(self.env.cr, 'commit'):
with mock.patch.object(self.env.cr, "commit"):
# we simulate an install by setting module states
Bmu._patch_method('upgrade_module', upgrade_module_mock)
Bmu._patch_method("upgrade_module", upgrade_module_mock)
try:
Imm.upgrade_changed_checksum()
self.assertEqual(upgrade_module_mock.call_count, 0)
finally:
Bmu._revert_method('upgrade_module')
Bmu._revert_method("upgrade_module")

View File

@ -4,17 +4,15 @@
Copyright 2018 Brainbean Apps (https://brainbeanapps.com)
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
-->
<record id="ir_module_module_upgrade_changed_checksum" model="ir.actions.server">
<field name="name">Auto-Upgrade Modules</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="base.model_ir_module_module"/>
<field name="model_id" ref="base.model_ir_module_module" />
<field name="state">code</field>
<field name="code">
action = model.upgrade_changed_checksum()
</field>
</record>
<menuitem
name="Auto-Upgrade Modules"
action="ir_module_module_upgrade_changed_checksum"
@ -22,6 +20,6 @@
groups="base.group_no_one"
parent="base.menu_management"
sequence="45"
icon="fa-exchange"/>
icon="fa-exchange"
/>
</odoo>