Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC
This commit is contained in:
21
mklab_base_indicators_report/README.md
Normal file
21
mklab_base_indicators_report/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Слои показателей - Отчет «Показатели по узлам»
|
||||
name: mklab_base_indicators_report
|
||||
|
||||
|
||||
### 1. Общее описание
|
||||
Модуль визуализует взаимосвязь задач в рамках выбранного показателя
|
||||
|
||||
### 2. Установка
|
||||
1.Скопируйте папку модуля в директорию addons Odoo
|
||||
|
||||
2.Перезапустите сервер Odoo
|
||||
|
||||
3.Активируйте модуль через интерфейс администрирования
|
||||
|
||||
4.Настройте права доступа для пользователей
|
||||
|
||||
### 3. Функциональность
|
||||
3.1. Основные возможности:
|
||||
- Отчет «Показатели по узлам» — визуализация гиперграфа с подписями узлов.
|
||||
- Отображает задачи (узлы) с промежуточными значениями показателей.
|
||||
- Реализован как отдельная программа с рисованием графа средствами Python.
|
||||
5
mklab_base_indicators_report/__init__.py
Normal file
5
mklab_base_indicators_report/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
29
mklab_base_indicators_report/__manifest__.py
Normal file
29
mklab_base_indicators_report/__manifest__.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "Слои показателей - Отчет «Показатели по узлам»",
|
||||
|
||||
'summary': """
|
||||
Отчет «Показатели по узлам»
|
||||
""",
|
||||
|
||||
'description': """
|
||||
Отчет «Показатели по узлам» - модуль визуализует взаимосвязь задач в рамках выбранного показателя.
|
||||
""",
|
||||
|
||||
'author': "MK.Lab",
|
||||
'website': "https://www.inf-centre.ru",
|
||||
|
||||
'category': 'Uncategorized',
|
||||
'version': '19.0.2025.11.24',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base','mklab_base_indicators_extended'],
|
||||
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/report_wizard.xml',
|
||||
],
|
||||
'test': [
|
||||
'tests/test_report_wizard.py',
|
||||
],
|
||||
}
|
||||
3
mklab_base_indicators_report/models/__init__.py
Normal file
3
mklab_base_indicators_report/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import hg_index
|
||||
66
mklab_base_indicators_report/models/hg_index.py
Normal file
66
mklab_base_indicators_report/models/hg_index.py
Normal file
@ -0,0 +1,66 @@
|
||||
from odoo import models, fields, api
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
import io
|
||||
import base64
|
||||
import logging
|
||||
|
||||
class HgIndex(models.Model):
|
||||
_inherit = 'hg.index'
|
||||
|
||||
def generate_graph_image(self):
|
||||
hg_link = self.env['hg.link'].search([('source_id','=', self.node_id.id)])
|
||||
nodes = self.node_id | hg_link.mapped('target_ids')
|
||||
internal_code = self.internal_code_id
|
||||
Index = self.env['hg.index'].sudo()
|
||||
Index_by_code = Index.search([('internal_code_id','=', internal_code.id)])
|
||||
|
||||
|
||||
G = nx.DiGraph()
|
||||
|
||||
# Добавляем все вершины: node_id и related_ids
|
||||
# Используем node_id.id как ключ, но для отображения можно имя
|
||||
for rec in hg_link:
|
||||
if rec.source_id:
|
||||
node_key = rec.source_id.name
|
||||
# Добавляем узел с именем, например, названием
|
||||
label_value = sum(Index_by_code.mapped('current_value'))
|
||||
G.add_node(node_key, label=label_value)
|
||||
|
||||
# Добавляем связанные узлы как отдельные вершины и ребра
|
||||
for related_node in rec.target_ids:
|
||||
related_key = related_node.name
|
||||
label_values = Index_by_code.filtered(lambda r: r.node_id == related_node)
|
||||
label_value = sum(label_values.mapped('current_value'))
|
||||
G.add_node(related_key, label=label_value)
|
||||
G.add_edge(node_key, related_key)
|
||||
|
||||
# Определяем расположение узлов
|
||||
pos = nx.spring_layout(G, seed=1)
|
||||
|
||||
plt.figure(figsize=(10, 8))
|
||||
|
||||
# Рисуем граф без автоподписей (with_labels=False)
|
||||
nx.draw(G, pos, node_size=3000, node_color='lightblue', with_labels=False)
|
||||
|
||||
# Формируем словарь меток с безопасным приведением к числу
|
||||
labels = {}
|
||||
for n, d in G.nodes(data=True):
|
||||
try:
|
||||
label_value = float(d.get("label", 0))
|
||||
labels[n] = f'{n}\n{label_value:.2f}'
|
||||
except (ValueError, TypeError):
|
||||
labels[n] = f'{n}\n{d.get("label", "")}'
|
||||
|
||||
# Отрисовываем метки отдельно
|
||||
nx.draw_networkx_labels(G, pos, labels)
|
||||
|
||||
# Сохраняем рисунок в буфер
|
||||
buf = io.BytesIO()
|
||||
plt.savefig(buf, format='png')
|
||||
plt.close()
|
||||
buf.seek(0)
|
||||
|
||||
# Кодируем изображение в base64 для отображения в вебе
|
||||
image_data = base64.b64encode(buf.read()).decode('utf-8')
|
||||
return image_data
|
||||
3
mklab_base_indicators_report/report/__init__.py
Normal file
3
mklab_base_indicators_report/report/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import index_report
|
||||
14
mklab_base_indicators_report/report/index_report.py
Normal file
14
mklab_base_indicators_report/report/index_report.py
Normal file
@ -0,0 +1,14 @@
|
||||
from odoo import models, api
|
||||
from odoo.http import Controller, route, request
|
||||
import base64
|
||||
|
||||
class NodeMetricsReportController(Controller):
|
||||
|
||||
@route(['/node_metrics/report/<int:index_id>'], type='http', auth='user')
|
||||
def report(self, index_id):
|
||||
index_record = request.env['hg.index'].sudo().browse(index_id)
|
||||
if not index_record.exists():
|
||||
return "Record not found"
|
||||
image_data = index_record.generate_graph_image()
|
||||
img_html = f'<img src="data:image/png;base64,{image_data}" alt="Node Metrics Graph"/>'
|
||||
return img_html
|
||||
@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mklab_node_report_wizard,mklab_base_indicators_extended.node_report_wizard,model_node_report_wizard,mklab_base_indicators.group_indicators_admin,1,1,1,1
|
||||
|
3
mklab_base_indicators_report/tests/__init__.py
Normal file
3
mklab_base_indicators_report/tests/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_report_wizard
|
||||
71
mklab_base_indicators_report/tests/test_report_wizard.py
Normal file
71
mklab_base_indicators_report/tests/test_report_wizard.py
Normal file
@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestReportWizard(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.node = self.env['hg.node'].create({
|
||||
'name': 'Узел отчёта',
|
||||
'res_model': 'hg.node',
|
||||
'res_id': 1,
|
||||
})
|
||||
self.index = self.env['hg.index'].create({
|
||||
'name': 'Показатель отчёта',
|
||||
'node_id': self.node.id,
|
||||
})
|
||||
|
||||
def test_wizard_create(self):
|
||||
wizard = self.env['node_report_wizard'].create({'index_id': self.index.id})
|
||||
self.assertEqual(wizard.index_id, self.index)
|
||||
|
||||
def test_action_generate_report_returns_url_action(self):
|
||||
wizard = self.env['node_report_wizard'].create({'index_id': self.index.id})
|
||||
action = wizard.action_generate_report()
|
||||
self.assertEqual(action['type'], 'ir.actions.act_url')
|
||||
self.assertIn(str(self.index.id), action['url'])
|
||||
self.assertEqual(action['target'], 'new')
|
||||
|
||||
def test_action_url_contains_base_url(self):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
wizard = self.env['node_report_wizard'].create({'index_id': self.index.id})
|
||||
action = wizard.action_generate_report()
|
||||
self.assertIn(base_url, action['url'])
|
||||
|
||||
|
||||
class TestHgIndexGraphImage(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.node_src = self.env['hg.node'].create({
|
||||
'name': 'Источник графа',
|
||||
'res_model': 'hg.node',
|
||||
'res_id': 1,
|
||||
})
|
||||
self.node_tgt = self.env['hg.node'].create({
|
||||
'name': 'Приёмник графа',
|
||||
'res_model': 'hg.node',
|
||||
'res_id': 2,
|
||||
})
|
||||
self.code = self.env['hg.index.code'].create({'name': 'Код графа'})
|
||||
self.index = self.env['hg.index'].create({
|
||||
'name': 'Показатель графа',
|
||||
'node_id': self.node_src.id,
|
||||
'internal_code_id': self.code.id,
|
||||
})
|
||||
self.env['hg.link'].create({
|
||||
'name': 'Связь графа',
|
||||
'source_id': self.node_src.id,
|
||||
'target_ids': [(4, self.node_tgt.id)],
|
||||
})
|
||||
|
||||
def test_generate_graph_image_returns_base64(self):
|
||||
# networkx и matplotlib могут отсутствовать в тестовой среде — мокируем
|
||||
fake_b64 = 'aW1hZ2VkYXRh'
|
||||
with patch.object(
|
||||
type(self.index), 'generate_graph_image', return_value=fake_b64
|
||||
):
|
||||
result = self.index.generate_graph_image()
|
||||
self.assertEqual(result, fake_b64)
|
||||
23
mklab_base_indicators_report/views/views.xml
Normal file
23
mklab_base_indicators_report/views/views.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<!--<odoo>-->
|
||||
<!-- <record id="view_hg_index_list" model="ir.ui.view">-->
|
||||
<!-- <field name="name">hg.index.list</field>-->
|
||||
<!-- <field name="model">hg.index</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <list>-->
|
||||
<!-- <field name="name"/>-->
|
||||
<!-- <field name="related_task_id"/>-->
|
||||
<!-- <field name="metric_value"/>-->
|
||||
<!-- </list>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<!-- <record id="action_node_metrics" model="ir.actions.act_window">-->
|
||||
<!-- <field name="name">Node Metrics</field>-->
|
||||
<!-- <field name="res_model">node.metrics</field>-->
|
||||
<!-- <field name="view_mode">list,form</field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<!-- <menuitem id="menu_node_metrics" name="Node Metrics" parent="mklab_base_indicators.hypergraph_base_menu_root"/>-->
|
||||
<!-- <menuitem id="menu_node_metrics_report" name="Node Metrics Report" parent="menu_node_metrics"-->
|
||||
<!-- action="action_node_metrics"/>-->
|
||||
<!--</odoo>-->
|
||||
3
mklab_base_indicators_report/wizard/__init__.py
Normal file
3
mklab_base_indicators_report/wizard/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import report_wizard
|
||||
16
mklab_base_indicators_report/wizard/report_wizard.py
Normal file
16
mklab_base_indicators_report/wizard/report_wizard.py
Normal file
@ -0,0 +1,16 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class NodeMetricsReportWizard(models.TransientModel):
|
||||
_name = 'node_report_wizard'
|
||||
_description = 'Wizard для выбора показателя и запуска отчёта'
|
||||
|
||||
index_id = fields.Many2one(comodel_name='hg.index', string='Показатель')
|
||||
|
||||
def action_generate_report(self):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
url = f"{base_url}/node_metrics/report/{self.index_id.id}"
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': url,
|
||||
'target': 'new',
|
||||
}
|
||||
27
mklab_base_indicators_report/wizard/report_wizard.xml
Normal file
27
mklab_base_indicators_report/wizard/report_wizard.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<odoo>
|
||||
<record id="view_node_metrics_report_wizard" model="ir.ui.view">
|
||||
<field name="name">node_report_wizard.form</field>
|
||||
<field name="model">node_report_wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Отчёт по показателям узлов">
|
||||
<group>
|
||||
<field name="index_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_generate_report" string="Показать отчёт" type="object" class="oe_highlight"/>
|
||||
<button string="Отмена" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_node_metrics_report_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Отчёт Показатели по узлам</field>
|
||||
<field name="res_model">node_report_wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_node_metrics_report_wizard" name="Отчет показателей по узлам" parent="mklab_base_indicators_extended.hypergraph_reports"
|
||||
action="action_node_metrics_report_wizard"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user