Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
{
'name': "Слои показателей - Смета",
'summary': """
Заготовленный блок показателей стоимостной сметы задач""",
'description': """
Модуль, добавляющий сквозные показатели, шаблоны показателей,
а также сводные отчеты по показателям в задачах проектов,
обеспечивая удобный и наглядный интерфейс для их анализа и редактирования.
""",
'author': "MK.Lab",
'website': "https://www.inf-centre.ru",
'category': 'Uncategorized',
'version': '19.0.2025.11.24',
'depends': ['base', 'mklab_project_task_indicators'],
'data': [
'security/ir.model.access.csv',
'data/data.xml',
'views/hg_templates.xml',
'views/hg_value.xml',
'views/project_task.xml',
'wizard/estimate_wizard.xml',
],
'demo': [
'demo/demo.xml',
],
'test': [
'tests/test_hg_templates.py',
'tests/test_estimate_wizard.py',
],
}

View File

@ -0,0 +1,37 @@
<odoo>
<data noupdate="0">
<record id="base_code_001" model="hg.index.code">
<field name="name">base_code_001</field>
</record>
<record id="work_design" model="hg.index">
<field name="name">Работы по проектированию</field>
<field name="internal_code_id" ref="mklab_base_indicators_extended.base_code_001"/>
<field name="external_code">EXT-DESIGN</field>
</record>
<record id="work_excavation" model="hg.index">
<field name="name">Подготовка котлована</field>
<field name="internal_code_id" ref="mklab_base_indicators_extended.base_code_001"/>
<field name="external_code">EXT-EXCAV</field>
</record>
<record id="work_foundation" model="hg.index">
<field name="name">Фундамент</field>
<field name="internal_code_id" ref="mklab_base_indicators_extended.base_code_001"/>
<field name="external_code">EXT-FOUND</field>
</record>
<record id="work_monolithic" model="hg.index">
<field name="name">Монолитные работы</field>
<field name="internal_code_id" ref="mklab_base_indicators_extended.base_code_001"/>
<field name="external_code">EXT-MONO</field>
</record>
<record id="work_finishing" model="hg.index">
<field name="name">Отделочные работы</field>
<field name="internal_code_id" ref="mklab_base_indicators_extended.base_code_001"/>
<field name="external_code">EXT-FINISH</field>
</record>
<record id="work_engineering" model="hg.index">
<field name="name">Инженерные работы</field>
<field name="internal_code_id" ref="mklab_base_indicators_extended.base_code_001"/>
<field name="external_code">EXT-ENG</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Шаблон показателей для сметы задачи -->
<record id="demo_hg_template_project_budget" model="hg.templates">
<field name="name">Смета проекта (базовая)</field>
</record>
<!-- Строки шаблона показателей -->
<record id="demo_hg_template_line_revenue" model="hg.templates.line">
<field name="template_id" ref="demo_hg_template_project_budget"/>
<field name="index_id" ref="mklab_base_indicators.demo_index_revenue"/>
<field name="date_due">2026-12-31</field>
<field name="value_float_plan">2000000.0</field>
</record>
<record id="demo_hg_template_line_cost" model="hg.templates.line">
<field name="template_id" ref="demo_hg_template_project_budget"/>
<field name="index_id" ref="mklab_base_indicators.demo_index_cost"/>
<field name="date_due">2026-12-31</field>
<field name="value_float_plan">1200000.0</field>
</record>
<record id="demo_hg_template_line_profit" model="hg.templates.line">
<field name="template_id" ref="demo_hg_template_project_budget"/>
<field name="index_id" ref="mklab_base_indicators.demo_index_profit"/>
<field name="date_due">2026-12-31</field>
<field name="value_float_plan">800000.0</field>
</record>
</data>
</odoo>

View File

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

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class HypergraphTemplates(models.Model):
_name = 'hg.templates'
name = fields.Char(string='Название')
line_ids = fields.One2many(comodel_name='hg.templates.line', inverse_name='template_id', string='Строки шаблона')
class HypergraphTemplatesLine(models.Model):
_name = 'hg.templates.line'
template_id = fields.Many2one(comodel_name='hg.templates', string='Шаблон', required=True, ondelete='cascade')
index_id = fields.Many2one('hg.index', string='Показатель', required=True)
date_due = fields.Date(string='Плановая дата')
value_float_plan = fields.Float(string='Плановая сумма')

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class ProjectTask(models.Model):
_inherit = 'project.task'
# Смета-анализ
def action_estimate_analysis(self):
context = dict(self.env.context or {})
domain = [('index_id', 'in', self.index_ids.ids)]
return {
'name': 'Смета-Анализ',
'type': 'ir.actions.act_window',
'res_model': 'hg.value',
'view_mode': 'pivot',
'target': 'new',
'views': [(self.env.ref('mklab_base_indicators_extended.view_estimate_pivot').id, 'pivot')],
'domain': domain,
'context': context,
}
# Править смету
def action_edit_estimate(self):
if self.index_ids:
context = dict(self.env.context or {})
domain = [('index_id', 'in', self.index_ids.ids)]
return {
'name': 'Править смету',
'type': 'ir.actions.act_window',
'res_model': 'hg.value',
'view_mode': 'list',
'target': 'new',
'views': [(self.env.ref('mklab_base_indicators_extended.view_edit_estimate_list').id, 'list')],
'domain': domain,
'context': context,
}
else:
return {
'name': 'Выбор шаблона сметы',
'type': 'ir.actions.act_window',
'res_model': 'estimate.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_template_id': False, 'active_id': self.id},
}

View File

@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mklab_hg_templates,mklab_base_indicators_extended.hg.templates,model_hg_templates,mklab_base_indicators.group_indicators_admin,1,1,1,1
access_mklab_hg_templates_line,mklab_base_indicators_extended.hg.templates.line,model_hg_templates_line,mklab_base_indicators.group_indicators_admin,1,1,1,1
access_mklab_hg_estimate_wizard,mklab_base_indicators_extended.estimate_wizard,model_estimate_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_hg_templates mklab_base_indicators_extended.hg.templates model_hg_templates mklab_base_indicators.group_indicators_admin 1 1 1 1
3 access_mklab_hg_templates_line mklab_base_indicators_extended.hg.templates.line model_hg_templates_line mklab_base_indicators.group_indicators_admin 1 1 1 1
4 access_mklab_hg_estimate_wizard mklab_base_indicators_extended.estimate_wizard model_estimate_wizard mklab_base_indicators.group_indicators_admin 1 1 1 1

View File

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

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class TestEstimateWizard(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.project = cls.env['project.project'].create({'name': 'Проект для сметы'})
cls.code = cls.env['hg.index.code'].create({'name': 'Код сметы'})
cls.index = cls.env['hg.index'].create({
'name': 'Показатель сметы',
'internal_code_id': cls.code.id,
})
cls.template = cls.env['hg.templates'].create({'name': 'Шаблон сметы'})
cls.env['hg.templates.line'].create({
'template_id': cls.template.id,
'index_id': cls.index.id,
'date_due': '2025-06-30',
'value_float_plan': 300000.0,
})
def _create_task(self, name='Задача сметы'):
return self.env['project.task'].create({
'name': name,
'project_id': self.project.id,
})
def test_confirm_action_creates_index_and_value(self):
task = self._create_task()
wizard = self.env['estimate.wizard'].with_context(active_id=task.id).create({
'template_id': self.template.id,
'code_id': self.code.id,
})
wizard.confirm_action()
# Должен создаться показатель, привязанный к узлу задачи
indexes = self.env['hg.index'].search([('node_id', '=', task.node_id.id)])
self.assertTrue(indexes, 'Показатель должен быть создан для узла задачи')
# Должно создаться значение для каждого показателя
for index in indexes:
values = self.env['hg.value'].search([('index_id', '=', index.id)])
self.assertTrue(values, 'Значение должно быть создано для показателя')
def test_confirm_action_value_plan_matches_template(self):
task = self._create_task('Задача проверки плана')
wizard = self.env['estimate.wizard'].with_context(active_id=task.id).create({
'template_id': self.template.id,
'code_id': self.code.id,
})
wizard.confirm_action()
indexes = self.env['hg.index'].search([('node_id', '=', task.node_id.id)])
value = self.env['hg.value'].search([('index_id', 'in', indexes.ids)], limit=1)
self.assertEqual(value.value_float_plan, 300000.0)
self.assertEqual(str(value.date_due), '2025-06-30')
def test_confirm_action_returns_close(self):
task = self._create_task('Задача закрытия')
wizard = self.env['estimate.wizard'].with_context(active_id=task.id).create({
'template_id': self.template.id,
'code_id': self.code.id,
})
result = wizard.confirm_action()
self.assertEqual(result['type'], 'ir.actions.act_window_close')
def test_confirm_action_without_active_id(self):
wizard = self.env['estimate.wizard'].create({
'template_id': self.template.id,
'code_id': self.code.id,
})
# Без active_id в контексте — должен вернуть None без ошибок
result = wizard.confirm_action()
self.assertIsNone(result)

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class TestHgTemplates(TransactionCase):
def setUp(self):
super().setUp()
self.code = self.env['hg.index.code'].create({'name': 'Код шаблона'})
self.index = self.env['hg.index'].create({
'name': 'Показатель шаблона',
'internal_code_id': self.code.id,
})
self.template = self.env['hg.templates'].create({'name': 'Тестовый шаблон'})
def test_template_create(self):
self.assertEqual(self.template.name, 'Тестовый шаблон')
self.assertFalse(self.template.line_ids)
def test_add_line_to_template(self):
line = self.env['hg.templates.line'].create({
'template_id': self.template.id,
'index_id': self.index.id,
'date_due': '2025-03-31',
'value_float_plan': 250000.0,
})
self.assertIn(line, self.template.line_ids)
self.assertEqual(line.value_float_plan, 250000.0)
self.assertEqual(line.index_id, self.index)
def test_multiple_lines(self):
index2 = self.env['hg.index'].create({'name': 'Второй показатель'})
self.env['hg.templates.line'].create({
'template_id': self.template.id,
'index_id': self.index.id,
'date_due': '2025-01-31',
'value_float_plan': 100000.0,
})
self.env['hg.templates.line'].create({
'template_id': self.template.id,
'index_id': index2.id,
'date_due': '2025-02-28',
'value_float_plan': 200000.0,
})
self.assertEqual(len(self.template.line_ids), 2)
def test_line_cascade_delete(self):
line = self.env['hg.templates.line'].create({
'template_id': self.template.id,
'index_id': self.index.id,
'date_due': '2025-01-31',
'value_float_plan': 50000.0,
})
line_id = line.id
self.template.unlink()
self.assertFalse(self.env['hg.templates.line'].browse(line_id).exists())

View File

@ -0,0 +1,60 @@
<odoo>
<record id="mklab_base_indicators_extended.hg_templates_line_list" model="ir.ui.view">
<field name="name">hg.templates.line.list</field>
<field name="model">hg.templates.line</field>
<field name="arch" type="xml">
<list>
<field name="index_id"/>
<field name="date_due"/>
<field name="value_float_plan"/>
</list>
</field>
</record>
<record id="mklab_base_indicators_extended.hypergraph_templates_view_list" model="ir.ui.view">
<field name="name">hg.templates.list</field>
<field name="model">hg.templates</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="line_ids"/>
</list>
</field>
</record>
<record id="mklab_base_indicators_extended.hypergraph_templates_view_form" model="ir.ui.view">
<field name="name">hg.templates.form</field>
<field name="model">hg.templates</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
</group>
<notebook>
<page name="value_line" string="Значения">
<field name="line_ids">
<list editable="bottom" create="true" delete="true">
<field name="index_id"/>
<field name="date_due"/>
<field name="value_float_plan"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mklab_base_indicators_extended.hg_templates_window" model="ir.actions.act_window">
<field name="name">Шаблон показателей</field>
<field name="res_model">hg.templates</field>
<field name="view_mode">list,form</field>
</record>
<menuitem name="Шаблоны показателей" id="hypergraph_base_shows_templates" parent="mklab_base_indicators.hypergraph_base_shows_list"
action="mklab_base_indicators_extended.hg_templates_window"/>
</odoo>

View File

@ -0,0 +1,45 @@
<odoo>
<record id="view_estimate_pivot" model="ir.ui.view">
<field name="name">Отчет по показателям</field>
<field name="model">hg.value</field>
<field name="arch" type="xml">
<pivot>
<field name="date_due" type="col" interval="week"/>
<field name="index_id" type="row"/>
<field name="value_float_actual" type="measure" operator="sum" string="Факт (Сумма)"/>
<field name="value_float_plan" type="measure" operator="sum" string="План (Сумма)"/>
</pivot>
</field>
</record>
<record id="view_edit_estimate_list" model="ir.ui.view">
<field name="name">Править смету</field>
<field name="model">hg.value</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="value_float_actual"/>
<field name="value_float_plan"/>
<field name="date_due"/>
<field name="index_id" string="Показатель"/>
<button name="calc" type="object" string="Вычислить"/>
</list>
</field>
</record>
<record id="mklab_base_indicators_extended.hg_reports_window" model="ir.actions.act_window">
<field name="name">Отчетность</field>
<field name="res_model">hg.value</field>
<field name="view_mode">list,form</field>
</record>
<record id="mklab_base_indicators_extended.hypergraph_view_estimate_pivot_window" model="ir.actions.act_window">
<field name="name">Отчет по показателям</field>
<field name="res_model">hg.value</field>
<field name="view_mode">pivot</field>
</record>
<menuitem name="Отчетность" id="hypergraph_reports" parent="mklab_base_indicators.hypergraph_base_menu_root"
action="mklab_base_indicators_extended.hg_reports_window"/>
<menuitem name="Отчет по показателям" id="hypergraph_reports_value" parent="hypergraph_reports"
action="mklab_base_indicators_extended.hypergraph_view_estimate_pivot_window"/>
</odoo>

View File

@ -0,0 +1,23 @@
<odoo>
<record id="mklab_view_task_form2_inherit_extended" model="ir.ui.view">
<field name="name">project.task.form.replace.hypergraph_page</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="mklab_project_task_indicators.mklab_view_task_form2_inherit"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='hypergraph_info']/h3" position="replace"/>
<xpath expr="//page[@name='hypergraph_info']/field[@name='index_ids']" position="replace"/>
</field>
</record>
<record id="mklab_view_task_form2_extended" model="ir.ui.view">
<field name="name">project.task.form.replace.hypergraph_page</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="header" position="inside">
<button name="action_estimate_analysis" type="object" string="Смета-анализ" class="btn-primary"/>
<button name="action_edit_estimate" type="object" string="Править смету" class="btn-primary"/>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

@ -0,0 +1,45 @@
from odoo import fields, models
class EstimateWizard(models.Model):
_name = "estimate.wizard"
template_id = fields.Many2one(comodel_name='hg.templates', string='Шаблон сметы', required=True)
code_id = fields.Many2one(comodel_name='hg.index.code', string='Код показателей', required=True)
def confirm_action(self):
self.ensure_one()
task = self.env.context.get('active_id') and self.env['project.task'].browse(self.env.context['active_id'])
if not task:
return
for line in self.template_id.line_ids:
node = self._get_node(res_model='project.task', res_id=task.id, name=task.name)
index = self.env['hg.index'].create({
'node_id': node.id,
'name': line.index_id.name,
'internal_code_id': self.code_id.id,
})
self.env['hg.value'].create({
'index_id': index.id,
'type': 'alone',
'date_due': line.date_due,
'value_float_plan': line.value_float_plan,
})
return {'type': 'ir.actions.act_window_close'}
def _get_node(self, res_model, res_id, name):
Node = self.env['hg.node']
node = Node.search([
('res_model', '=', res_model),
('res_id', '=', res_id)
], limit=1)
if not node:
node = Node.create({
'name': name,
'res_model': 'project.task',
'res_id': res_id,
})
return node

View File

@ -0,0 +1,18 @@
<odoo>
<record id="wizard_estimate_form" model="ir.ui.view">
<field name="name">wizard.estimate.form</field>
<field name="model">estimate.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="template_id"/>
<field name="code_id"/>
</group>
<footer>
<button name="confirm_action" string="Подтвердить" class="btn-primary" type="object" />
<button name="cancel" string="Отмена" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>