Maciej Wawro
6 years ago
22 changed files with 681 additions and 0 deletions
-
1.gitignore
-
88galicea_environment_checkup/README.rst
-
7galicea_environment_checkup/__init__.py
-
27galicea_environment_checkup/__manifest__.py
-
3galicea_environment_checkup/controllers/__init__.py
-
26galicea_environment_checkup/controllers/dashboard.py
-
0galicea_environment_checkup/environment_checkup/__init__.py
-
49galicea_environment_checkup/environment_checkup/core.py
-
30galicea_environment_checkup/environment_checkup/custom.py
-
181galicea_environment_checkup/environment_checkup/dependencies.py
-
24galicea_environment_checkup/environment_checkup/runtime.py
-
0galicea_environment_checkup/environment_checkup/utils.py
-
1galicea_environment_checkup/models/__init__.py
-
19galicea_environment_checkup/models/ext_module.py
-
BINgalicea_environment_checkup/static/description/custom.png
-
BINgalicea_environment_checkup/static/description/dependencies.png
-
BINgalicea_environment_checkup/static/description/icon.png
-
108galicea_environment_checkup/static/src/js/environment_checkup.js
-
71galicea_environment_checkup/static/src/xml/templates.xml
-
9galicea_environment_checkup/views/data.xml
-
17galicea_environment_checkup/views/environment_checks.xml
-
20galicea_environment_checkup/views/views.xml
@ -0,0 +1 @@ |
|||
*.pyc |
@ -0,0 +1,88 @@ |
|||
About |
|||
===== |
|||
|
|||
This add-on allows you to: |
|||
|
|||
- programmatically check software dependencies required by your add-on, as well as inform the Administrator as to how to meet them, |
|||
- add custom verification for Odoo instance set-up and inform the Administrator about any inconsistencies. |
|||
|
|||
Dependency checks |
|||
================= |
|||
.. image:: /galicea_environment_checkup/static/description/dependencies.png |
|||
|
|||
How-to |
|||
------ |
|||
Just add ``environment_checkup`` entry to ``__manifest__.py`` |
|||
|
|||
.. code:: |
|||
|
|||
{ |
|||
... |
|||
'environment_checkup': { |
|||
'dependencies': { |
|||
'python': [ |
|||
{ |
|||
'name': 'Crypto', |
|||
'version': '>=2.6.2', |
|||
'install': "pip install 'PyCrypto>=2.6.1'" |
|||
}, |
|||
], |
|||
'external': [ |
|||
{ |
|||
'name': 'wkhtmltopdf', |
|||
'install': "apt install wkhtmltopdf" |
|||
}, |
|||
{ |
|||
'name': 'git', |
|||
'version': '^3.0.0', |
|||
'install': "apt install git" |
|||
} |
|||
], |
|||
'internal': [ |
|||
{ |
|||
'name': 'web', |
|||
'version': '~10.0.1.0' |
|||
} |
|||
] |
|||
} |
|||
} |
|||
} |
|||
|
|||
Custom in-code checks |
|||
===================== |
|||
.. image:: /galicea_environment_checkup/static/description/custom.png |
|||
|
|||
How-to |
|||
------ |
|||
|
|||
1. Add the check |
|||
|
|||
``system_checks.py`` |
|||
|
|||
.. code:: |
|||
|
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import cgi |
|||
from odoo.addons.galicea_environment_checkup import custom_check, CheckSuccess, CheckWarning, CheckFail |
|||
|
|||
@custom_check |
|||
def check_mail(env): |
|||
users_without_emails = env['res.users'].sudo().search([('email', '=', False)]) |
|||
|
|||
if users_without_emails: |
|||
raise CheckWarning( |
|||
'Some users don\'t have their e-mails set up.', |
|||
details='See user <tt>{}</tt>.'.format(cgi.escape(users_without_emails[0].name)) |
|||
) |
|||
|
|||
return CheckSuccess('All users have their e-mails set.') |
|||
|
|||
2. Make sure it's loaded |
|||
|
|||
``__init__.py`` |
|||
|
|||
.. code:: |
|||
|
|||
# -*- coding: utf-8 -*- |
|||
from . import system_checks |
@ -0,0 +1,7 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
from . import models |
|||
from . import controllers |
|||
|
|||
from environment_checkup.custom import custom_check |
|||
from environment_checkup.core import CheckFail, CheckWarning, CheckSuccess |
@ -0,0 +1,27 @@ |
|||
# -*- coding: utf-8 -*- |
|||
{ |
|||
'name': "Galicea Enviromnent Check-up", |
|||
|
|||
'summary': """ |
|||
Programmatically validate environment, including internal and external |
|||
dependencies""", |
|||
|
|||
'author': "Maciej Wawro", |
|||
'maintainer': "Galicea", |
|||
'website': "http://galicea.pl", |
|||
|
|||
'category': 'Technical Settings', |
|||
'version': '10.0.1.0', |
|||
|
|||
'depends': ['web'], |
|||
|
|||
'data': [ |
|||
'views/data.xml', |
|||
'views/views.xml', |
|||
'views/environment_checks.xml' |
|||
], |
|||
|
|||
'qweb': ['static/src/xml/templates.xml'], |
|||
|
|||
'installable': True |
|||
} |
@ -0,0 +1,3 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
from . import dashboard |
@ -0,0 +1,26 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
from odoo import http |
|||
from odoo.exceptions import AccessError |
|||
from odoo.http import request |
|||
|
|||
from ..environment_checkup.runtime import all_installed_checks, display_data |
|||
from ..environment_checkup.core import CheckResult |
|||
|
|||
class Dashboard(http.Controller): |
|||
@http.route('/galicea_environment_checkup/data', type='json', auth='user') |
|||
def data(self, request, **kw): |
|||
if not request.env.user.has_group('base.group_erp_manager'): |
|||
raise AccessError("Access Denied") |
|||
|
|||
checks = all_installed_checks(request.env) |
|||
response = display_data(request.env, checks) |
|||
|
|||
priority = { |
|||
CheckResult.FAIL: 0, |
|||
CheckResult.WARNING: 1, |
|||
CheckResult.SUCCESS: 2 |
|||
} |
|||
response.sort(key=lambda res: (priority[res['result']], res['module'])) |
|||
|
|||
return response |
@ -0,0 +1,49 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import logging |
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
class CheckResult(object): |
|||
SUCCESS = 'success' |
|||
WARNING = 'warning' |
|||
FAIL = 'fail' |
|||
|
|||
def __init__(self, result, message, details = None): |
|||
super(CheckResult, self).__init__() |
|||
|
|||
self.result = result |
|||
self.message = message |
|||
self.details = details |
|||
|
|||
class CheckSuccess(CheckResult): |
|||
def __init__(self, message, **kwargs): |
|||
super(CheckSuccess, self).__init__(CheckResult.SUCCESS, message, **kwargs) |
|||
|
|||
class CheckIssue(CheckResult, Exception): |
|||
def __init__(self, result, message, **kwargs): |
|||
Exception.__init__(self, message) |
|||
CheckResult.__init__(self, result, message, **kwargs) |
|||
|
|||
class CheckFail(CheckIssue): |
|||
def __init__(self, message, **kwargs): |
|||
super(CheckFail, self).__init__(CheckResult.FAIL, message, **kwargs) |
|||
|
|||
class CheckWarning(CheckIssue): |
|||
def __init__(self, message, **kwargs): |
|||
super(CheckWarning, self).__init__(CheckResult.WARNING, message, **kwargs) |
|||
|
|||
class Check(object): |
|||
def __init__(self, module): |
|||
self.module = module |
|||
|
|||
def run(self, env): |
|||
try: |
|||
return self._run(env) |
|||
except CheckIssue as issue: |
|||
return issue |
|||
except Exception as ex: |
|||
_logger.exception(ex) |
|||
return CheckFail('Check failed when processing: {}'.format(ex)) |
|||
|
|||
def _run(self, env): |
|||
raise NotImplementedError('Should be overriden by the subclass') |
@ -0,0 +1,30 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import collections |
|||
|
|||
from core import Check |
|||
|
|||
custom_checks_per_module = collections.defaultdict(list) |
|||
|
|||
class CustomCheck(Check): |
|||
def __init__(self, module, func): |
|||
super(CustomCheck, self).__init__(module) |
|||
self.func = func |
|||
|
|||
def _run(self, env): |
|||
return self.func(env) |
|||
|
|||
def custom_check(func): |
|||
try: |
|||
module = func.__module__.split('.')[2] |
|||
except IndexError: |
|||
module = '' |
|||
|
|||
custom_checks_per_module[module].append( |
|||
CustomCheck(module=module, func=func) |
|||
) |
|||
|
|||
return func |
|||
|
|||
def get_checks_for_module(module_name): |
|||
return custom_checks_per_module[module_name] |
@ -0,0 +1,181 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import subprocess |
|||
import re |
|||
import cgi |
|||
from odoo.modules.module import load_information_from_description_file |
|||
from odoo.tools import which |
|||
|
|||
from core import Check, CheckSuccess, CheckWarning, CheckFail |
|||
|
|||
class DependencyCheck(Check): |
|||
dependency_type = None |
|||
|
|||
def __init__(self, module, dependency): |
|||
super(DependencyCheck, self).__init__(module) |
|||
self.dependency = dependency |
|||
|
|||
def _dependency_installed(self, env, name): |
|||
raise NotImplementedError('Should be overriden by the subclass') |
|||
|
|||
def _installed_version(self, env, name): |
|||
raise NotImplementedError('Should be overriden by the subclass') |
|||
|
|||
def _details(self): |
|||
if 'install' in self.dependency: |
|||
return 'Install command: <pre>{}</pre>'.format(self.dependency['install']) |
|||
return None |
|||
|
|||
def __has_required_version(self, installed_version, version_expression): |
|||
version_operator = '=' |
|||
version = self.dependency['version'] |
|||
if version[:1] in ['=', '~', '^']: |
|||
version_operator = version[:1] |
|||
version = version[1:] |
|||
elif version[:2] in ['>=']: |
|||
version_operator = version[:2] |
|||
version = version[2:] |
|||
|
|||
try: |
|||
parsed_version = map(int, version.split('.')) |
|||
except ValueError: |
|||
raise CheckFail( |
|||
'Invalid version expression', |
|||
details = """ |
|||
Allowed expressions are <pre>=x.y.z</pre>, <pre>>=x.y.z</pre>, <pre>^x.z.y</pre>, |
|||
<pre>~x.y.z. Got <pre>{}</pre>""".format(cgi.escape(self.dependency['version'])) |
|||
) |
|||
parsed_installed_version = map(int, installed_version.split('.')) |
|||
|
|||
parsed_version.extend(0 for _ in range(len(parsed_installed_version) - len(parsed_version))) |
|||
parsed_installed_version.extend(0 for _ in range(len(parsed_version) - len(parsed_installed_version))) |
|||
|
|||
if version_operator == '^': |
|||
if parsed_installed_version[:1] != parsed_version[:1]: |
|||
return False |
|||
version_operator = '>=' |
|||
elif version_operator == '~': |
|||
if parsed_installed_version[:2] != parsed_version[:2]: |
|||
return False |
|||
version_operator = '>=' |
|||
|
|||
if version_operator == '>=': |
|||
return tuple(parsed_installed_version) >= tuple(parsed_version) |
|||
elif version_operator == '=': |
|||
return tuple(parsed_installed_version) == tuple(parsed_version) |
|||
|
|||
assert False |
|||
|
|||
def _run(self, env): |
|||
name = self.dependency['name'] |
|||
if not self._dependency_installed(env, name): |
|||
raise CheckFail( |
|||
'Required {} - {} - is not installed.'.format(self.dependency_type, name), |
|||
details=self._details() |
|||
) |
|||
if 'version' in self.dependency: |
|||
version_expression = self.dependency['version'] |
|||
installed_version = self._installed_version(env, name) |
|||
if not self.__has_required_version(installed_version, version_expression): |
|||
raise CheckWarning( |
|||
'Required {} - {} - has version {}, but {} is needed.'.format( |
|||
self.dependency_type, |
|||
name, |
|||
installed_version, |
|||
version_expression |
|||
), |
|||
details=self._details() |
|||
) |
|||
return CheckSuccess( |
|||
'Required {} - {} - is installed.'.format(self.dependency_type, name), |
|||
details=self._details() |
|||
) |
|||
|
|||
class InternalDependencyCheck(DependencyCheck): |
|||
dependency_type = 'Odoo module' |
|||
|
|||
def _dependency_installed(self, env, name): |
|||
return name in env.registry._init_modules |
|||
|
|||
def _installed_version(self, env, name): |
|||
return env['ir.module.module'].sudo().search([('name', '=', name)]).latest_version |
|||
|
|||
class PythonDependencyCheck(DependencyCheck): |
|||
dependency_type = 'Python module' |
|||
|
|||
def _dependency_installed(self, env, name): |
|||
try: |
|||
__import__(name) |
|||
return True |
|||
except ImportError: |
|||
return False |
|||
|
|||
def _installed_version(self, env, name): |
|||
try: |
|||
return __import__(name).__version__ |
|||
except AttributeError: |
|||
raise CheckWarning( |
|||
'Could not detect version of the Python module: {}.'.format(name), |
|||
details=self._details() |
|||
) |
|||
|
|||
class ExternalDependencyCheck(DependencyCheck): |
|||
dependency_type = 'system executable' |
|||
|
|||
def _dependency_installed(self, env, name): |
|||
try: |
|||
which(name) |
|||
return True |
|||
except IOError: |
|||
return False |
|||
|
|||
def _installed_version(self, env, name): |
|||
try: |
|||
exe = which(name) |
|||
out = subprocess.check_output([exe, '--version']) |
|||
match = re.search('[\d.]+', out) |
|||
if not match: |
|||
raise CheckWarning( |
|||
'Unable to detect version for executable {}'.format(name), |
|||
details="Command {} --version returned <pre>{}</pre>".format(exe, out) |
|||
) |
|||
return match.group(0) |
|||
except subprocess.CalledProcessError as e: |
|||
raise CheckWarning( |
|||
'Unable to detect version for executable {}: {}'.format(name, e), |
|||
details=self._details() |
|||
) |
|||
|
|||
def get_checks_for_module(module_name): |
|||
result = [] |
|||
|
|||
manifest = load_information_from_description_file(module_name) |
|||
manifest_checks = manifest.get('environment_checkup') or {} |
|||
dependencies = manifest_checks.get('dependencies') or {} |
|||
|
|||
for dependency in dependencies.get('python') or []: |
|||
result.append(PythonDependencyCheck(module_name, dependency)) |
|||
for dependency in dependencies.get('external') or []: |
|||
result.append(ExternalDependencyCheck(module_name, dependency)) |
|||
for dependency in dependencies.get('internal') or []: |
|||
result.append(InternalDependencyCheck(module_name, dependency)) |
|||
|
|||
return result |
|||
|
|||
def get_checks_for_module_recursive(module): |
|||
class ModuleDFS(object): |
|||
def __init__(self): |
|||
self.visited_modules = set() |
|||
self.checks = [] |
|||
|
|||
def visit(self, module): |
|||
if module.name in self.visited_modules: |
|||
return |
|||
self.visited_modules.add(module.name) |
|||
self.checks += get_checks_for_module(module.name) |
|||
for module_dependency in module.dependencies_id: |
|||
if module_dependency.depend_id: |
|||
self.visit(module_dependency.depend_id) |
|||
return self |
|||
|
|||
return ModuleDFS().visit(module).checks |
@ -0,0 +1,24 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import custom, dependencies |
|||
|
|||
def all_installed_checks(env): |
|||
result = [] |
|||
installed_modules = env.registry._init_modules |
|||
for module_name in installed_modules: |
|||
result += custom.get_checks_for_module(module_name) |
|||
result += dependencies.get_checks_for_module(module_name) |
|||
return result |
|||
|
|||
def display_data(env, checks): |
|||
response = [] |
|||
for check in checks: |
|||
result = check.run(env) |
|||
response.append({ |
|||
'module': check.module, |
|||
'message': result.message, |
|||
'details': result.details, |
|||
'result': result.result |
|||
}) |
|||
|
|||
return response |
@ -0,0 +1 @@ |
|||
from . import ext_module |
@ -0,0 +1,19 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import json |
|||
from odoo import api, fields, models |
|||
|
|||
from ..environment_checkup import dependencies |
|||
from ..environment_checkup.runtime import display_data |
|||
|
|||
class Module(models.Model): |
|||
_inherit = 'ir.module.module' |
|||
|
|||
dependency_checks = fields.Text( |
|||
compute='_compute_dependency_checks' |
|||
) |
|||
|
|||
@api.one |
|||
def _compute_dependency_checks(self): |
|||
checks = dependencies.get_checks_for_module_recursive(self) |
|||
self.dependency_checks = json.dumps(display_data(self.env, checks)) |
After Width: 625 | Height: 198 | Size: 24 KiB |
After Width: 966 | Height: 755 | Size: 92 KiB |
After Width: 600 | Height: 600 | Size: 40 KiB |
@ -0,0 +1,108 @@ |
|||
odoo.define('galicea_environment_checkup', function(require) { |
|||
"use strict"; |
|||
|
|||
var core = require('web.core'); |
|||
var form_common = require('web.form_common'); |
|||
var Widget = require('web.Widget'); |
|||
var session = require('web.session'); |
|||
var QWeb = core.qweb; |
|||
var SystrayMenu = require('web.SystrayMenu'); |
|||
var Model = require('web.Model'); |
|||
|
|||
var Users = new Model('res.users'); |
|||
|
|||
var SystrayIcon = Widget.extend({ |
|||
tagName: 'li', |
|||
events: { |
|||
"click": "on_click", |
|||
}, |
|||
|
|||
start: function(){ |
|||
this.load(this.all_dashboards); |
|||
return this._super(); |
|||
}, |
|||
|
|||
load: function(dashboards){ |
|||
var self = this; |
|||
var loading_done = new $.Deferred(); |
|||
Users.call('has_group', ['base.group_erp_manager']).then(function(is_admin) { |
|||
if (is_admin) { |
|||
session.rpc('/galicea_environment_checkup/data', {}) |
|||
.then(function (data) { |
|||
var counts = { 'success': 0, 'warning': 0, 'fail': 0 }; |
|||
data.forEach(function (check) { ++counts[check.result]; }); |
|||
|
|||
var result; |
|||
if (counts['fail']) { |
|||
result = 'fail'; |
|||
} else if (counts['warning']) { |
|||
result = 'warning'; |
|||
} else { |
|||
result = 'success'; |
|||
} |
|||
|
|||
self.replaceElement(QWeb.render('GaliceaEnvironmentCheckupIcon', { |
|||
'result': result, |
|||
'count': counts['warning'] + counts['fail'] |
|||
})); |
|||
loading_done.resolve(); |
|||
}); |
|||
} else { |
|||
loading_done.resolve(); |
|||
} |
|||
}); |
|||
|
|||
return loading_done; |
|||
}, |
|||
|
|||
on_click: function (event) { |
|||
event.preventDefault(); |
|||
this.do_action('galicea_environment_checkup.dashboard_action', {clear_breadcrumbs: true}); |
|||
}, |
|||
}); |
|||
|
|||
SystrayMenu.Items.push(SystrayIcon); |
|||
|
|||
var Dashboard = Widget.extend({ |
|||
start: function(){ |
|||
return this.load(this.all_dashboards); |
|||
}, |
|||
|
|||
load: function(dashboards) { |
|||
var self = this; |
|||
var loading_done = new $.Deferred(); |
|||
session.rpc('/galicea_environment_checkup/data', {}) |
|||
.then(function (data) { |
|||
self.replaceElement(QWeb.render('GaliceaEnvironmentCheckupDashboard', {'data': data})); |
|||
loading_done.resolve(); |
|||
}); |
|||
return loading_done; |
|||
}, |
|||
}); |
|||
|
|||
core.action_registry.add('galicea_environment_checkup.dashboard', Dashboard); |
|||
|
|||
var FormWidget = form_common.AbstractField.extend({ |
|||
init: function() { |
|||
this._super.apply(this, arguments); |
|||
this.set("value", "[]"); |
|||
}, |
|||
|
|||
render_value: function() { |
|||
var data = JSON.parse(this.get('value')); |
|||
if (data.length == 0) { |
|||
this.replaceElement('<div />'); |
|||
return; |
|||
} |
|||
this.replaceElement(QWeb.render('GaliceaEnvironmentCheckupFormWidget', {'data': data})); |
|||
}, |
|||
}); |
|||
|
|||
core.form_widget_registry.add('environment_checks', FormWidget); |
|||
|
|||
return { |
|||
SystrayIcon: SystrayIcon, |
|||
Dashboard: Dashboard, |
|||
FormWidget: FormWidget |
|||
}; |
|||
}); |
@ -0,0 +1,71 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
|
|||
<templates xml:space="preserve"> |
|||
<t t-name="GaliceaEnvironmentCheckupIcon"> |
|||
<li> |
|||
<a href="#" t-att-title="result == 'success' ? 'No system setup issues' : 'Found '+count+' system setup issues'"> |
|||
<t t-if="result == 'success'"> |
|||
<i class="fa fa-check" style="opacity:0.5"></i> |
|||
</t> |
|||
<t t-if="result == 'warning'"> |
|||
<i class="fa fa-exclamation-triangle" style="color: orange"></i> |
|||
</t> |
|||
<t t-if="result == 'fail'"> |
|||
<i class="fa fa-exclamation-circle" style="color: red"></i> |
|||
</t> |
|||
<t t-if="result != 'success'"> |
|||
<span><t t-raw="count" /></span> |
|||
</t> |
|||
</a> |
|||
</li> |
|||
</t> |
|||
|
|||
<t t-name="GaliceaEnvironmentChecks"> |
|||
<div class="row"> |
|||
<t t-foreach="data" t-as="check"> |
|||
<div class="col-md-12"> |
|||
<div style="display: flex; background-color: white; padding: 10px; margin: 10px"> |
|||
<div style="flex-grow: 0; flex-shrink: 0; width:50px; margin-right: 20px"> |
|||
<t t-if="check.result == 'success'"> |
|||
<i class="fa fa-check fa-4x" style="color: green" aria-hidden="true"></i> |
|||
</t> |
|||
<t t-if="check.result == 'warning'"> |
|||
<i class="fa fa-exclamation-triangle fa-4x" style="color: orange" aria-hidden="true"></i> |
|||
</t> |
|||
<t t-if="check.result == 'fail'"> |
|||
<i class="fa fa-exclamation-circle fa-4x" style="color: red" aria-hidden="true"></i> |
|||
</t> |
|||
</div> |
|||
<div style="flex-grow: 1; flex-shrink: 1"> |
|||
Module: <t t-esc="check.module" /> |
|||
<h4><t t-esc="check.message" /></h4> |
|||
<div> |
|||
<t t-raw="check.details" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</t> |
|||
</div> |
|||
</t> |
|||
|
|||
<t t-name="GaliceaEnvironmentCheckupDashboard"> |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<h2>Environment check-up</h2> |
|||
<t t-call="GaliceaEnvironmentChecks"> |
|||
<t t-set="data" t-value="data" /> |
|||
</t> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</t> |
|||
|
|||
<t t-name="GaliceaEnvironmentCheckupFormWidget"> |
|||
<h1>Module dependencies</h1> |
|||
<t t-call="GaliceaEnvironmentChecks"> |
|||
<t t-set="data" t-value="data" /> |
|||
</t> |
|||
</t> |
|||
</templates> |
@ -0,0 +1,9 @@ |
|||
<odoo> |
|||
<data> |
|||
<template id="assets_backend" inherit_id="web.assets_backend"> |
|||
<xpath expr="." position="inside"> |
|||
<script src="/galicea_environment_checkup/static/src/js/environment_checkup.js" type="text/javascript" /> |
|||
</xpath> |
|||
</template> |
|||
</data> |
|||
</odoo> |
@ -0,0 +1,17 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<template id="stale_modules"> |
|||
<p>The following modules need to be upgraded:</p> |
|||
<ul> |
|||
<t t-foreach="modules" t-as="module"> |
|||
<li> |
|||
<a t-att-href="'#id={}&view_type=form&model=ir.module.module'.format(module.id)"> |
|||
<t t-esc="module.shortdesc" /> (technical name: <t t-esc="module.name" />; |
|||
version on disk: <strong><t t-esc="module.installed_version" /></strong>; |
|||
version in DB: <strong><t t-esc="module.latest_version" /></strong>) |
|||
</a> |
|||
</li> |
|||
</t> |
|||
</ul> |
|||
</template> |
|||
</odoo> |
@ -0,0 +1,20 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<record id="dashboard_action" model="ir.actions.client"> |
|||
<field name="name">Environment check-up dashboard</field> |
|||
<field name="tag">galicea_environment_checkup.dashboard</field> |
|||
</record> |
|||
<menuitem name="Environment check-up" id="dashboard_menu" |
|||
action="dashboard_action" parent="base.menu_administration" groups="base.group_erp_manager" /> |
|||
|
|||
<record id="module_form" model="ir.ui.view"> |
|||
<field name="name">module_form.checks</field> |
|||
<field name="model">ir.module.module</field> |
|||
<field name="inherit_id" ref="base.module_form"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="description_html" position="before"> |
|||
<field name="dependency_checks" widget="environment_checks" /> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue