Public release from ruodoo-project: 19.0 - 2026-07-26 21:17:35 UTC

This commit is contained in:
CI Publish Bot
2026-07-26 21:17:45 +00:00
commit 4f7b594ec8
1335 changed files with 191620 additions and 0 deletions

View File

@ -0,0 +1,62 @@
# Слои показателей для задач проектов
name: mklab_project_task_indicators
version: 19.0.2025.11.17
author: MK.Lab
### 1. Общее описание
Модуль добавляет в задачи Odoo Project систему **слоёв показателей** — механизм для отслеживания числовых метрик (KPI) прямо внутри задачи. Каждой задаче можно назначить набор показателей с плановыми значениями и датами, а затем следить за их фактическим выполнением.
### 2. Установка
1. Скопируйте папку модуля mklab_project_task_indicators в директорию addons Odoo
2. Перезапустите сервер Odoo
3. Обновите список приложений: Настройки — Приложения — Обновить список
4. Найдите модуль через интерфейс администрирования и установите
5. Настройте права доступа для пользователей
> Внимание! Сначала должен быть установлен модуль `mklab_base_indicators`.
### 3. Функциональность
3.1 Основные возможности:
- Интегрировать задачи проекта в систему гиперграфных связей
- Отслеживать показатели эффективности задач
- Управлять связями между задачами и другими объектами системы
- Проводить комплексный анализ задач с учетом их взаимосвязей
### Пример использования
### 1. Работа с показателями в задаче
Откройте любую задачу в модуле **Проект**. В форме задачи появятся:
- Кнопка **«Показатели»** (stat button) — открывает список всех показателей задачи
- Блок **«Вершины»** — показывает связи задачи с другими объектами в системе показателей
![img_1.png](img_1.png)
### 2. Меню «Слои показателей»
В главном меню появляется раздел **Слои показателей**, где можно:
- Создавать **вершины** (узлы) — привязка к конкретному объекту (задаче, проекту и т.д.)
- Управлять **показателями** (`hg.index`) — именованные метрики
- Просматривать **значения** (`hg.value`) — конкретные числа с датами
![img_4.png](img_4.png)
### 3. Создание показателей для задачи
1. Откройте задачу и нажмите **«Показатели»**
2. Создайте показатель: укажите название, код, плановую дату и плановое значение
3. По мере выполнения вносите фактические значения
4. Смотрите динамику в сводной таблице
### 4. Печать сводной ведомости
В настройках **Проекта** (не задачи) доступна печать **«Сводная ведомость по проекту»** — отчёт со всеми задачами и их показателями, плановыми датами и значениями. В колонтитуле — дата печати и ответственный по проекту.
![img_2.png](img_2.png)

View File

@ -0,0 +1,133 @@
# Техническая документация: mklab_project_task_indicators
**name:** `mklab_project_task_indicators`
**version:** 19.0.2025.11.14
**author:** MK.Lab
**depends:** `base`, `project`, `mklab_base_indicators`
---
## Архитектура
Модуль подключает стандартную модель `project.task` к инфраструктуре гиперграфных показателей через `hg.hg_mixin`. Новых моделей данных не вводит — только регистрирует наследование и добавляет представления.
```
project.task
hg.hg_mixin (из mklab_base_indicators)
├── hg.node — вершина графа (привязка к res_model / res_id)
├── hg.index — показатель (именованная метрика)
└── hg.value — значение показателя (план / факт / дата)
```
---
## Модели
### `project.task` (расширение)
**Файл:** `models/project_task.py`
Регистрирует множественное наследование от `hg.hg_mixin`. После этого каждая задача при создании автоматически получает вершину графа (`hg.node`), к которой можно привязывать показатели и связи.
```python
class ProjectTask(models.Model):
_name = 'project.task'
_inherit = ['project.task', 'hg.hg_mixin']
```
Через `hg.hg_mixin` задача получает следующие поля:
| Поле | Тип | Описание |
|---|---|---|
| `node_id` | Many2one к `hg.node` | Вершина графа данной задачи |
| `index_ids` | Many2many к `hg.index` | Показатели, привязанные к вершине задачи |
| `related_ids` | Many2many к `hg.node` | Связанные вершины из матрицы связности |
Полное описание полей и методов миксина — в документации модуля `mklab_base_indicators`.
---
## Представления
### Вкладка «Связи и показатели» на форме задачи
**Файл:** `views/project_task_views.xml`
Расширяет стандартную форму задачи (`project.view_task_form2`). Вкладка добавляется после вкладки «Описание» и содержит три блока:
**Блок «Показатели»** — список `index_ids` с колонками:
| Колонка | Поле | Описание |
|---|---|---|
| Название | `name` | Название показателя |
| Внутренний код | `internal_code_id` | Классификатор |
| Внешний код | `external_code` | Код для интеграций |
| Текущее значение | `current_value` | Вычисленное актуальное значение |
| Кнопка «Вычислить» | `calc` (object) | Запускает пересчёт значения по формуле |
**Блок «Связи»** — список `related_ids` с колонками:
| Колонка | Поле | Описание |
|---|---|---|
| Название | `name` | Название связанной вершины |
| Модель | `res_model` | Техническое имя модели Odoo |
| Кнопка «Открыть» | `goto_related` (object) | Открывает связанную запись в диалоге |
**Поле «Вершина графа»**`node_id`, readonly, показывает привязанную вершину.
---
## Отчёт «Сводная ведомость по проекту»
**Файлы:** `reports/template.xml`, `reports/action.xml`
**Модель:** `project.project`
**ID шаблона:** `mklab_project_task_indicators.general_report`
Отчёт формируется из формы проекта и итерирует по `task_ids`. Структура вывода для каждой задачи:
- Строка-заголовок с названием задачи (серый фон) — выводится если у задачи больше одного показателя
- Для каждого показателя задачи (`index_ids`) выводятся строки значений (`value_ids`):
| Колонка | Поле | Формат |
|---|---|---|
| Наименование показателя | `index.name` | текст |
| На дату | `hg_value.date_due` | дд.мм.гггг |
| Плановый показатель | `hg_value.value_float_plan` | число |
**Колонтитул отчёта:**
| Позиция | Содержимое |
|---|---|
| Левый | Дата печати в формате дд.мм.гггг |
| Центр | Текст «Согласовано» |
| Правый | Ответственный по проекту (`user_id`) |
---
## Демо-данные
**Файл:** `demo/demo.xml`
Создаёт демо-задачи с привязанными вершинами и показателями. Использует показатели из демо-данных `mklab_base_indicators`: `demo_index_revenue`, `demo_index_cost`, `demo_index_profit`.
---
## Тесты
**Файл:** `tests/`
| Файл | Что покрывает |
|---|---|
| `test_project_task.py` | Создание задачи, автосоздание вершины графа, привязка показателей |
| `test_hg_mixin.py` | Работа полей и методов миксина на модели `project.task` |
---
## Связанные модули
| Модуль | Роль |
|---|---|
| `mklab_base_indicators` | Обязательная зависимость: содержит `hg.hg_mixin` и все `hg.*` модели |
| `mklab_base_indicators_extended` | Надстройка: шаблоны смет, визард создания показателей, кнопки «Смета-анализ» и «Править смету» |
| `mklab_base_indicators_report` | Граф-отчёт «Показатели по узлам» |

View File

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

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
{
'name': "Слои показателей для задач проектов",
'summary': """
Слои показателей для задач модуля project""",
'description': """
Слои показателей для задач модуля project
""",
'author': "MK.Lab",
'website': "https://www.inf-centre.ru",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '19.0.2025.11.14',
# any module necessary for this one to work correctly
'depends': ['base', 'mklab_base_indicators', 'project'],
# always loaded
'data': [
'views/project_task_views.xml',
'reports/action.xml',
'reports/template.xml'
],
'demo': [
'demo/demo.xml',
],
'test': [
'tests/test_project_task.py',
],
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Демо-проекты -->
<record id="demo_project_construction" model="project.project">
<field name="name">Строительство производственного склада (демо)</field>
</record>
<record id="demo_project_it" model="project.project">
<field name="name">Разработка корпоративной CRM-системы (демо)</field>
</record>
<!-- Задачи проекта (project.task наследует hg.hg_mixin, узел создаётся автоматически) -->
<record id="demo_task_design_docs" model="project.task">
<field name="name">Разработка проектной документации (демо)</field>
<field name="project_id" ref="demo_project_construction"/>
<field name="description">Подготовка полного комплекта проектной документации для строительства склада.</field>
</record>
<record id="demo_task_foundation" model="project.task">
<field name="name">Устройство монолитного фундамента (демо)</field>
<field name="project_id" ref="demo_project_construction"/>
<field name="description">Земляные работы, армирование и заливка монолитного фундамента.</field>
</record>
<record id="demo_task_roofing" model="project.task">
<field name="name">Монтаж кровельного покрытия (демо)</field>
<field name="project_id" ref="demo_project_construction"/>
<field name="description">Монтаж металлочерепицы и водосточной системы.</field>
</record>
<record id="demo_task_backend" model="project.task">
<field name="name">Разработка серверной части CRM (демо)</field>
<field name="project_id" ref="demo_project_it"/>
<field name="description">Проектирование и реализация REST API, интеграция с базой данных.</field>
</record>
<record id="demo_task_frontend" model="project.task">
<field name="name">Разработка пользовательского интерфейса CRM (демо)</field>
<field name="project_id" ref="demo_project_it"/>
<field name="description">Вёрстка и реализация клиентской части на основе утверждённых макетов.</field>
</record>
<record id="demo_task_testing" model="project.task">
<field name="name">Тестирование и приёмка системы (демо)</field>
<field name="project_id" ref="demo_project_it"/>
<field name="description">Функциональное и нагрузочное тестирование, устранение замечаний.</field>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

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

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
from odoo import models
class ProjectTask(models.Model):
_inherit = ['project.task', 'hg.hg_mixin']
_name = "project.task"

View File

@ -0,0 +1,15 @@
<odoo>
<data>
<record id="action_report_project" model="ir.actions.report">
<field name="name">Сводная ведомость по проекту</field>
<field name="model">project.project</field>
<field name="report_type">qweb-html</field>
<field name="report_name">mklab_project_task_indicators.general_report</field>
<field name="report_file">mklab_project_task_indicators.general_report</field>
<field name="binding_model_id" ref="project.model_project_project"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Ведомость для проекта '+str(object.name)</field>
<field name="attachment">'Ведомость для проекта '+str(object.name)</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,101 @@
<odoo>
<template id="mklab_project_task_indicators.general_report">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.basic_layout">
<div class="header">
<style>
.separator {
margin-bottom: 15px;
}
.gen-style {
border: 1px solid black;
text-align: center;
font-family: Arial, Helvetica, sans-serif;
}
</style>
<div class="container">
<div class="row gen-style">
<div class="col-12">
<h2>Сводная ведомость по проекту:</h2>
</div>
</div>
<div class="row separator gen-style">
<div class="col-12">
<h2 t-field="doc.sudo().name"/>
</div>
</div>
</div>
</div>
<div class="page">
<style>
.separator {
margin-bottom: 15px;
}
.gen-style {
border: 1px solid black;
text-align: center;
}
.font-arial {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
.task-name{
background-color:lightgray;
}
</style>
<div class="oe_structure"/>
<div class="container">
<div class="row gen-style">
<div class="col-4 font-arial">
Наименование показателя
</div>
<div class="col-4 font-arial">
На дату
</div>
<div class="col-4 font-arial">
Плановый показатель
</div>
</div>
<div t-foreach="doc.sudo().task_ids" t-as="task">
<t t-if="len(task.index_ids) > 1">
<div class="row gen-style">
<div class="col-12 font-arial task-name">
<span t-field="task.sudo().name"/>
</div>
</div>
</t>
<t t-foreach="task.sudo().index_ids" t-as="value">
<div class="col-4 font-arial">
<span t-field="value.sudo().name"/>
</div>
<t t-foreach="value.sudo().value_ids" t-as="h_values">
<div class="row separator">
<div class="col-4 font-arial"/>
<div class="col-4 font-arial">
<span t-field="h_values.sudo().date_due" t-options="{&quot;widget&quot;: &quot;date&quot;,&quot;format&quot;: &quot;dd.MM.yyyy&quot;}"/>
</div>
<div class="col-4 font-arial">
<span t-field="h_values.sudo().value_float_plan"/>
</div>
</div>
</t>
</t>
</div>
</div>
</div>
<div class="footer">
<div class="container">
<div class="row">
<div class="col-4"><span t-esc="context_timestamp(datetime.datetime.now()).strftime('%d.%m.%Y')"/></div>
<div class="col-4">Согласовано</div>
<div class="col-4"><span t-field="doc.sudo().user_id"/></div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import test_project_task
from . import test_hg_mixin

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class TestHgMixin(TransactionCase):
"""
Тестируем mixin через project.task (наследует hg.hg_mixin).
Требует установленного модуля mklab_project_task_indicators.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.project = cls.env['project.project'].create({'name': 'Тестовый проект'})
def test_node_created_on_task_create(self):
task = self.env['project.task'].create({
'name': 'Задача с узлом',
'project_id': self.project.id,
})
self.assertTrue(task.node_id, 'При создании задачи должна создаваться вершина графа')
self.assertEqual(task.node_id.res_model, 'project.task')
self.assertEqual(task.node_id.res_id, task.id)
def test_node_name_matches_task(self):
task = self.env['project.task'].create({
'name': 'Задача для проверки имени',
'project_id': self.project.id,
})
self.assertEqual(task.node_id.name, 'Задача для проверки имени')
def test_index_ids_computed(self):
task = self.env['project.task'].create({
'name': 'Задача с показателями',
'project_id': self.project.id,
})
index = self.env['hg.index'].create({
'name': 'Показатель задачи',
'node_id': task.node_id.id,
})
task._compute_indexes()
self.assertIn(index, task.index_ids)
def test_related_ids_computed(self):
task_a = self.env['project.task'].create({
'name': 'Задача А',
'project_id': self.project.id,
})
task_b = self.env['project.task'].create({
'name': 'Задача Б',
'project_id': self.project.id,
})
self.env['hg.link'].create({
'name': 'Связь А → Б',
'source_id': task_a.node_id.id,
'target_ids': [(4, task_b.node_id.id)],
})
task_a._compute_related()
self.assertIn(task_b.node_id, task_a.related_ids)
def test_related_ids_empty_without_links(self):
task = self.env['project.task'].create({
'name': 'Изолированная задача',
'project_id': self.project.id,
})
task._compute_related()
self.assertFalse(task.related_ids)

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class TestProjectTaskIndicators(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.project = cls.env['project.project'].create({'name': 'Тестовый проект'})
def _create_task(self, name='Тестовая задача'):
return self.env['project.task'].create({
'name': name,
'project_id': self.project.id,
})
def test_task_inherits_mixin(self):
task = self._create_task()
self.assertTrue(hasattr(task, 'node_id'))
self.assertTrue(hasattr(task, 'index_ids'))
self.assertTrue(hasattr(task, 'related_ids'))
def test_node_auto_created(self):
task = self._create_task('Задача с автоузлом')
self.assertTrue(task.node_id)
self.assertEqual(task.node_id.res_model, 'project.task')
self.assertEqual(task.node_id.res_id, task.id)
def test_multiple_tasks_have_separate_nodes(self):
task_a = self._create_task('Задача 1')
task_b = self._create_task('Задача 2')
self.assertNotEqual(task_a.node_id, task_b.node_id)
def test_index_assigned_via_node(self):
task = self._create_task('Задача с показателем')
index = self.env['hg.index'].create({
'name': 'Показатель задачи',
'node_id': task.node_id.id,
})
task._compute_indexes()
self.assertIn(index, task.index_ids)
def test_no_indexes_without_node_assignment(self):
task = self._create_task('Задача без показателей')
task._compute_indexes()
other_node = self.env['hg.node'].create({
'name': 'Чужой узел',
'res_model': 'hg.node',
'res_id': 0,
})
index = self.env['hg.index'].create({
'name': 'Чужой показатель',
'node_id': other_node.id,
})
task._compute_indexes()
self.assertNotIn(index, task.index_ids)
def test_related_tasks_via_link(self):
task_a = self._create_task('Задача-источник')
task_b = self._create_task('Задача-приёмник')
self.env['hg.link'].create({
'name': 'Связь задач',
'source_id': task_a.node_id.id,
'target_ids': [(4, task_b.node_id.id)],
})
task_a._compute_related()
self.assertIn(task_b.node_id, task_a.related_ids)

View File

@ -0,0 +1,33 @@
<odoo>
<record id="mklab_view_task_form2_inherit" model="ir.ui.view">
<field name="name">Кастомизация формы задачи Project</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='description_page']" position="after">
<page name = "hypergraph_info" string="Связи и показатели">
<h3>Показатели</h3>
<field name="index_ids">
<list>
<field name="name"/>
<field name="internal_code_id"/>
<field name="external_code"/>
<field name="current_value"/>
<button name="calc" type="object" string="Вычислить"/>
</list>
</field>
<h3>Связи</h3>
<field name="related_ids">
<list>
<field name="name" string="Название"/>
<field name="res_model" string="Модель"/>
<button name="goto_related" type="object" string="Открыть"/>
</list>
</field>
<h4>Вершина графа</h4>
<field name="node_id" readonly="1"/>
</page>
</xpath>
</field>
</record>
</odoo>