5 Commits
16.0 ... 18.0

47 changed files with 846 additions and 1238 deletions

View File

@@ -49,12 +49,9 @@ repos:
$(git rev-parse --show-toplevel))"' $(git rev-parse --show-toplevel))"'
- id: oca-gen-addon-readme - id: oca-gen-addon-readme
entry: entry:
bash -c 'oca-gen-addon-readme bash -c 'oca-gen-addon-readme --addons-dir=. --branch=$(git symbolic-ref
--addons-dir=.
--branch=$(git symbolic-ref
refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@") refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@")
--repo-name=$(basename $(git rev-parse --show-toplevel)) --repo-name=$(basename $(git rev-parse --show-toplevel)) --org-name="Elabore"
--org-name="Elabore"
--if-source-changed --keep-source-digest' --if-source-changed --keep-source-digest'
- repo: https://github.com/OCA/odoo-pre-commit-hooks - repo: https://github.com/OCA/odoo-pre-commit-hooks

View File

@@ -1,48 +0,0 @@
=============================================
maintenance_create_requests_from_project_task
=============================================
Allow the creation of multiple maintenance requests from a projet task.
When user click on the button "Create maintenance requests", a wizard appears.
The wizard allows the user to configure the requests and to select the maintenance equipments concerned.
At wizard validation, one or several maintenance requests are created, one for each equipement selected.
Installation
============
Use Odoo normal module installation procedure to install
``maintenance_create_requests_from_project_task``.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`_. In case of
trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Stéphan Sainléger
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
from . import models from . import models
from . import wizard from . import wizard

View File

@@ -3,7 +3,7 @@
{ {
"name": "maintenance_create_requests_from_project_task", "name": "maintenance_create_requests_from_project_task",
"version": "16.0.1.0.0", "version": "18.0.1.0.0",
"author": "Elabore", "author": "Elabore",
"website": "https://elabore.coop", "website": "https://elabore.coop",
"maintainer": "Stéphan Sainléger", "maintainer": "Stéphan Sainléger",

View File

@@ -1,10 +1,12 @@
from odoo import fields, models, api from odoo import api, fields, models
class ProjectTask(models.Model): class ProjectTask(models.Model):
_inherit = "project.task" _inherit = "project.task"
maintenance_request_ids = fields.One2many("maintenance.request", "task_id", string="Maintenance Requests") maintenance_request_ids = fields.One2many(
"maintenance.request", "task_id", string="Maintenance Requests"
)
maintenance_request_count = fields.Integer( maintenance_request_count = fields.Integer(
compute="_compute_maintenance_request_count" compute="_compute_maintenance_request_count"
) )
@@ -18,7 +20,7 @@ class ProjectTask(models.Model):
def action_view_maintenance_request_ids(self): def action_view_maintenance_request_ids(self):
""" """
Access to the undone maintenance requests for this task Access to the undone maintenance requests for this task.
""" """
self.ensure_one() self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id( action = self.env["ir.actions.actions"]._for_xml_id(

View File

@@ -1,17 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record id="view_task_form2_maintenance_inherited" model="ir.ui.view"> <record id="view_task_form2_maintenance_inherited" model="ir.ui.view">
<field name="model">project.task</field> <field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2" /> <field name="inherit_id" ref="project.view_task_form2" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="after"> <xpath expr="//field[@name='company_id']" position="after">
<field name="maintenance_request_count" invisible="1"/> <field name="maintenance_request_count" invisible="1" />
</xpath> </xpath>
<xpath expr="//div[@name='button_box']" position="inside"> <xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_maintenance_request_ids" type="object" attrs="{'invisible': [('maintenance_request_count', '=', 0)]}" class="oe_stat_button" icon="fa-tasks" > <button
name="action_view_maintenance_request_ids"
type="object"
invisible="maintenance_request_count == 0"
class="oe_stat_button"
icon="fa-tasks"
>
<div class="o_field_widget o_stat_info"> <div class="o_field_widget o_stat_info">
<span class="o_stat_value "> <span class="o_stat_value ">
<field name="maintenance_request_count" widget="statinfo" nolabel="1" /> <field
name="maintenance_request_count"
widget="statinfo"
nolabel="1"
/>
Maintenance Requests Maintenance Requests
</span> </span>
</div> </div>

View File

@@ -1,9 +1,11 @@
from odoo import api, fields, models, _ from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval from odoo.tools.safe_eval import safe_eval
class CreateMaintenanceRequestsWizard(models.TransientModel): class CreateMaintenanceRequestsWizard(models.TransientModel):
_name= "create.maintenance.requests.wizard" _name = "create.maintenance.requests.wizard"
_description= "Configure the maintenance requests to create from the current task." _description = "Configure the maintenance requests to create from the current task."
@api.model @api.model
def _default_task_id(self): def _default_task_id(self):
@@ -13,19 +15,28 @@ class CreateMaintenanceRequestsWizard(models.TransientModel):
def _default_equipment_model(self): def _default_equipment_model(self):
default_domain = [] default_domain = []
task_id = self.env["project.task"].browse(self._context.get("active_ids")) task_id = self.env["project.task"].browse(self._context.get("active_ids"))
project_equipements = self.env["maintenance.equipment"].search([("project_id", "=", task_id.project_id.id)]) project_equipements = self.env["maintenance.equipment"].search(
[("project_id", "=", task_id.project_id.id)]
)
if project_equipements: if project_equipements:
equipment_ids_list = [x.id for x in project_equipements] equipment_ids_list = [x.id for x in project_equipements]
default_domain.append(("id", "in", equipment_ids_list)) default_domain.append(("id", "in", equipment_ids_list))
return default_domain return default_domain
name = fields.Char("Title", required=True) name = fields.Char("Title", required=True)
user_id = fields.Many2one('res.users', string='Technician') user_id = fields.Many2one("res.users", string="Technician")
priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') priority = fields.Selection(
maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective") [("0", "Very Low"), ("1", "Low"), ("2", "Normal"), ("3", "High")],
schedule_date = fields.Datetime('Scheduled Date') string="Priority",
)
maintenance_type = fields.Selection(
[("corrective", "Corrective"), ("preventive", "Preventive")],
string="Maintenance Type",
default="corrective",
)
schedule_date = fields.Datetime("Scheduled Date")
duration = fields.Float(help="Duration in hours.") duration = fields.Float(help="Duration in hours.")
description = fields.Html('Description') description = fields.Html("Description")
equipment_domain = fields.Char("Equipment Domain", default=_default_equipment_model) equipment_domain = fields.Char("Equipment Domain", default=_default_equipment_model)
@@ -42,15 +53,14 @@ class CreateMaintenanceRequestsWizard(models.TransientModel):
Open the form view. Open the form view.
""" """
return { return {
'name': _('Create maintenance requests'), "name": _("Create maintenance requests"),
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'res_model': 'create.maintenance.requests.wizard', "res_model": "create.maintenance.requests.wizard",
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'target': 'new', "target": "new",
} }
def create_maintenance_requests(self): def create_maintenance_requests(self):
""" """
Create the maintenance requests with the data filled in the wizard form. Create the maintenance requests with the data filled in the wizard form.
@@ -59,14 +69,20 @@ class CreateMaintenanceRequestsWizard(models.TransientModel):
maintenance_requests = self.env["maintenance.request"].sudo().create(vals_list) maintenance_requests = self.env["maintenance.request"].sudo().create(vals_list)
return self._get_action(maintenance_requests) return self._get_action(maintenance_requests)
def _compute_vals_list(self): def _compute_vals_list(self):
""" """
Compute the list of data to use for all the maintenance requests creation Compute the list of data to use for all the maintenance requests creation.
""" """
equipment_list = self.env["maintenance.equipment"].search(safe_eval(self.equipment_domain)) equipment_list = self.env["maintenance.equipment"].search(
safe_eval(self.equipment_domain)
)
if len(equipment_list) == 0: if len(equipment_list) == 0:
raise UserError("No equipment is matching the domain. Maintenance request creation is not possible.") raise UserError(
_(
"No equipment is matching the domain. "
"Maintenance request creation is not possible."
)
)
vals_list = [] vals_list = []
common_vals = { common_vals = {
@@ -78,7 +94,7 @@ class CreateMaintenanceRequestsWizard(models.TransientModel):
"duration": self.duration, "duration": self.duration,
"description": self.description, "description": self.description,
"task_id": self.task_id.id, "task_id": self.task_id.id,
"project_id": self.task_id.project_id.id "project_id": self.task_id.project_id.id,
} }
for equipment in equipment_list: for equipment in equipment_list:
@@ -90,20 +106,25 @@ class CreateMaintenanceRequestsWizard(models.TransientModel):
return vals_list return vals_list
def _get_action(self, maintenance_requests): def _get_action(self, maintenance_requests):
""" """
Provide the action to go to the tree view of the maintenance requests created. Provide the action to go to the list view of the maintenance requests created.
""" """
search_view_ref = self.env.ref('maintenance.hr_equipment_request_view_search', False) search_view_ref = self.env.ref(
form_view_ref = self.env.ref('maintenance.hr_equipment_request_view_form', False) "maintenance.hr_equipment_request_view_search", False
tree_view_ref = self.env.ref('maintenance.hr_equipment_request_view_tree', False) )
form_view_ref = self.env.ref(
"maintenance.hr_equipment_request_view_form", False
)
list_view_ref = self.env.ref(
"maintenance.hr_equipment_request_view_tree", False
)
return { return {
'domain': [('id', 'in', maintenance_requests.ids)], "domain": [("id", "in", maintenance_requests.ids)],
'name': 'Maintenance Requests', "name": "Maintenance Requests",
'res_model': 'maintenance.request', "res_model": "maintenance.request",
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'views': [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')], "views": [(list_view_ref.id, "list"), (form_view_ref.id, "form")],
'search_view_id': search_view_ref and [search_view_ref.id], "search_view_id": search_view_ref and [search_view_ref.id],
} }

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="create_maintenance_requests_wizard_view_form" model="ir.ui.view"> <record id="create_maintenance_requests_wizard_view_form" model="ir.ui.view">
<field name="name">create.maintenance.requests.wizard.view.form</field> <field name="name">create.maintenance.requests.wizard.view.form</field>
@@ -12,10 +12,10 @@
</group> </group>
<group name="domain" string="Equipments targetted"> <group name="domain" string="Equipments targetted">
<field <field
name="equipment_domain" name="equipment_domain"
widget="domain" widget="domain"
options='{"model": "maintenance.equipment"}' options='{"model": "maintenance.equipment"}'
/> />
</group> </group>
<group name="data" string="Requests data"> <group name="data" string="Requests data">
<field name="user_id" /> <field name="user_id" />
@@ -27,8 +27,12 @@
</group> </group>
</sheet> </sheet>
<footer> <footer>
<button string="Create" name="create_maintenance_requests" type="object" <button
class="btn-primary" /> string="Create"
name="create_maintenance_requests"
type="object"
class="btn-primary"
/>
<button string="Cancel" class="btn-secondary" special="cancel" /> <button string="Cancel" class="btn-secondary" special="cancel" />
</footer> </footer>
</form> </form>
@@ -36,7 +40,9 @@
</record> </record>
<record <record
id="action_create_maintenance_requests_wizard" model="ir.actions.act_window"> id="action_create_maintenance_requests_wizard"
model="ir.actions.act_window"
>
<field name="name">Create Maintenance Requests</field> <field name="name">Create Maintenance Requests</field>
<field name="res_model">create.maintenance.requests.wizard</field> <field name="res_model">create.maintenance.requests.wizard</field>
<field name="view_mode">form</field> <field name="view_mode">form</field>
@@ -44,10 +50,16 @@
<field name="target">new</field> <field name="target">new</field>
</record> </record>
<record id="task_wizard_action_create_maintenance_requests" model="ir.actions.server"> <record
id="task_wizard_action_create_maintenance_requests"
model="ir.actions.server"
>
<field name="name">Create maintenance requests</field> <field name="name">Create maintenance requests</field>
<field name="model_id" ref="maintenance_create_requests_from_project_task.model_create_maintenance_requests_wizard"/> <field
<field name="binding_model_id" ref="project.model_project_task"/> name="model_id"
ref="maintenance_create_requests_from_project_task.model_create_maintenance_requests_wizard"
/>
<field name="binding_model_id" ref="project.model_project_task" />
<field name="state">code</field> <field name="state">code</field>
<field name="code">action = model.action_open_wizard()</field> <field name="code">action = model.action_open_wizard()</field>
</record> </record>

View File

@@ -2,22 +2,44 @@
maintenance_project_task_domain maintenance_project_task_domain
=============================== ===============================
This module adjusts the domain applied to task field in order to limit This module adjusts the domain applied to the task field on maintenance requests
selection to tasks in any of unfolded stages. in order to limit selection to tasks in any of unfolded stages only.
When linking a maintenance request to a project task, the default behavior shows
all tasks from the project. This module filters out tasks that are in folded
stages (typically completed or cancelled tasks), making it easier to select
relevant active tasks.
# Installation # Installation
Use Odoo normal module installation procedure to install Use Odoo normal module installation procedure to install
`maintenance_project_task_domain`. `maintenance_project_task_domain`.
This module depends on `maintenance_project` and will be auto-installed when
that module is present.
# Configuration
No configuration is needed. The module automatically applies the domain filter
to the task field on maintenance request forms.
# Usage
1. Go to Maintenance > Maintenance Requests
2. Create or edit a maintenance request
3. When selecting a task in the "Task" field, only tasks from unfolded stages
will be displayed
# Known issues / Roadmap # Known issues / Roadmap
- Consider making the stage filter configurable via settings
# Bug Tracker # Bug Tracker
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`\_. In case of Bugs are tracked on
trouble, please check there if your issue has already been [our issues website](https://git.elabore.coop/Elabore/maintenance-tools/issues). In
reported. If you spotted it first, help us smashing it by providing a case of trouble, please check there if your issue has already been reported. If you
detailed and welcomed feedback. spotted it first, help us smashing it by providing a detailed and welcomed feedback.
# Credits # Credits

View File

@@ -2,18 +2,16 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
"name": "maintenance_project_task_domain", "name": "maintenance_project_task_domain",
"version": "16.0.1.0.0", "version": "18.0.1.0.0",
"author": "Elabore", "author": "Elabore",
"website": "https://elabore.coop", "website": "https://git.elabore.coop/elabore/maintenance-tools",
"maintainer": "Quentin Mondot", "maintainer": "Quentin Mondot",
"license": "AGPL-3", "license": "AGPL-3",
"category": "Tools", "category": "Tools",
"summary": "This module adjusts the domain applied to task field in order to limit " "summary": "This module adjusts the domain applied to task field in order to limit "
"selection to tasks in any of unfolded stages.", "selection to tasks in any of unfolded stages.",
"depends": ["maintenance_project"], "depends": ["maintenance_project"],
"data": [ "data": ["views/maintenance_view_form_task_domain.xml"],
"views/maintenance_view_form_task_domain.xml"
],
"installable": True, "installable": True,
"auto_install": True, "auto_install": True,
"application": False, "application": False,

View File

@@ -1,14 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record id="maintenance_view_form_task_domain" model="ir.ui.view"> <record id="maintenance_view_form_task_domain" model="ir.ui.view">
<field name="name">maintenance.form.task.domain</field> <field name="name">maintenance.form.task.domain</field>
<field name="model">maintenance.request</field> <field name="model">maintenance.request</field>
<field name="inherit_id" ref="maintenance_project.hr_equipment_request_view_form" /> <field
name="inherit_id"
ref="maintenance_project.hr_equipment_request_view_form"
/>
<field name="priority" eval="99" /> <field name="priority" eval="99" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='task_id' and @domain]" position="attributes"> <xpath expr="//field[@name='task_id' and @domain]" position="attributes">
<attribute <attribute
name="domain" name="domain"
>[('project_id', '=', project_id),('stage_id.fold', '=', False)]</attribute> >[('project_id', '=', project_id),('stage_id.fold', '=', False)]</attribute>
</xpath> </xpath>
</field> </field>

View File

@@ -1,44 +0,0 @@
======================================
maintenance_server_data
======================================
Gather several identification data about the servers to maintain.
Installation
============
Use Odoo normal module installation procedure to install
``maintenance_server_data``.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`_. In case of
trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Stéphan Sainléger
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,3 +1 @@
# -*- coding: utf-8 -*-
from . import models from . import models

View File

@@ -3,9 +3,9 @@
{ {
"name": "maintenance_server_data", "name": "maintenance_server_data",
"version": "16.0.1.0.0", "version": "18.0.1.0.0",
"author": "Elabore", "author": "Elabore",
"website": "https://elabore.coop", "website": "https://git.elabore.coop/elabore/maintenance-tools",
"maintainer": "Stéphan Sainléger", "maintainer": "Stéphan Sainléger",
"license": "AGPL-3", "license": "AGPL-3",
"category": "Tools", "category": "Tools",

View File

@@ -1,4 +1,4 @@
from odoo import fields, models, api from odoo import api, fields, models
class MaintenanceEquipment(models.Model): class MaintenanceEquipment(models.Model):

View File

@@ -1,17 +1,18 @@
from odoo import api, fields, models from odoo import api, fields, models
class OsDistribution(models.Model): class OsDistribution(models.Model):
_name = 'os.distribution' _name = "os.distribution"
name = fields.Char('Name', compute="_compute_name") name = fields.Char("Name", compute="_compute_name")
distrib_name = fields.Char('Distrib Name', required=True) distrib_name = fields.Char("Distrib Name", required=True)
distrib_version = fields.Char('Distrib Version') distrib_version = fields.Char("Distrib Version")
@api.depends("distrib_name","distrib_version") @api.depends("distrib_name", "distrib_version")
def _compute_name(self): def _compute_name(self):
for distrib in self: for distrib in self:
distrib.name = "" distrib.name = ""
if distrib.distrib_name != "": if distrib.distrib_name != "":
distrib.name = distrib.distrib_name distrib.name = distrib.distrib_name
if distrib.distrib_version != "": if distrib.distrib_version != "":
distrib.name = distrib.name + ' ' + distrib.distrib_version distrib.name = distrib.name + " " + distrib.distrib_version

View File

@@ -1,28 +1,31 @@
from odoo import fields, models from odoo import fields, models
class Service(models.Model):
_name = 'service'
name = fields.Char('Name', required=True) class Service(models.Model):
_name = "service"
name = fields.Char("Name", required=True)
class ServiceVersion(models.Model): class ServiceVersion(models.Model):
_name = "service.version" _name = "service.version"
service_id = fields.Many2one('service', string='Service', required=True) service_id = fields.Many2one("service", string="Service", required=True)
name = fields.Char('Name') name = fields.Char("Name")
is_last_version = fields.Boolean('Is Last Version?') is_last_version = fields.Boolean("Is Last Version?")
class ServiceInstance(models.Model): class ServiceInstance(models.Model):
_name = "service.instance" _name = "service.instance"
equipment_id = fields.Many2one('maintenance.equipment', string='Equipment') equipment_id = fields.Many2one("maintenance.equipment", string="Equipment")
service_id = fields.Many2one('service', string='Service', required=True) service_id = fields.Many2one("service", string="Service", required=True)
version_id = fields.Many2one('service.version', string='Version') version_id = fields.Many2one("service.version", string="Version")
service_url = fields.Char(string='Service Url') service_url = fields.Char(string="Service Url")
active = fields.Boolean(default=True) active = fields.Boolean(default=True)
class BackupServer(models.Model): class BackupServer(models.Model):
_name = 'backup.server' _name = "backup.server"
name = fields.Char('Name', required=True) name = fields.Char("Name", required=True)

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record id="equipment_view_form_server_inherit" model="ir.ui.view"> <record id="equipment_view_form_server_inherit" model="ir.ui.view">
<field name="name">equipment.form.server.inherit</field> <field name="name">equipment.form.server.inherit</field>
@@ -23,11 +23,14 @@
<xpath expr="//notebook" position="inside"> <xpath expr="//notebook" position="inside">
<page name="services" string="Services"> <page name="services" string="Services">
<field name="service_ids" nolabel="1"> <field name="service_ids" nolabel="1">
<tree create="true" delete="true" editable="top"> <list create="true" delete="true" editable="top">
<field name="service_id" /> <field name="service_id" />
<field name="version_id" domain="[('service_id', '=', service_id)]" /> <field
name="version_id"
domain="[('service_id', '=', service_id)]"
/>
<field name="service_url" /> <field name="service_url" />
</tree> </list>
</field> </field>
</page> </page>
</xpath> </xpath>
@@ -36,7 +39,7 @@
<record id="equipment_view_tree_server_inherit" model="ir.ui.view"> <record id="equipment_view_tree_server_inherit" model="ir.ui.view">
<field name="name">equipment.tree.server.inherit</field> <field name="name">equipment.list.server.inherit</field>
<field name="model">maintenance.equipment</field> <field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree" /> <field name="inherit_id" ref="maintenance.hr_equipment_view_tree" />
<field name="arch" type="xml"> <field name="arch" type="xml">

View File

@@ -1,20 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="os_distribution_view_tree" model="ir.ui.view"> <record id="os_distribution_view_tree" model="ir.ui.view">
<field name="name">os.distribution.view.tree</field> <field name="name">os.distribution.view.list</field>
<field name="model">os.distribution</field> <field name="model">os.distribution</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="OS Distributions" editable="top"> <list editable="top">
<field name="distrib_name" /> <field name="distrib_name" />
<field name="distrib_version" /> <field name="distrib_version" />
</tree> </list>
</field> </field>
</record> </record>
<record id="os_distribution_action" model="ir.actions.act_window"> <record id="os_distribution_action" model="ir.actions.act_window">
<field name="name">OS Distribution</field> <field name="name">OS Distribution</field>
<field name="res_model">os.distribution</field> <field name="res_model">os.distribution</field>
<field name="view_mode">tree</field> <field name="view_mode">list</field>
<field name="view_id" ref="os_distribution_view_tree" /> <field name="view_id" ref="os_distribution_view_tree" />
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
@@ -28,6 +28,7 @@
name="OS Distributions" name="OS Distributions"
parent="maintenance.menu_maintenance_configuration" parent="maintenance.menu_maintenance_configuration"
action="os_distribution_action" action="os_distribution_action"
sequence="3" /> sequence="3"
/>
</odoo> </odoo>

View File

@@ -1,47 +1,47 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<!-- VIEWS --> <!-- VIEWS -->
<record id="service_view_tree" model="ir.ui.view"> <record id="service_view_tree" model="ir.ui.view">
<field name="name">service.view.tree</field> <field name="name">service.view.list</field>
<field name="model">service</field> <field name="model">service</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Services" editable="top"> <list editable="top">
<field name="name" /> <field name="name" />
</tree> </list>
</field> </field>
</record> </record>
<record id="service_version_view_tree" model="ir.ui.view"> <record id="service_version_view_tree" model="ir.ui.view">
<field name="name">service.version.view.tree</field> <field name="name">service.version.view.list</field>
<field name="model">service.version</field> <field name="model">service.version</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Service versions" editable="top"> <list editable="top">
<field name="service_id" /> <field name="service_id" />
<field name="name" /> <field name="name" />
<field name="is_last_version" /> <field name="is_last_version" />
</tree> </list>
</field> </field>
</record> </record>
<record id="backup_server_view_tree" model="ir.ui.view"> <record id="backup_server_view_tree" model="ir.ui.view">
<field name="name">backup.server.view.tree</field> <field name="name">backup.server.view.list</field>
<field name="model">backup.server</field> <field name="model">backup.server</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Backup Servers" editable="top"> <list editable="top">
<field name="name" /> <field name="name" />
</tree> </list>
</field> </field>
</record> </record>
<record id="service_instance_view_tree" model="ir.ui.view"> <record id="service_instance_view_tree" model="ir.ui.view">
<field name="name">service.instance.view.tree</field> <field name="name">service.instance.view.list</field>
<field name="model">service.instance</field> <field name="model">service.instance</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <list>
<field name="equipment_id"/> <field name="equipment_id" />
<field name="service_id"/> <field name="service_id" />
<field name="version_id"/> <field name="version_id" />
</tree> </list>
</field> </field>
</record> </record>
@@ -50,17 +50,33 @@
<field name="model">service.instance</field> <field name="model">service.instance</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Service Instances"> <search string="Search Service Instances">
<field name="equipment_id"/> <field name="equipment_id" />
<field name="service_id"/> <field name="service_id" />
<field name="version_id"/> <field name="version_id" />
<field name="service_url"/> <field name="service_url" />
<separator/> <separator />
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/> <filter
<separator/> string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<separator />
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter string="Equipment" name="group_equipment" context="{'group_by': 'equipment_id'}"/> <filter
<filter string="Service" name="group_service" context="{'group_by': 'service_id'}"/> string="Equipment"
<filter string="Version" name="group_version" context="{'group_by': 'version_id'}"/> name="group_equipment"
context="{'group_by': 'equipment_id'}"
/>
<filter
string="Service"
name="group_service"
context="{'group_by': 'service_id'}"
/>
<filter
string="Version"
name="group_version"
context="{'group_by': 'version_id'}"
/>
</group> </group>
</search> </search>
</field> </field>
@@ -70,7 +86,7 @@
<record id="service_action" model="ir.actions.act_window"> <record id="service_action" model="ir.actions.act_window">
<field name="name">Service</field> <field name="name">Service</field>
<field name="res_model">service</field> <field name="res_model">service</field>
<field name="view_mode">tree</field> <field name="view_mode">list</field>
<field name="view_id" ref="service_view_tree" /> <field name="view_id" ref="service_view_tree" />
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
@@ -82,7 +98,7 @@
<record id="service_version_action" model="ir.actions.act_window"> <record id="service_version_action" model="ir.actions.act_window">
<field name="name">Service Version</field> <field name="name">Service Version</field>
<field name="res_model">service.version</field> <field name="res_model">service.version</field>
<field name="view_mode">tree</field> <field name="view_mode">list</field>
<field name="view_id" ref="service_version_view_tree" /> <field name="view_id" ref="service_version_view_tree" />
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
@@ -94,7 +110,7 @@
<record id="backup_server_action" model="ir.actions.act_window"> <record id="backup_server_action" model="ir.actions.act_window">
<field name="name">Backup server</field> <field name="name">Backup server</field>
<field name="res_model">backup.server</field> <field name="res_model">backup.server</field>
<field name="view_mode">tree</field> <field name="view_mode">list</field>
<field name="view_id" ref="backup_server_view_tree" /> <field name="view_id" ref="backup_server_view_tree" />
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
@@ -106,8 +122,8 @@
<record id="service_instance_action" model="ir.actions.act_window"> <record id="service_instance_action" model="ir.actions.act_window">
<field name="name">Services</field> <field name="name">Services</field>
<field name="res_model">service.instance</field> <field name="res_model">service.instance</field>
<field name="view_mode">tree</field> <field name="view_mode">list</field>
<field name="view_id" ref="service_instance_view_tree"/> <field name="view_id" ref="service_instance_view_tree" />
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Add a new Service Instance Add a new Service Instance
@@ -121,27 +137,31 @@
name="Services" name="Services"
parent="maintenance.menu_maintenance_configuration" parent="maintenance.menu_maintenance_configuration"
action="service_action" action="service_action"
sequence="4" /> sequence="4"
/>
<menuitem <menuitem
id="menu_maintenance_service_version" id="menu_maintenance_service_version"
name="Service Versions" name="Service Versions"
parent="maintenance.menu_maintenance_configuration" parent="maintenance.menu_maintenance_configuration"
action="service_version_action" action="service_version_action"
sequence="5" /> sequence="5"
/>
<menuitem <menuitem
id="menu_maintenance_backup_server" id="menu_maintenance_backup_server"
name="Backup Servers" name="Backup Servers"
parent="maintenance.menu_maintenance_configuration" parent="maintenance.menu_maintenance_configuration"
action="backup_server_action" action="backup_server_action"
sequence="5" /> sequence="5"
/>
<menuitem <menuitem
id="menu_maintenance_service_instance" id="menu_maintenance_service_instance"
name="Services" name="Services"
parent="maintenance.menu_maintenance_title" parent="maintenance.menu_maintenance_title"
action="service_instance_action" action="service_instance_action"
sequence="10"/> sequence="10"
/>
</odoo> </odoo>

View File

@@ -1,2 +0,0 @@
*.*~
*pyc

View File

@@ -1,44 +0,0 @@
======================================
maintenance_server_monitoring
======================================
Monitor some data on remote hosts
Installation
============
Use Odoo normal module installation procedure to install
``maintenance_server_monitoring``.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`_. In case of
trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Clément Thomas
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import models

View File

@@ -1,39 +0,0 @@
# Copyright 2023 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "maintenance_server_monitoring",
"version": "16.0.1.0.0",
"author": "Elabore",
"website": "https://elabore.coop",
"maintainer": "Clément Thomas",
"license": "AGPL-3",
"category": "Tools",
"summary": "Monitor some data on remote hosts",
# any module necessary for this one to work correctly
"depends": [
"base",
"maintenance",
"maintenance_server_ssh"
],
"qweb": [
# "static/src/xml/*.xml",
],
"external_dependencies": {
"python": [],
},
# always loaded
"data": [
"views/maintenance_equipment_views.xml",
"data/cron.xml",
],
# only loaded in demonstration mode
"demo": [],
"js": [],
"css": [],
"installable": True,
# Install this module automatically if all dependency have been previously
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}

View File

@@ -1,12 +0,0 @@
<odoo>
<record id="ir_cron_server_monitoring" model="ir.cron">
<field name="name">Server Monitoring : check all equipments</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="state">code</field>
<field name="code">model.cron_monitoring_test()</field>
<field name="interval_number">1</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
</record>
</odoo>

View File

@@ -1 +0,0 @@
from . import maintenance_equipment

View File

@@ -1,372 +0,0 @@
from odoo import fields, models, api
import subprocess
import sys
import psutil
from io import StringIO
LOG_LIMIT = 100000
AVAILABLE_MEMORY_PERCENT_COMMAND = "free | grep Mem | awk '{print $3/$2 * 100.0}'"
MIN_AVAILABLE_MEMORY_PERCENT_WARNING = 20
MIN_AVAILABLE_MEMORY_PERCENT_ERROR = 5
USED_DISK_SPACE_COMMAND = "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 5 -d ' ' | cut -f 1 -d %"
MAX_USED_DISK_SPACE_WARNING = 70
MAX_USED_DISK_SPACE_ERROR = 90
MAX_PING_MS_WARNING = 1000
MAX_PING_MS_ERROR = 5000
"""
if you want to add a new test :
* add new field to MaintenanceEquipment (named {fieldname} below)
* add a new function named test_{fieldname} which return a filled MonitoringTest class with :
-> log = logs you want to appear in logs
-> result = value which will be set to {fieldname}
-> error = MonitoringTest.ERROR or MonitoringTest.WARNING to generate maintenance request
** Note you can use test_ok, test_warning, and test_error functions to simplify code **
* add requirements if necessary in install_dependencies function
* call your function in monitoring_test() with a simple launch_test({fieldname}, *args)
if needed, *args can be passed by parameters to your test function
"""
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
last_monitoring_test_date = fields.Datetime('Date of last monitoring test', readonly=True)
#tests
ping_ok = fields.Boolean("Ping ok", readonly=True)
available_memory_percent = fields.Float('Percent of available memory', readonly=True)
used_disk_space = fields.Float('Percent of used disk space', readonly=True)
ssh_ok = fields.Boolean("SSH ok", readonly=True)
#log
log = fields.Html("Log", readonly=True)
#maintenance requests
error_maintenance_request = fields.Many2one('maintenance.request', "Error maintenance request")
warning_maintenance_request = fields.Many2one('maintenance.request', "Warning maintenance request")
class MonitoringTest:
"""Class to make the tests
"""
WARNING = "warning"
ERROR = "error"
def __init__(self, name):
self.name = name # name of the test
self.result = 0 # result of the test
self.log = "" # logs of the test
self.date = fields.Datetime.now() # date of the test
self.error = "" # errors of the test
def add_to_log(self, text):
"""
add a new line to logs composed with DATE > TEST NAME > WHAT TO LOG
"""
self.log += f"{self.date} > {self.name} > {text}\n"
def test_ok(self, result, log):
"""to call when the test is ok.
It just fill the test with result and embellished log
Args:
result: result of test
log (string): what to log
Returns:
MonitoringTest: filled test
"""
self.add_to_log(log)
self.result = result
return self
def test_error(self, result, log):
"""to call when test error.
It just fill the test with result, embellished log and set error value to ERROR
Args:
result: result of test
log (string): what to log
Returns:
MonitoringTest: filled test
"""
self.add_to_log(f"🚨 ERROR : {log}")
self.result = result
self.error = self.ERROR
return self
def test_warning(self, result, log):
"""to call when test warning.
It just fill the test with result, embellished log and set error value to WARNING
Args:
result: result of test
log (string): what to log
Returns:
MonitoringTest: filled test
"""
self.add_to_log(f"🔥 WARNING : {log}")
self.result = result
self.error = self.WARNING
return self
@api.model
def cron_monitoring_test(self):
"""cron launch test on all equipments
"""
self.search([]).monitoring_test()
def monitoring_test(self):
def launch_test(attribute, *test_function_args):
"""run test function with name = test_[attribute]
associate result of test to equipment
write logs of test
Args:
attribute (string): attribute of MaintenanceEquipment we want to test
Returns:
MonitoringTest: returned by test function
"""
test_function = getattr(equipment,"test_"+attribute)
test = test_function(*test_function_args)
setattr(equipment, attribute, test.result)
log.write(test.log)
tests.append(test)
return test
for equipment in self:
# we use StingIO instead of string to use mutable object
log = StringIO()
# array of all tests
tests = []
# install dependencies and log it
log.write(equipment.install_dependencies().log) # launch_test is not used, only logs are necessary
# run ping test
launch_test("ping_ok")
# SSH dependant test
ssh = launch_test("ssh_ok").result
if ssh:
# test available memory
launch_test("available_memory_percent", ssh)
# test disk usage
launch_test("used_disk_space", ssh)
else:
equipment.available_memory_percent = -1 #set -1 by convention if error
equipment.used_disk_space = -1 #set -1 by convention if error
# set test date
equipment.last_monitoring_test_date = fields.Datetime.now()
# write logs
log.seek(0) #log is a StringIO so seek to beginning before read
new_log = f'📣 {fields.Datetime.now()}\n{log.read()}\n'
new_log = new_log.replace("\n","<br />") # log field is HTML, so format lines
equipment.log = f'{new_log}<br />{equipment.log}'[:LOG_LIMIT] #limit logs
# if error create maintenance request
error = warning =False
if any(test.error == test.ERROR for test in tests):
error = True # if any arror in tests
elif any(test.error == test.WARNING for test in tests):
warning = True # if any warning in tests
if error or warning:
# check if error or warning request (not done) already exists before creating a new one
# if only a warning request exists, error request will be created anyway
existing_not_done_error_request = None
existing_not_done_warning_request = None
if equipment.error_maintenance_request and not equipment.error_maintenance_request.stage_id.done:
existing_not_done_error_request = equipment.error_maintenance_request
if equipment.warning_maintenance_request and not equipment.warning_maintenance_request.stage_id.done:
existing_not_done_warning_request = equipment.warning_maintenance_request
if (error and not existing_not_done_error_request) \
or (warning and not existing_not_done_warning_request and not existing_not_done_error_request):
maintenance_request = self.env['maintenance.request'].create({
"name":f'[{"ERROR" if error else "WARNING"}] {equipment.name}',
"equipment_id":equipment.id,
"employee_id":equipment.employee_id,
"user_id":equipment.technician_user_id,
"maintenance_team_id":equipment.maintenance_team_id.id or self.env["maintenance.team"].search([], limit=1),
"priority":'2' if error else '3',
"maintenance_type":"corrective" if error else "preventive",
"description":new_log
})
if error:
equipment.error_maintenance_request = maintenance_request
else:
equipment.warning_maintenance_request = maintenance_request
def install_dependencies(self):
"""
install dependencies needed to do all tests, as python or shell programs
Returns:
MonitoringTest: representing current test with result=0 if not error
"""
monitoring_test = self.MonitoringTest("install dependencies")
if "ping3" in sys.modules:
return monitoring_test.test_ok(0, "ping3 already installed")
else:
try:
command = ['pip','install',"ping3"]
response = subprocess.call(command) # run "pip install ping3" command
if response == 0:
return monitoring_test.test_ok(0, "ping3 installation successful")
else:
monitoring_test.test_error(f"ping3 : unable to install : response = {response}")
except Exception as e:
return monitoring_test.test_error(f"ping3 : unable to install : {e}")
def test_ssh_ok(self):
"""
test ssh with maintenance_server_ssh module
Returns:
MonitoringTest: representing current test with :
* result = False if error
* result = ssh connection if no error
* error = MonitoringTest.ERROR if connection failed
* log file
"""
test = self.MonitoringTest("SSH OK")
try:
# SSH connection ok : set ssh connection in result, converted in boolean (True) when set in ssh_ok field
return test.test_ok(self.get_ssh_connection(), "SSH Connection OK") #ssh connection given by maintenance_server_ssh module
except Exception as e:
# SSH connection failed
return test.test_error(False, f"{fields.Datetime.now()} > SSH > connection failed {e}\n")
def test_available_memory_percent(self, ssh):
"""
test available memory with a bash command called by ssh
Args:
ssh (paramiko.SSHClient): ssh client
Returns:
MonitoringTest: representing current test with :
* result = -2 if error
* result = percent of available memory if no error
* error defined with MonitoringTest.ERROR or MonitoringTest.WARNING depending on result comparaison
with MIN_AVAILABLE_MEMORY_PERCENT_WARNING and MIN_AVAILABLE_MEMORY_PERCENT_ERROR
* log file
"""
try:
test = self.MonitoringTest("Available memory percent")
_stdin, stdout, _stderr = ssh.exec_command(AVAILABLE_MEMORY_PERCENT_COMMAND)
available_memory_percent = float(stdout.read().decode())
if available_memory_percent > MIN_AVAILABLE_MEMORY_PERCENT_WARNING:
return test.test_ok(available_memory_percent, f"{available_memory_percent}% available")
elif available_memory_percent > MIN_AVAILABLE_MEMORY_PERCENT_ERROR:
# memory between warning and error step
return test.test_warning(available_memory_percent, f"{available_memory_percent}% available (<{MIN_AVAILABLE_MEMORY_PERCENT_WARNING})")
else:
# memory available lower than error step
return test.test_error(available_memory_percent, f"{available_memory_percent}% available (<{MIN_AVAILABLE_MEMORY_PERCENT_ERROR})")
except Exception as e:
return test.test_error(-2, f"{e}")
def test_used_disk_space(self, ssh):
"""
test Used disk space with a bash command called by ssh
Args:
ssh (paramiko.SSHClient): ssh client
Returns:
MonitoringTest: representing current test with :
* result = -2 if error
* result = percent of Used disk space if no error
* error defined with MonitoringTest.ERROR or MonitoringTest.WARNING depending on result comparaison
with MAX_USED_DISK_SPACE_WARNING and MAX_USED_DISK_SPACE_ERROR
* log file
"""
try:
test = self.MonitoringTest("Used disk space")
_stdin, stdout, _stderr = ssh.exec_command(USED_DISK_SPACE_COMMAND)
used_disk_space = float(stdout.read().decode())
if used_disk_space < MAX_USED_DISK_SPACE_WARNING:
return test.test_ok(used_disk_space, f"{used_disk_space}% used")
elif used_disk_space < MAX_USED_DISK_SPACE_ERROR:
# disk usage between WARNING and ERROR steps
return test.test_warning(used_disk_space, f"{used_disk_space}% used (>{MAX_USED_DISK_SPACE_WARNING})")
else:
# disk usage higher than ERROR steps
return test.test_error(used_disk_space, f"{used_disk_space}% used (>{MAX_USED_DISK_SPACE_ERROR})")
except Exception as e:
return test.test_error(-2, f"{e}")
def test_ping_ok(self):
"""
test PING with ping3 library
Returns:
MonitoringTest: representing current test with :
* result = False if error
* result = True if no error
* error defined with MonitoringTest.ERROR or MonitoringTest.WARNING depending on ping time comparaison
with MAX_PING_MS_WARNING and MAX_PING_MS_ERROR
* log file
"""
test = self.MonitoringTest("Ping")
try:
from ping3 import ping
except Exception as e:
# unable to import ping3
return test.test_error(False, f"ping3 dependencie not satisfied : {e}")
hostname = self.server_domain
if not hostname:
# equipment host name not filled
return test.test_error(False, f"host name seems empty !")
try:
r = ping(hostname)
except Exception as e:
# Any problem when call ping
return test.test_error(False, f"unable to call ping ! > {e}")
if r:
test.result = True
ping_ms = int(r*1000)
if ping_ms < MAX_PING_MS_WARNING:
# ping OK
return test.test_ok(True, f"PING OK in {ping_ms} ms")
elif ping_ms < MAX_PING_MS_ERROR:
# ping result between WARNING and ERROR => WARNING
return test.test_warning(True, f"PING OK in {ping_ms}ms (> {MAX_PING_MS_WARNING})")
else:
# ping result higher than ERROR => ERROR
return test.test_error(False, f"PING OK in {ping_ms}ms (> {MAX_PING_MS_ERROR})")
else:
return test.test_error(False, "PING FAILED")

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="equipment_view_form_server_inherit" model="ir.ui.view">
<field name="name">equipment.form.server.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form" />
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page name="monitoring" string="Monitoring">
<group name="monitoring_test" string="Test">
<field name="last_monitoring_test_date" />
<field name="ping_ok" />
<field name="ssh_ok" />
<field name="available_memory_percent" />
<field name="used_disk_space" />
<button name="monitoring_test" type="object" string="Test" />
</group>
<group name="monitoring_log" string="Log">
<field name="log" />
</group>
</page>
</xpath>
</field>
</record>
<record id="equipment_view_tree_server_inherit" model="ir.ui.view">
<field name="name">equipment.tree.server.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='category_id']" position="after">
<field name="ping_ok" optional="hide" />
<field name="ssh_ok" />
<field name="available_memory_percent" optional="hide" />
<field name="used_disk_space" optional="hide" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,2 +0,0 @@
*.*~
*pyc

View File

@@ -1,44 +0,0 @@
======================================
maintenance_server_ssh
======================================
Create an SSH remote connection for maintenance equipment, usable for other modules
Installation
============
Use Odoo normal module installation procedure to install
``maintenance_server_ssh``.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`_. In case of
trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Clément Thomas
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import models

View File

@@ -1,37 +0,0 @@
# Copyright 2023 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "maintenance_server_ssh",
"version": "16.0.1.0.0",
"author": "Elabore",
"website": "https://elabore.coop",
"maintainer": "Clément Thomas",
"license": "AGPL-3",
"category": "Tools",
"summary": "Monitor some data on remote hosts",
# any module necessary for this one to work correctly
"depends": [
"base",
"maintenance",
],
"qweb": [
# "static/src/xml/*.xml",
],
"external_dependencies": {
"python": [],
},
# always loaded
"data": [
"views/maintenance_equipment_views.xml",
],
# only loaded in demonstration mode
"demo": [],
"js": [],
"css": [],
"installable": True,
# Install this module automatically if all dependency have been previously
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}

View File

@@ -1 +0,0 @@
from . import maintenance_equipment

View File

@@ -1,20 +0,0 @@
from odoo import fields, models
import subprocess
import sys
import psutil
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
server_domain = fields.Char('Server Domain')
ssh_private_key_path = fields.Char("SSH private key path", default="/opt/odoo/auto/dev/ssh_keys/id_rsa")
def get_ssh_connection(self):
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(self.server_domain, username="root", key_filename=self.ssh_private_key_path)
return ssh

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="equipment_view_form_server_inherit" model="ir.ui.view">
<field name="name">equipment.form.server.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form" />
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page name="ssh" string="SSH">
<group name="ssh_connection" string="SSH Connection">
<field name="server_domain" />
<field name="ssh_private_key_path" />
</group>
</page>
</xpath>
</field>
</record>
<record id="equipment_view_tree_server_inherit" model="ir.ui.view">
<field name="name">equipment.tree.server.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='category_id']" position="after">
<field name="server_domain" optional="hide" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -3,6 +3,7 @@
## Contexte codebase ## Contexte codebase
### Architecture des modules ### Architecture des modules
``` ```
maintenance_server_data (base, maintenance) maintenance_server_data (base, maintenance)
├── Définit: service, service.version, service.instance, backup.server ├── Définit: service, service.version, service.instance, backup.server
@@ -22,51 +23,64 @@ maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
``` ```
### Modèle service.instance actuel (dans maintenance_server_data) ### Modèle service.instance actuel (dans maintenance_server_data)
- equipment_id (Many2one → maintenance.equipment) - equipment_id (Many2one → maintenance.equipment)
- service_id (Many2one → service, required) - service_id (Many2one → service, required)
- version_id (Many2one → service.version) - version_id (Many2one → service.version)
- service_url (Char) ← URL déjà existante, parfait pour les checks HTTP - service_url (Char) ← URL déjà existante, parfait pour les checks HTTP
### Pattern de création maintenance.request existant ### Pattern de création maintenance.request existant
- Vérifie si une request non-done existe déjà avant d'en créer une nouvelle - Vérifie si une request non-done existe déjà avant d'en créer une nouvelle
- Stocke la référence sur equipment: error_maintenance_request / warning_maintenance_request - Stocke la référence sur equipment: error_maintenance_request /
warning_maintenance_request
- Assignation auto: employee_id, technician_user_id, maintenance_team_id - Assignation auto: employee_id, technician_user_id, maintenance_team_id
## Requirements confirmés (depuis les specs) ## Requirements confirmés (depuis les specs)
- [x] Modifier le module `maintenance_server_monitoring` (pas de nouveau module) - [x] Modifier le module `maintenance_server_monitoring` (pas de nouveau module)
- [x] Ajouter `maintenance_server_data` aux dépendances (nécessaire pour accéder à service.instance) - [x] Ajouter `maintenance_server_data` aux dépendances (nécessaire pour accéder à
- [x] Étendre `service.instance` avec: last_status_code (Integer), last_check_date (Datetime), status OK/KO (Boolean) service.instance)
- [x] Nouvelle vue liste standalone pour service.instance (nom, version, URL, date check, status code, statut) - [x] Étendre `service.instance` avec: last_status_code (Integer), last_check_date
(Datetime), status OK/KO (Boolean)
- [x] Nouvelle vue liste standalone pour service.instance (nom, version, URL, date
check, status code, statut)
- [x] Paramètres module: fréquence vérification + durée mode maintenance - [x] Paramètres module: fréquence vérification + durée mode maintenance
- [x] Mode maintenance sur equipment: bool tracked + bandeau - [x] Mode maintenance sur equipment: bool tracked + bandeau
- [x] Si service KO → création maintenance.request (pas de doublon si toujours KO) - [x] Si service KO → création maintenance.request (pas de doublon si toujours KO)
## Décision architecturale : NOUVEAU MODULE ## Décision architecturale : NOUVEAU MODULE
**Nom**: `maintenance_service_http_monitoring` **Nom**: `maintenance_service_http_monitoring` **Raison**: Séparation des préoccupations
**Raison**: Séparation des préoccupations — monitoring infra (SSH/ping) vs monitoring applicatif (HTTP) — monitoring infra (SSH/ping) vs monitoring applicatif (HTTP) **Dépendances**: `base`,
**Dépendances**: `base`, `maintenance`, `maintenance_server_data` `maintenance`, `maintenance_server_data` **PAS de dépendance** vers
**PAS de dépendance** vers `maintenance_server_ssh` ni `maintenance_server_monitoring` `maintenance_server_ssh` ni `maintenance_server_monitoring`
## Décisions techniques ## Décisions techniques
- Paramètres via res.config.settings + ir.config_parameter (pattern standard Odoo) - Paramètres via res.config.settings + ir.config_parameter (pattern standard Odoo)
- _inherit = 'service.instance' dans le nouveau module pour étendre le modèle - \_inherit = 'service.instance' dans le nouveau module pour étendre le modèle
- Cron dédié pour les checks HTTP (fréquence configurable) - Cron dédié pour les checks HTTP (fréquence configurable)
- _inherit = 'maintenance.equipment' pour ajouter maintenance_mode + http_maintenance_request - \_inherit = 'maintenance.equipment' pour ajouter maintenance_mode +
http_maintenance_request
- mail.thread déjà hérité par maintenance.equipment dans Odoo base → tracking fonctionne - mail.thread déjà hérité par maintenance.equipment dans Odoo base → tracking fonctionne
## Décisions utilisateur (interview) ## Décisions utilisateur (interview)
1. **Mode maintenance**: HTTP uniquement — le monitoring existant (ping, SSH, mémoire, disque) continue normalement 1. **Mode maintenance**: HTTP uniquement — le monitoring existant (ping, SSH, mémoire,
2. **Lien maintenance.request**: Sur maintenance.equipment — nouveau champ `http_maintenance_request` (Many2one). Si plusieurs services KO sur un même equipment, UNE seule request qui liste les services KO. disque) continue normalement
3. **Menu service list**: Sous Maintenance > principal, même niveau que Équipements et Demandes 2. **Lien maintenance.request**: Sur maintenance.equipment — nouveau champ
4. **Tests**: OUI — setup pytest-odoo + tests unitaires pour la logique HTTP check et création maintenance requests `http_maintenance_request` (Many2one). Si plusieurs services KO sur un même
equipment, UNE seule request qui liste les services KO.
3. **Menu service list**: Sous Maintenance > principal, même niveau que Équipements et
Demandes
4. **Tests**: OUI — setup pytest-odoo + tests unitaires pour la logique HTTP check et
création maintenance requests
## Scope boundaries ## Scope boundaries
### IN ### IN
- NOUVEAU module `maintenance_service_http_monitoring` - NOUVEAU module `maintenance_service_http_monitoring`
- Étendre service.instance avec champs monitoring HTTP - Étendre service.instance avec champs monitoring HTTP
- Cron dédié pour checks HTTP (fréquence configurable) - Cron dédié pour checks HTTP (fréquence configurable)
@@ -77,6 +91,7 @@ maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
- Setup pytest-odoo + tests unitaires - Setup pytest-odoo + tests unitaires
### OUT ### OUT
- Aucune modification de `maintenance_server_monitoring` ni de `maintenance_server_ssh` - Aucune modification de `maintenance_server_monitoring` ni de `maintenance_server_ssh`
- Pas de notification email (juste la maintenance.request) - Pas de notification email (juste la maintenance.request)
- Pas de dashboard / reporting - Pas de dashboard / reporting
@@ -84,9 +99,13 @@ maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
## Décisions Metis (post-gap-analysis) ## Décisions Metis (post-gap-analysis)
- Services orphelins (sans equipment_id): N'existent pas en pratique, filtrés par sécurité - Services orphelins (sans equipment_id): N'existent pas en pratique, filtrés par
- Récupération service KO→OK: Rien d'automatique, close manuelle de la maintenance.request sécurité
- Définition KO: Tout échec = KO (timeout, DNS, SSL, connexion refusée, code != 200). Log le détail. - Récupération service KO→OK: Rien d'automatique, close manuelle de la
- Timeout HTTP: Hardcodé (constante, comme les seuils existants dans maintenance_server_monitoring) maintenance.request
- Définition KO: Tout échec = KO (timeout, DNS, SSL, connexion refusée, code != 200).
Log le détail.
- Timeout HTTP: Hardcodé (constante, comme les seuils existants dans
maintenance_server_monitoring)
- `requests` library: Déclarer dans external_dependencies.python - `requests` library: Déclarer dans external_dependencies.python
- Chaque requests.get() DOIT avoir timeout= (pylintrc enforce external-request-timeout) - Chaque requests.get() DOIT avoir timeout= (pylintrc enforce external-request-timeout)

View File

@@ -1,58 +0,0 @@
======================================
maintenance_service_http_monitoring
======================================
Module that allows monitoring of different services by checking their HTTP 200 responses:
- Uses the `maintenance_server_data` service for each device.
If the service has a URL, a request is made.
- Adds maintenance mode for a device:
- Allows disabling HTTP checks for a specified time (defined in the cron job).
## Logic:
If a request fails and a maintenance task has already been created for the same day,
no new task is added.
The default values for the cron jobs are located in `data/cron.xml`.
Installation
============
Use Odoo normal module installation procedure to install
``maintenance_service_http_monitoring``.
Known issues / Roadmap
======================
None yet.
Bug Tracker
===========
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`_. In case of
trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Boris Gallet
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -1,6 +1,6 @@
{ {
"name": "maintenance_service_http_monitoring", "name": "maintenance_service_http_monitoring",
"version": "16.0.1.0.0", "version": "18.0.1.0.0",
"author": "Elabore", "author": "Elabore",
"license": "AGPL-3", "license": "AGPL-3",
"category": "Tools", "category": "Tools",
@@ -10,7 +10,7 @@
"data": [ "data": [
"data/cron.xml", "data/cron.xml",
"views/service_instance_views.xml", "views/service_instance_views.xml",
"views/maintenance_equipment_views.xml" "views/maintenance_equipment_views.xml",
], ],
"installable": True "installable": True,
} }

View File

@@ -1,23 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record id="ir_cron_http_service_monitoring" model="ir.cron"> <record id="ir_cron_http_service_monitoring" model="ir.cron">
<field name="name">HTTP Service Monitoring : check all services</field> <field name="name">HTTP Service Monitoring : check all services</field>
<field name="model_id" ref="maintenance_server_data.model_service_instance"/> <field name="model_id" ref="maintenance_server_data.model_service_instance" />
<field name="state">code</field> <field name="state">code</field>
<field name="code">model.cron_check_http_services()</field> <field name="code">model.cron_check_http_services()</field>
<field name="interval_number">15</field> <field name="interval_number">15</field>
<field name="interval_type">minutes</field> <field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall">False</field>
</record> </record>
<record id="ir_cron_maintenance_mode_expiry" model="ir.cron"> <record id="ir_cron_maintenance_mode_expiry" model="ir.cron">
<field name="name">HTTP Service Monitoring : deactivate expired maintenance mode</field> <field
<field name="model_id" ref="maintenance_server_data.model_maintenance_equipment"/> name="name"
>HTTP Service Monitoring : deactivate expired maintenance mode</field>
<field
name="model_id"
ref="maintenance_server_data.model_maintenance_equipment"
/>
<field name="state">code</field> <field name="state">code</field>
<field name="code">model.cron_deactivate_expired_maintenance_mode()</field> <field name="code">model.cron_deactivate_expired_maintenance_mode()</field>
<field name="interval_number">15</field> <field name="interval_number">15</field>
<field name="interval_type">minutes</field> <field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall">False</field>
</record> </record>
</odoo> </odoo>

View File

@@ -1,8 +1,10 @@
from datetime import timedelta from datetime import timedelta
from odoo import models, fields, api
from odoo import api, fields, models
class MaintenanceEquipment(models.Model): class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment' _inherit = "maintenance.equipment"
maintenance_mode = fields.Boolean( maintenance_mode = fields.Boolean(
string="Maintenance Mode", string="Maintenance Mode",
@@ -19,37 +21,48 @@ class MaintenanceEquipment(models.Model):
help="Computed from start + configured duration", help="Computed from start + configured duration",
) )
http_maintenance_request = fields.Many2one( http_maintenance_request = fields.Many2one(
'maintenance.request', "maintenance.request",
string="HTTP Maintenance Request", string="HTTP Maintenance Request",
readonly=True, readonly=True,
) )
def action_activate_maintenance_mode(self): def action_activate_maintenance_mode(self):
for rec in self: for rec in self:
duration = int(self.env['ir.config_parameter'].sudo().get_param( duration = int(
'maintenance_service_http_monitoring.maintenance_mode_duration', 4)) self.env["ir.config_parameter"]
.sudo()
.get_param(
"maintenance_service_http_monitoring.maintenance_mode_duration", 4
)
)
now = fields.Datetime.now() now = fields.Datetime.now()
rec.write({ rec.write(
'maintenance_mode': True, {
'maintenance_mode_start': now, "maintenance_mode": True,
'maintenance_mode_end': now + timedelta(hours=duration), "maintenance_mode_start": now,
}) "maintenance_mode_end": now + timedelta(hours=duration),
}
)
def action_deactivate_maintenance_mode(self): def action_deactivate_maintenance_mode(self):
for rec in self: for rec in self:
rec.write({ rec.write(
'maintenance_mode': False, {
'maintenance_mode_start': False, "maintenance_mode": False,
'maintenance_mode_end': False, "maintenance_mode_start": False,
}) "maintenance_mode_end": False,
}
)
@api.model @api.model
def cron_deactivate_expired_maintenance_mode(self): def cron_deactivate_expired_maintenance_mode(self):
now = fields.Datetime.now() now = fields.Datetime.now()
expired = self.search([ expired = self.search(
('maintenance_mode', '=', True), [
('maintenance_mode_end', '<=', now), ("maintenance_mode", "=", True),
]) ("maintenance_mode_end", "<=", now),
]
)
expired.action_deactivate_maintenance_mode() expired.action_deactivate_maintenance_mode()
def create_http_maintenance_request(self, ko_services): def create_http_maintenance_request(self, ko_services):
@@ -57,13 +70,13 @@ class MaintenanceEquipment(models.Model):
today = fields.Date.context_today(self) today = fields.Date.context_today(self)
name = f"[HTTP KO] {self.name}" name = f"[HTTP KO] {self.name}"
domain = [ domain = [
('name', '=', name), ("name", "=", name),
('equipment_id', '=', self.id), ("equipment_id", "=", self.id),
('maintenance_type', '=', 'corrective'), ("maintenance_type", "=", "corrective"),
('create_date', '>=', f"{today} 00:00:00"), ("create_date", ">=", f"{today} 00:00:00"),
('create_date', '<=', f"{today} 23:59:59"), ("create_date", "<=", f"{today} 23:59:59"),
] ]
existing = self.env['maintenance.request'].search(domain, limit=1) existing = self.env["maintenance.request"].search(domain, limit=1)
# Check if a task with same name already exist for the day, if its the case : skip # Check if a task with same name already exist for the day, if its the case : skip
if existing: if existing:
self.http_maintenance_request = existing.id self.http_maintenance_request = existing.id
@@ -72,26 +85,26 @@ class MaintenanceEquipment(models.Model):
if request and not request.stage_id.done: if request and not request.stage_id.done:
return request return request
vals = { vals = {
'name': name, "name": name,
'equipment_id': self.id, "equipment_id": self.id,
'priority': '2', "priority": "2",
'maintenance_type': 'corrective', "maintenance_type": "corrective",
'description': self._build_ko_services_description(ko_services), "description": self._build_ko_services_description(ko_services),
} }
if self.employee_id: if self.employee_id:
vals['employee_id'] = self.employee_id.id vals["employee_id"] = self.employee_id.id
if self.technician_user_id: if self.technician_user_id:
vals['user_id'] = self.technician_user_id.id vals["user_id"] = self.technician_user_id.id
if self.maintenance_team_id: if self.maintenance_team_id:
vals['maintenance_team_id'] = self.maintenance_team_id.id vals["maintenance_team_id"] = self.maintenance_team_id.id
else: else:
team = self.env['maintenance.team'].search([], limit=1) team = self.env["maintenance.team"].search([], limit=1)
if team: if team:
vals['maintenance_team_id'] = team.id vals["maintenance_team_id"] = team.id
request = self.env['maintenance.request'].create(vals) request = self.env["maintenance.request"].create(vals)
self.http_maintenance_request = request.id self.http_maintenance_request = request.id
return request return request
def _build_ko_services_description(self, ko_services): def _build_ko_services_description(self, ko_services):
lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services] lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services]
return '\n'.join(lines) return "\n".join(lines)

View File

@@ -1,6 +1,6 @@
import logging import logging
from datetime import datetime
from odoo import models, fields, api from odoo import api, fields, models
try: try:
import requests import requests
@@ -11,8 +11,9 @@ _logger = logging.getLogger(__name__)
HTTP_CHECK_TIMEOUT = 10 # seconds HTTP_CHECK_TIMEOUT = 10 # seconds
class ServiceInstance(models.Model): class ServiceInstance(models.Model):
_inherit = 'service.instance' _inherit = "service.instance"
last_http_status_code = fields.Integer( last_http_status_code = fields.Integer(
string="Last HTTP Status Code", string="Last HTTP Status Code",
@@ -34,36 +35,42 @@ class ServiceInstance(models.Model):
if not rec.service_url or not rec.equipment_id: if not rec.service_url or not rec.equipment_id:
continue continue
equipment = rec.equipment_id equipment = rec.equipment_id
if getattr(equipment, 'maintenance_mode', False): if getattr(equipment, "maintenance_mode", False):
continue continue
status_ok = False status_ok = False
status_code = -1 status_code = -1
now = fields.Datetime.now() now = fields.Datetime.now()
url = rec.service_url url = rec.service_url
if not url.lower().startswith("https://"): if not url.lower().startswith("https://"):
url = "https://" + url.lstrip("http://") url = "https://" + url.removeprefix("http://").removeprefix("HTTP://")
try: try:
response = requests.get(url, timeout=HTTP_CHECK_TIMEOUT) response = requests.get(url, timeout=HTTP_CHECK_TIMEOUT)
status_code = response.status_code status_code = response.status_code
status_ok = (status_code == 200) status_ok = status_code == 200
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
_logger.warning(f"HTTP check failed for %s: %s", rec.service_url, e) _logger.warning("HTTP check failed for %s: %s", rec.service_url, e)
rec.write({ rec.write(
'last_http_status_code': status_code, {
'last_http_check_date': now, "last_http_status_code": status_code,
'http_status_ok': status_ok, "last_http_check_date": now,
}) "http_status_ok": status_ok,
}
)
if not status_ok: if not status_ok:
# Delegate maintenance.request creation to equipment # Delegate maintenance.request creation to equipment
if hasattr(equipment, 'create_http_maintenance_request'): if hasattr(equipment, "create_http_maintenance_request"):
equipment.create_http_maintenance_request([rec]) equipment.create_http_maintenance_request([rec])
@api.model @api.model
def cron_check_http_services(self): def cron_check_http_services(self):
domain = [('active', '=', True), ('service_url', '!=', False), ('equipment_id', '!=', False)] domain = [
("active", "=", True),
("service_url", "!=", False),
("equipment_id", "!=", False),
]
services = self.search(domain) services = self.search(domain)
for service in services: for service in services:
equipment = service.equipment_id equipment = service.equipment_id
if getattr(equipment, 'maintenance_mode', False): if getattr(equipment, "maintenance_mode", False):
continue continue
service.check_http_status() service.check_http_status()

View File

@@ -1,41 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record id="view_equipment_form_http_monitoring" model="ir.ui.view"> <record id="view_equipment_form_http_monitoring" model="ir.ui.view">
<field name="name">maintenance.equipment.form.http.monitoring</field> <field name="name">maintenance.equipment.form.http.monitoring</field>
<field name="model">maintenance.equipment</field> <field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/> <field name="inherit_id" ref="maintenance.hr_equipment_view_form" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//sheet" position="before"> <xpath expr="//sheet" position="before">
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('maintenance_mode', '=', False)]}"> <div
Mode maintenance actif — Vérifications HTTP désactivées.<br/> class="alert alert-warning"
Fin prévue : <field name="maintenance_mode_end" readonly="1"/> role="alert"
invisible="not maintenance_mode"
>
Mode maintenance actif — Vérifications HTTP désactivées.<br />
Fin prévue : <field name="maintenance_mode_end" readonly="1" />
</div> </div>
</xpath> </xpath>
<xpath expr="//header" position="inside"> <xpath expr="//header" position="inside">
<button name="action_activate_maintenance_mode" type="object" string="Activer le mode maintenance" <button
attrs="{'invisible': [('maintenance_mode', '=', True)]}" class="oe_highlight"/> name="action_activate_maintenance_mode"
<button name="action_deactivate_maintenance_mode" type="object" string="Désactiver le mode maintenance" type="object"
attrs="{'invisible': [('maintenance_mode', '=', False)]}"/> string="Activer le mode maintenance"
invisible="maintenance_mode"
class="oe_highlight"
/>
<button
name="action_deactivate_maintenance_mode"
type="object"
string="Désactiver le mode maintenance"
invisible="not maintenance_mode"
/>
</xpath> </xpath>
<xpath expr="//notebook" position="inside"> <xpath expr="//notebook" position="inside">
<page string="HTTP Monitoring"> <page string="HTTP Monitoring">
<group> <group>
<field name="maintenance_mode"/> <field name="maintenance_mode" />
<field name="maintenance_mode_start"/> <field name="maintenance_mode_start" />
<field name="maintenance_mode_end"/> <field name="maintenance_mode_end" />
<field name="http_maintenance_request" readonly="1"/> <field name="http_maintenance_request" readonly="1" />
</group> </group>
</page> </page>
</xpath> </xpath>
</field> </field>
</record> </record>
<record id="view_equipment_tree_http_monitoring" model="ir.ui.view"> <record id="view_equipment_tree_http_monitoring" model="ir.ui.view">
<field name="name">maintenance.equipment.tree.http.monitoring</field> <field name="name">maintenance.equipment.list.http.monitoring</field>
<field name="model">maintenance.equipment</field> <field name="model">maintenance.equipment</field>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/> <field name="inherit_id" ref="maintenance.hr_equipment_view_tree" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//tree" position="inside"> <xpath expr="//list" position="inside">
<field name="maintenance_mode" optional="hide"/> <field name="maintenance_mode" optional="hide" />
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@@ -1,19 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<!-- Inherit from base tree view to add HTTP monitoring fields --> <!-- Inherit from base list view to add HTTP monitoring fields -->
<record id="service_instance_http_monitoring_tree" model="ir.ui.view"> <record id="service_instance_http_monitoring_tree" model="ir.ui.view">
<field name="name">service.instance.http.monitoring.tree</field> <field name="name">service.instance.http.monitoring.list</field>
<field name="model">service.instance</field> <field name="model">service.instance</field>
<field name="inherit_id" ref="maintenance_server_data.service_instance_view_tree"/> <field
name="inherit_id"
ref="maintenance_server_data.service_instance_view_tree"
/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree position="attributes"> <list position="attributes">
<attribute name="decoration-danger">http_status_ok == False</attribute> <attribute name="decoration-danger">http_status_ok == False</attribute>
</tree> </list>
<field name="version_id" position="after"> <field name="version_id" position="after">
<field name="service_url"/> <field name="service_url" />
<field name="last_http_check_date"/> <field name="last_http_check_date" />
<field name="last_http_status_code"/> <field name="last_http_status_code" />
<field name="http_status_ok"/> <field name="http_status_ok" />
</field> </field>
</field> </field>
</record> </record>
@@ -22,18 +25,33 @@
<record id="service_instance_http_monitoring_search" model="ir.ui.view"> <record id="service_instance_http_monitoring_search" model="ir.ui.view">
<field name="name">service.instance.http.monitoring.search</field> <field name="name">service.instance.http.monitoring.search</field>
<field name="model">service.instance</field> <field name="model">service.instance</field>
<field name="inherit_id" ref="maintenance_server_data.service_instance_view_search"/> <field
name="inherit_id"
ref="maintenance_server_data.service_instance_view_search"
/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="service_url" position="after"> <field name="service_url" position="after">
<field name="last_http_status_code" string="Status Code"/> <field name="last_http_status_code" string="Status Code" />
</field> </field>
<filter name="inactive" position="before"> <filter name="inactive" position="before">
<filter string="Status OK" name="status_ok" domain="[('http_status_ok', '=', True)]"/> <filter
<filter string="Status Error" name="status_error" domain="[('http_status_ok', '=', False)]"/> string="Status OK"
<separator/> name="status_ok"
domain="[('http_status_ok', '=', True)]"
/>
<filter
string="Status Error"
name="status_error"
domain="[('http_status_ok', '=', False)]"
/>
<separator />
</filter> </filter>
<filter name="group_version" position="after"> <filter name="group_version" position="after">
<filter string="Status Code" name="group_status_code" context="{'group_by': 'last_http_status_code'}"/> <filter
string="Status Code"
name="group_status_code"
context="{'group_by': 'last_http_status_code'}"
/>
</filter> </filter>
</field> </field>
</record> </record>