Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC

This commit is contained in:
CI Publish Bot
2026-05-10 21:19:11 +00:00
commit cbf9e6e6d6
1213 changed files with 183945 additions and 0 deletions

View File

@ -0,0 +1,21 @@
# Слои показателей - Отчет «Показатели по узлам»
name: mklab_base_indicators_report
### 1. Общее описание
Модуль визуализует взаимосвязь задач в рамках выбранного показателя
### 2. Установка
1.Скопируйте папку модуля в директорию addons Odoo
2.Перезапустите сервер Odoo
3.Активируйте модуль через интерфейс администрирования
4.Настройте права доступа для пользователей
### 3. Функциональность
3.1. Основные возможности:
- Отчет «Показатели по узлам» — визуализация гиперграфа с подписями узлов.
- Отображает задачи (узлы) с промежуточными значениями показателей.
- Реализован как отдельная программа с рисованием графа средствами Python.

View File

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

View 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',
],
}

View File

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

View 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

View File

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

View 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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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

View File

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

View 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)

View 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>-->

View File

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

View 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',
}

View 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>