Browse Source
[ADD] module_auto_update: Create module (#882)
[ADD] module_auto_update: Create module (#882)
* [IMP] module_auto_update: Create new module * Add checksum_dir and checksum_installed fields to ir.module.module * Add checksum_dir to compute current checksum of module directory in addons path * Add checksum_installed to store checksum of module directory when module was last installed or upgraded * Use checksumdir Python library to compute module directory sha1 hashes, ignoring pyc and pyo extensions * Extend update_list method to compare modules' checksum_dir and checksum_installed, then change state of modules with differing checksums to 'to upgrade' * Replace Apps/Updates menu item with menu item of same name, which updates apps list and displays tree view of ir.module.module records with state 'to upgrade' * Extend create and write methods to store computed checksum_dir as checksum_installed during module installation and upgrade, and set checksum_installed to False on uninstall * Use context to stop checksum_installed from being updated during upgrade/uninstall cancellation * Add cron job to periodically check for module upgrades by comparing checksums, then perform any available upgrades * Extend upgrade_module method (called by cron and 'Apply Scheduled Upgrades' menu item) to call update_list * Add post_init_hook to store checksum_installed of existing modules * Add test coverage * [FIX] module_auto_update: Fix test broken by changes * Use dummy module to test update_list method instead of module_auto_updatepull/844/merge
Brenton Hughes
8 years ago
committed by
Jairo Llopis
14 changed files with 566 additions and 0 deletions
-
76module_auto_update/README.rst
-
7module_auto_update/__init__.py
-
30module_auto_update/__manifest__.py
-
15module_auto_update/data/cron_data.xml
-
14module_auto_update/hooks.py
-
5module_auto_update/models/__init__.py
-
83module_auto_update/models/module.py
-
6module_auto_update/tests/__init__.py
-
212module_auto_update/tests/test_module.py
-
42module_auto_update/tests/test_module_upgrade.py
-
49module_auto_update/views/module_views.xml
-
5module_auto_update/wizards/__init__.py
-
21module_auto_update/wizards/module_upgrade.py
-
1requirements.txt
@ -0,0 +1,76 @@ |
|||
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg |
|||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html |
|||
:alt: License: LGPL-3 |
|||
|
|||
================== |
|||
Module Auto Update |
|||
================== |
|||
|
|||
This module will automatically check for and apply module upgrades on a schedule. |
|||
|
|||
Upgrade checking is accomplished by comparing the SHA1 checksums of currently-installed modules to the checksums of corresponding modules in the addons directories. |
|||
|
|||
Installation |
|||
============ |
|||
|
|||
Prior to installing this module, you need to: |
|||
|
|||
#. Install checksumdir with `pip install checksumdir` |
|||
#. Ensure all installed modules are up-to-date. When installed, this module will assume the versions found in the addons directories are currently installed. |
|||
|
|||
Configuration |
|||
============= |
|||
|
|||
The default time for checking and applying upgrades is 3:00 AM (UTC). To change this schedule, modify the "Perform Module Upgrades" scheduled action. |
|||
|
|||
This module will ignore .pyc and .pyo file extensions by default. To modify this, create a module_auto_update.checksum_excluded_extensions system parameter with the desired extensions listed as comma-separated values. |
|||
|
|||
Usage |
|||
===== |
|||
|
|||
Modules scheduled for upgrade can be viewed by clicking the "Updates" menu item in the Apps sidebar. |
|||
|
|||
To perform upgrades manually, click the "Apply Scheduled Upgrades" menu item in the Apps sidebar. |
|||
|
|||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas |
|||
:alt: Try me on Runbot |
|||
:target: https://runbot.odoo-community.org/runbot/149/10.0 |
|||
|
|||
Bug Tracker |
|||
=========== |
|||
|
|||
Bugs are tracked on `GitHub Issues |
|||
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please |
|||
check there if your issue has already been reported. If you spotted it first, |
|||
help us smash it by providing detailed and welcomed feedback. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Images |
|||
------ |
|||
|
|||
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
|||
|
|||
Contributors |
|||
------------ |
|||
|
|||
* Brent Hughes <brent.hughes@laslabs.com> |
|||
* Juan José Scarafía <jjs@adhoc.com.ar> |
|||
|
|||
Do not contact contributors directly about support or help with technical issues. |
|||
|
|||
Maintainer |
|||
---------- |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://odoo-community.org |
|||
|
|||
This module is maintained by the OCA. |
|||
|
|||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|||
mission is to support the collaborative development of Odoo features and |
|||
promote its widespread use. |
|||
|
|||
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,7 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from . import models |
|||
from . import wizards |
|||
from .hooks import post_init_hook |
@ -0,0 +1,30 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
{ |
|||
'name': 'Module Auto Update', |
|||
'summary': 'Automatically update Odoo modules', |
|||
'version': '10.0.1.0.0', |
|||
'category': 'Extra Tools', |
|||
'website': 'https://odoo-community.org/', |
|||
'author': 'LasLabs, ' |
|||
'Juan José Scarafía, ' |
|||
'Odoo Community Association (OCA)', |
|||
'license': 'LGPL-3', |
|||
'application': False, |
|||
'installable': True, |
|||
'post_init_hook': 'post_init_hook', |
|||
'external_dependencies': { |
|||
'python': [ |
|||
'checksumdir', |
|||
], |
|||
}, |
|||
'depends': [ |
|||
'base', |
|||
], |
|||
'data': [ |
|||
'views/module_views.xml', |
|||
'data/cron_data.xml', |
|||
], |
|||
} |
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo noupdate="1"> |
|||
<record model="ir.cron" id="module_check_upgrades_cron"> |
|||
<field name="name">Perform Module Upgrades</field> |
|||
<field name="active" eval="True"/> |
|||
<field name="user_id" ref="base.user_root"/> |
|||
<field name="interval_number">1</field> |
|||
<field name="interval_type">days</field> |
|||
<field name="numbercall">-1</field> |
|||
<field name="nextcall" eval="(DateTime.now() + timedelta(days= +1)).strftime('%Y-%m-%d 3:00:00')"/> |
|||
<field name="model">base.module.upgrade</field> |
|||
<field name="function">upgrade_module</field> |
|||
<field name="args" eval="'()'"/> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,14 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from odoo import SUPERUSER_ID, api |
|||
|
|||
|
|||
def post_init_hook(cr, registry): |
|||
env = api.Environment(cr, SUPERUSER_ID, {}) |
|||
installed_modules = env['ir.module.module'].search([ |
|||
('state', '=', 'installed'), |
|||
]) |
|||
for r in installed_modules: |
|||
r.checksum_installed = r.checksum_dir |
@ -0,0 +1,5 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from . import module |
@ -0,0 +1,83 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
import logging |
|||
|
|||
from odoo import api, fields, models |
|||
from odoo.modules.module import get_module_path |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
try: |
|||
from checksumdir import dirhash |
|||
except ImportError: |
|||
_logger.debug('Cannot `import checksumdir`.') |
|||
|
|||
|
|||
class Module(models.Model): |
|||
_inherit = 'ir.module.module' |
|||
|
|||
checksum_dir = fields.Char( |
|||
compute='_compute_checksum_dir', |
|||
) |
|||
checksum_installed = fields.Char() |
|||
|
|||
@api.depends('name') |
|||
def _compute_checksum_dir(self): |
|||
exclude = self.env["ir.config_parameter"].get_param( |
|||
"module_auto_update.checksum_excluded_extensions", |
|||
"pyc,pyo", |
|||
).split(",") |
|||
|
|||
for r in self: |
|||
r.checksum_dir = dirhash( |
|||
get_module_path(r.name), |
|||
'sha1', |
|||
excluded_extensions=exclude, |
|||
) |
|||
|
|||
def _store_checksum_installed(self, vals): |
|||
if self.env.context.get('retain_checksum_installed'): |
|||
return |
|||
if 'checksum_installed' not in vals: |
|||
if vals.get('state') == 'installed': |
|||
for r in self: |
|||
r.checksum_installed = r.checksum_dir |
|||
elif vals.get('state') == 'uninstalled': |
|||
self.write({'checksum_installed': False}) |
|||
|
|||
@api.multi |
|||
def button_uninstall_cancel(self): |
|||
return super( |
|||
Module, |
|||
self.with_context(retain_checksum_installed=True), |
|||
).button_uninstall_cancel() |
|||
|
|||
@api.multi |
|||
def button_upgrade_cancel(self): |
|||
return super( |
|||
Module, |
|||
self.with_context(retain_checksum_installed=True), |
|||
).button_upgrade_cancel() |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
res = super(Module, self).create(vals) |
|||
res._store_checksum_installed(vals) |
|||
return res |
|||
|
|||
@api.model |
|||
def update_list(self): |
|||
res = super(Module, self).update_list() |
|||
installed_modules = self.search([('state', '=', 'installed')]) |
|||
upgradeable_modules = installed_modules.filtered( |
|||
lambda r: r.checksum_dir != r.checksum_installed, |
|||
) |
|||
upgradeable_modules.write({'state': "to upgrade"}) |
|||
return res |
|||
|
|||
@api.multi |
|||
def write(self, vals): |
|||
res = super(Module, self).write(vals) |
|||
self._store_checksum_installed(vals) |
|||
return res |
@ -0,0 +1,6 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from . import test_module |
|||
from . import test_module_upgrade |
@ -0,0 +1,212 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
import logging |
|||
import tempfile |
|||
|
|||
import mock |
|||
|
|||
from odoo.modules import get_module_path |
|||
from odoo.tests.common import TransactionCase |
|||
|
|||
from .. import post_init_hook |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
try: |
|||
from checksumdir import dirhash |
|||
except ImportError: |
|||
_logger.debug('Cannot `import checksumdir`.') |
|||
|
|||
model = 'odoo.addons.module_auto_update.models.module' |
|||
|
|||
|
|||
class TestModule(TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super(TestModule, self).setUp() |
|||
module_name = 'module_auto_update' |
|||
self.own_module = self.env['ir.module.module'].search([ |
|||
('name', '=', module_name), |
|||
]) |
|||
self.own_dir_path = get_module_path(module_name) |
|||
self.own_checksum = dirhash( |
|||
self.own_dir_path, |
|||
'sha1', |
|||
excluded_extensions=['pyc', 'pyo'], |
|||
) |
|||
|
|||
@mock.patch('%s.get_module_path' % model) |
|||
def create_test_module(self, vals, get_module_path_mock): |
|||
get_module_path_mock.return_value = self.own_dir_path |
|||
test_module = self.env['ir.module.module'].create(vals) |
|||
return test_module |
|||
|
|||
def test_compute_checksum_dir(self): |
|||
"""It should compute the directory's SHA-1 hash""" |
|||
self.assertEqual( |
|||
self.own_module.checksum_dir, self.own_checksum, |
|||
'Module directory checksum not computed properly', |
|||
) |
|||
|
|||
def test_compute_checksum_dir_ignore_excluded(self): |
|||
"""It should exclude .pyc/.pyo extensions from checksum |
|||
calculations""" |
|||
with tempfile.NamedTemporaryFile( |
|||
suffix='.pyc', dir=self.own_dir_path): |
|||
self.assertEqual( |
|||
self.own_module.checksum_dir, self.own_checksum, |
|||
'SHA1 checksum does not ignore excluded extensions', |
|||
) |
|||
|
|||
def test_compute_checksum_dir_recomputes_when_file_added(self): |
|||
"""It should return a different value when a non-.pyc/.pyo file is |
|||
added to the module directory""" |
|||
with tempfile.NamedTemporaryFile( |
|||
suffix='.py', dir=self.own_dir_path): |
|||
self.assertNotEqual( |
|||
self.own_module.checksum_dir, self.own_checksum, |
|||
'SHA1 checksum not recomputed', |
|||
) |
|||
|
|||
def test_store_checksum_installed_state_installed(self): |
|||
"""It should set the module's checksum_installed equal to |
|||
checksum_dir when vals contain state 'installed'""" |
|||
self.own_module.checksum_installed = 'test' |
|||
self.own_module._store_checksum_installed({'state': 'installed'}) |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, self.own_module.checksum_dir, |
|||
'Setting state to installed does not store checksum_dir ' |
|||
'as checksum_installed', |
|||
) |
|||
|
|||
def test_store_checksum_installed_state_uninstalled(self): |
|||
"""It should clear the module's checksum_installed when vals |
|||
contain state 'uninstalled'""" |
|||
self.own_module.checksum_installed = 'test' |
|||
self.own_module._store_checksum_installed({'state': 'uninstalled'}) |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, False, |
|||
'Setting state to uninstalled does not clear checksum_installed', |
|||
) |
|||
|
|||
def test_store_checksum_installed_vals_contain_checksum_installed(self): |
|||
"""It should not set checksum_installed to False or checksum_dir when |
|||
a checksum_installed is included in vals""" |
|||
self.own_module.checksum_installed = 'test' |
|||
self.own_module._store_checksum_installed({ |
|||
'state': 'installed', |
|||
'checksum_installed': 'test', |
|||
}) |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, 'test', |
|||
'Providing checksum_installed in vals did not prevent overwrite', |
|||
) |
|||
|
|||
def test_store_checksum_installed_with_retain_context(self): |
|||
"""It should not set checksum_installed to False or checksum_dir when |
|||
self has context retain_checksum_installed=True""" |
|||
self.own_module.checksum_installed = 'test' |
|||
self.own_module.with_context( |
|||
retain_checksum_installed=True, |
|||
)._store_checksum_installed({'state': 'installed'}) |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, 'test', |
|||
'Providing retain_checksum_installed context did not prevent ' |
|||
'overwrite', |
|||
) |
|||
|
|||
def test_button_uninstall_cancel(self): |
|||
"""It should preserve checksum_installed when cancelling uninstall""" |
|||
self.own_module.write({'state': 'to remove'}) |
|||
self.own_module.checksum_installed = 'test' |
|||
self.own_module.button_uninstall_cancel() |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, 'test', |
|||
'Uninstall cancellation does not preserve checksum_installed', |
|||
) |
|||
|
|||
def test_button_upgrade_cancel(self): |
|||
"""It should preserve checksum_installed when cancelling upgrades""" |
|||
self.own_module.write({'state': 'to upgrade'}) |
|||
self.own_module.checksum_installed = 'test' |
|||
self.own_module.button_upgrade_cancel() |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, 'test', |
|||
'Upgrade cancellation does not preserve checksum_installed', |
|||
) |
|||
|
|||
def test_create(self): |
|||
"""It should call _store_checksum_installed method""" |
|||
_store_checksum_installed_mock = mock.MagicMock() |
|||
self.env['ir.module.module']._patch_method( |
|||
'_store_checksum_installed', |
|||
_store_checksum_installed_mock, |
|||
) |
|||
vals = { |
|||
'name': 'module_auto_update_test_module', |
|||
'state': 'installed', |
|||
} |
|||
self.create_test_module(vals) |
|||
_store_checksum_installed_mock.assert_called_once_with(vals) |
|||
self.env['ir.module.module']._revert_method( |
|||
'_store_checksum_installed', |
|||
) |
|||
|
|||
@mock.patch('%s.get_module_path' % model) |
|||
def test_update_list(self, get_module_path_mock): |
|||
"""It should change the state of modules with different |
|||
checksum_dir and checksum_installed to 'to upgrade'""" |
|||
get_module_path_mock.return_value = self.own_dir_path |
|||
vals = { |
|||
'name': 'module_auto_update_test_module', |
|||
'state': 'installed', |
|||
} |
|||
test_module = self.create_test_module(vals) |
|||
test_module.checksum_installed = 'test' |
|||
self.env['ir.module.module'].update_list() |
|||
self.assertEqual( |
|||
test_module.state, 'to upgrade', |
|||
'List update does not mark upgradeable modules "to upgrade"', |
|||
) |
|||
|
|||
def test_update_list_only_changes_installed(self): |
|||
"""It should not change the state of a module with a former state |
|||
other than 'installed' to 'to upgrade'""" |
|||
vals = { |
|||
'name': 'module_auto_update_test_module', |
|||
'state': 'uninstalled', |
|||
} |
|||
test_module = self.create_test_module(vals) |
|||
self.env['ir.module.module'].update_list() |
|||
self.assertNotEqual( |
|||
test_module.state, 'to upgrade', |
|||
'List update changed state of an uninstalled module', |
|||
) |
|||
|
|||
def test_write(self): |
|||
"""It should call _store_checksum_installed method""" |
|||
_store_checksum_installed_mock = mock.MagicMock() |
|||
self.env['ir.module.module']._patch_method( |
|||
'_store_checksum_installed', |
|||
_store_checksum_installed_mock, |
|||
) |
|||
vals = {'state': 'installed'} |
|||
self.own_module.write(vals) |
|||
_store_checksum_installed_mock.assert_called_once_with(vals) |
|||
self.env['ir.module.module']._revert_method( |
|||
'_store_checksum_installed', |
|||
) |
|||
|
|||
def test_post_init_hook(self): |
|||
"""It should set checksum_installed equal to checksum_dir for all |
|||
installed modules""" |
|||
installed_modules = self.env['ir.module.module'].search([ |
|||
('state', '=', 'installed'), |
|||
]) |
|||
post_init_hook(self.env.cr, None) |
|||
self.assertListEqual( |
|||
installed_modules.mapped('checksum_dir'), |
|||
installed_modules.mapped('checksum_installed'), |
|||
'Installed modules did not have checksum_installed stored', |
|||
) |
@ -0,0 +1,42 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
import mock |
|||
|
|||
from odoo.modules import get_module_path |
|||
from odoo.modules.registry import Registry |
|||
from odoo.tests.common import TransactionCase |
|||
|
|||
|
|||
class TestModuleUpgrade(TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super(TestModuleUpgrade, self).setUp() |
|||
module_name = 'module_auto_update' |
|||
self.own_module = self.env['ir.module.module'].search([ |
|||
('name', '=', module_name), |
|||
]) |
|||
self.own_dir_path = get_module_path(module_name) |
|||
|
|||
def test_upgrade_module_cancel(self): |
|||
"""It should preserve checksum_installed when cancelling upgrades""" |
|||
self.own_module.write({'state': 'to upgrade'}) |
|||
self.own_module.checksum_installed = 'test' |
|||
self.env['base.module.upgrade'].upgrade_module_cancel() |
|||
self.assertEqual( |
|||
self.own_module.checksum_installed, 'test', |
|||
'Upgrade cancellation does not preserve checksum_installed', |
|||
) |
|||
|
|||
@mock.patch.object(Registry, 'new') |
|||
def test_upgrade_module(self, new_mock): |
|||
"""It should call update_list method on ir.module.module""" |
|||
update_list_mock = mock.MagicMock() |
|||
self.env['ir.module.module']._patch_method( |
|||
'update_list', |
|||
update_list_mock, |
|||
) |
|||
self.env['base.module.upgrade'].upgrade_module() |
|||
update_list_mock.assert_called_once_with() |
|||
self.env['ir.module.module']._revert_method('update_list') |
@ -0,0 +1,49 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Module Search View --> |
|||
<record id="module_view_search" model="ir.ui.view"> |
|||
<field name="name">updates.module.search</field> |
|||
<field name="model">ir.module.module</field> |
|||
<field name="inherit_id" ref="base.view_module_filter"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="category_id" position="after"> |
|||
<filter name="scheduled_upgrades" string="Scheduled Upgrades" domain="[('state', '=', 'to upgrade')]"/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
|
|||
<!--Open Updates Action (updates apps list first)--> |
|||
<record id="module_action_open_updates" model="ir.actions.server"> |
|||
<field name="name">Open Updates and Update Apps List Server Action</field> |
|||
<field name="model_id" ref="model_ir_module_module"/> |
|||
<field name="code"> |
|||
if model.update_list(): |
|||
action = { |
|||
'name': 'Updates', |
|||
'type': 'ir.actions.act_window', |
|||
'res_model': 'ir.module.module', |
|||
'view_type': 'form', |
|||
'view_mode': 'tree,form', |
|||
'target': 'main', |
|||
'context': '{"search_default_scheduled_upgrades": 1}', |
|||
} |
|||
</field> |
|||
</record> |
|||
|
|||
<!--Apps / Updates menu item--> |
|||
<menuitem |
|||
name="Updates" |
|||
action="module_action_open_updates" |
|||
id="module_menu_updates" |
|||
groups="base.group_no_one" |
|||
parent="base.menu_management" |
|||
sequence="20"/> |
|||
|
|||
<!-- Menu in Settings > Technical for standard Updates link --> |
|||
<menuitem parent="base.menu_custom" sequence="27" name="Modules" id="menu_default_modules"/> |
|||
|
|||
<!-- Moved standard Updates link --> |
|||
<record model="ir.ui.menu" id="base.menu_module_updates"> |
|||
<field name="parent_id" ref="menu_default_modules"/> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,5 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from . import module_upgrade |
@ -0,0 +1,21 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from odoo import api, models |
|||
|
|||
|
|||
class ModuleUpgrade(models.TransientModel): |
|||
_inherit = 'base.module.upgrade' |
|||
|
|||
@api.multi |
|||
def upgrade_module_cancel(self): |
|||
return super( |
|||
ModuleUpgrade, |
|||
self.with_context(retain_checksum_installed=True), |
|||
).upgrade_module_cancel() |
|||
|
|||
@api.multi |
|||
def upgrade_module(self): |
|||
self.env['ir.module.module'].update_list() |
|||
super(ModuleUpgrade, self).upgrade_module() |
Write
Preview
Loading…
Cancel
Save
Reference in new issue