Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
====================================
РуОду — Аренда и коммунальные услуги
====================================
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
|badge1| |badge2|
Управление договорами аренды и коммунальных услуг: учёт объектов и
приборов учёта (счётчиков), ввод показаний по периодам и автоматическое
формирование счетов (``account.move``) — фиксированной суммой или по
расходу счётчика. Договор аренды связывается с российским договором
(``partner.contract.customer``), нативная бухгалтерия Odoo не затрагивается.
Порт коммерческого модуля ``comm_service`` (Odoo 17) на платформу РуОду
(Odoo 19) с согласованием с российской локализацией ``l10n_ru_*``.
**Содержание**
.. contents::
:local:
Возможности
===========
* Договоры аренды с привязкой к объектам (офисы, машиноместа и т. п.)
и статусами «Черновик / Действует / Не действует»
* Каталог объектов с признаком занятости и историей цен
* Приборы учёта (счётчики) и показания по периодам с контролем
корректности (год, месяц, монотонность показаний)
* Автоматическое формирование счетов за период:
* ежемесячно, ежеквартально или ежегодно
* фиксированной суммой либо по расходу счётчика (разница показаний)
* защита от повторного начисления за один и тот же период
* Мастер массового создания счетов по всем действующим договорам
* Мастер ввода показаний счётчиков за период
* Связь договора аренды с российским договором
(``partner.contract.customer``); поля периода и договора на счёте
* Печатная форма «Счёт по форме 1С» (устаревшая, см. ниже)
* Портал заказчика: отображение текущего долга
Зависимости
===========
* ``account`` — счета (``account.move``)
* ``l10n_ru_contract`` — российские договоры (``partner.contract.customer``),
реквизиты компании и контрагента (``chief_id``, ``inn``, ``kpp``)
* ``l10n_ru_contract_account`` — поле ``mt_contract_id`` и синхронизация
счёта с договором (``set_ons``)
Python-зависимости: ``pytils`` (суммы и даты прописью в печатной форме).
Использование
=============
#. Заведите типы объектов и объекты в *Аренда > Объекты*.
#. Создайте договор аренды в *Аренда > Договоры*: укажите заказчика,
компанию, объект и строки услуг (продукт, цена, период, при
необходимости — счётчик). Переведите договор в статус «Действует».
#. Вносите показания счётчиков вручную или через
*Аренда > Счётчики > Мастер ввода показаний*.
#. Формируйте счета кнопкой «Создать счета» в договоре или через
*Аренда > Договоры > Создание счетов* (массовый мастер по всем
действующим договорам).
#. При необходимости привяжите договор аренды к российскому договору
(кнопка «Договор не привязан. Создать?»).
Счета создаются в состоянии «Черновик»: их можно проверить и провести
штатными средствами Odoo. При проведении счёт попадает в РСБУ-слой через
коннектор документов (событие ``action_post``) — отдельных действий не
требуется.
Печатные формы
==============
Форма «Счёт по форме 1С» (``ruodoo_commservice.report_inv_1c``) помечена
**устаревшей**: печать в проекте переведена на DOCX. Форма скрыта из
интерфейса (сброшен ``binding_model_id``), но код сохранён и может быть
вызван программно. Планируемое удаление — в версии 20 (см.
``docs/research/11-qweb-reports-registry.md``).
Известные ограничения
=====================
* Учёт ведётся в рублях, ориентирован на российскую локализацию.
* Счёт дохода в строке счёта берётся из категории товара; если он не
задан, подставляется счёт по умолчанию из журнала продаж.
Тестирование
============
Модуль покрыт тестами ``TransactionCase`` (создание счетов по периодам и
по счётчикам, контроль дублей и показаний, мастера). Запуск::
docker compose run --rm odoo odoo -d <db> -i ruodoo_commservice \
--test-enable --test-tags=/ruodoo_commservice --stop-after-init
Авторы
======
* MK.Lab, RuOdoo
Модуль сопровождается MK.Lab, RuOdoo.
.. image:: https://ruodoo.ru/logo.png
:alt: MK.Lab, RuOdoo
:target: https://ruodoo.ru
Модуль является частью решения РуОду.

View File

@@ -0,0 +1,5 @@
from . import controllers
from . import models
from . import report_helper
from . import report

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
{
'name': 'РуОду — Аренда и коммунальные услуги',
'version': '19.0.2026.09.27',
'summary': 'Договоры аренды, начисление по счётчикам и целевым взносам',
'description': """
РуОду — Аренда и коммунальные услуги
====================================
Управление договорами аренды и коммунальных услуг:
* Договоры аренды с привязкой к объектам (офисы, машиноместа и т. п.).
* Учёт приборов учёта (счётчиков) и показаний по периодам.
* Автоматическое формирование счетов (`account.move`) за период —
ежемесячно, ежеквартально или ежегодно, в том числе по показаниям
счётчиков (расход между периодами).
* Связь договора аренды с российским договором
(`partner.contract.customer`) и печать счёта по форме 1С.
* Портал заказчика: отображение текущего долга.
Порт коммерческого модуля `comm_service` (Odoo 17) на платформу RuOdoo
(Odoo 19) с согласованием с российской локализацией `l10n_ru_*`.
""",
'author': 'MK.Lab, RuOdoo',
'website': 'https://ruodoo.ru',
'category': 'Accounting/Localizations',
'depends': [
'base',
'mail',
'account',
'l10n_ru_contract',
'l10n_ru_contract_account',
],
'external_dependencies': {
'python': ['pytils'],
},
'data': [
'security/ir.model.access.csv',
'views/comm_service_views.xml',
'report/report_order.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
'maintainers': ['mklab', 'ruodoo'],
}

View File

@@ -0,0 +1,2 @@
from . import portal

View File

@@ -0,0 +1,15 @@
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal
class CustomerCredit(CustomerPortal):
def _prepare_portal_layout_values(self):
values = super()._prepare_portal_layout_values()
partner = request.env.user.partner_id
values.update({
'credit': partner.credit,
'parent_credit': partner.parent_id.credit if partner.parent_id else 0,
})
return values

View File

@@ -0,0 +1,8 @@
from . import building
from . import counter
from . import contract
from . import contract_line
from . import account_move
from . import create_invoice_wizard
from . import save_values_wizard

View File

@@ -0,0 +1,11 @@
from odoo import fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
month = fields.Integer(string='Месяц')
year = fields.Integer(string='Год')
comm_contract_id = fields.Many2one('comm.contract', string='Договор аренды')
kladov = fields.Many2one('res.users', string='Кладовщик')

View File

@@ -0,0 +1,91 @@
from odoo import api, fields, models
class CommBuildingType(models.Model):
_name = 'comm.btype'
_description = 'Тип объекта аренды'
name = fields.Char(string='Название типа объекта', required=True)
class CommBuilding(models.Model):
_name = 'comm.building'
_description = 'Объект аренды'
name = fields.Char(string='Номер офиса или машиноместа', required=True)
type = fields.Many2one('comm.btype', string='Тип')
busy_until = fields.Date(string='Занято до', compute='_compute_busy_until')
is_free = fields.Boolean(
string='Сейчас свободно',
compute='_compute_is_free',
search='_search_is_free',
)
price_analysis = fields.Html(string='История цен', compute='_compute_price_analysis')
@api.depends('name', 'type')
def _compute_display_name(self):
for rec in self:
if rec.type and rec.name:
rec.display_name = '%s %s' % (rec.type.name, rec.name)
else:
rec.display_name = rec.name or ''
def _compute_price_analysis(self):
ContractLine = self.env['comm.contractline']
for rec in self:
lines = ContractLine.search([
('counter_id', '=', False),
('contract_id.building_id', '=', rec.id),
])
if lines:
sorted_lines = lines.sorted(
key=lambda line: line.contract_id.date_end or fields.Date.today(),
reverse=True,
)
rows = sorted_lines.mapped(
lambda line: '%s %s %sр. ' % (
line.contract_id.date_end or '',
line.product_id.name or '',
line.price,
)
)
rec.price_analysis = '<p>%s</p>' % '<br/>'.join(rows)
else:
rec.price_analysis = ''
def _compute_busy_until(self):
Contract = self.env['comm.contract']
for rec in self:
contracts = Contract.search([
('state', '=', 'open'),
('building_id', '=', rec.id),
])
if contracts:
rec.busy_until = contracts.sorted(
key=lambda c: c.date_end or fields.Date.today(),
reverse=True,
)[0].date_end
else:
rec.busy_until = False
@api.depends('busy_until')
def _compute_is_free(self):
for rec in self:
rec.is_free = not rec.busy_until
def _search_is_free(self, operator, value):
busy_building_ids = self.env['comm.contract'].search([
('state', '=', 'open'),
]).mapped('building_id').ids
# Нормализуем леф вида (is_free, op, value) к «ищем свободные?».
if operator in ('=', '=='):
searching_free = bool(value)
elif operator == '!=':
searching_free = not bool(value)
else:
searching_free = bool(value)
# Свободные объекты — те, что не заняты действующим договором.
if searching_free:
return [('id', 'not in', busy_building_ids)]
return [('id', 'in', busy_building_ids)]

View File

@@ -0,0 +1,305 @@
import calendar
from datetime import date, datetime
from odoo import api, fields, models
QUARTER_NAMES = {1: 'первый', 4: 'второй', 7: 'третий', 10: 'четвертый'}
class CommContract(models.Model):
_name = 'comm.contract'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Договор аренды'
_order = 'name'
name = fields.Char(string='Номер договора', tracking=True)
date_start = fields.Date(string='Дата начала действия')
date_end = fields.Date(string='Дата окончания действия')
building_id = fields.Many2one('comm.building', string='Объект')
state = fields.Selection(
[
('draft', 'Черновик'),
('open', 'Действует'),
('closed', 'Не действует'),
],
string='Статус', default='draft', tracking=True,
)
partner_id = fields.Many2one('res.partner', string='Заказчик', required=True)
commercial_partner_id = fields.Many2one(
'res.partner', string='Коммерческий партнёр',
store=True, compute='_compute_commercial_partner_id',
)
line_ids = fields.One2many('comm.contractline', 'contract_id', string='Услуги')
building = fields.Char(string='Название объекта')
credit = fields.Float(string='Долг', compute='_compute_credit')
inv_count = fields.Integer(string='Количество счетов', compute='_compute_inv_count')
company_id = fields.Many2one(
'res.company', string='Компания', required=True,
default=lambda self: self.env.company,
)
contract_id = fields.Many2one(
'partner.contract.customer', string='Связанный фин. договор',
)
day_inv = fields.Integer(string='День счёта, 0 если автомат', default=0)
day_upd = fields.Integer(string='День УПД, 0 если автомат', default=0)
@api.depends('partner_id', 'partner_id.parent_id')
def _compute_commercial_partner_id(self):
for rec in self:
rec.commercial_partner_id = (
rec.partner_id.parent_id or rec.partner_id
)
def _compute_credit(self):
for rec in self:
rec.credit = rec.partner_id.credit if rec.partner_id else 0.0
def _compute_inv_count(self):
Move = self.env['account.move']
for rec in self:
rec.inv_count = Move.search_count([
('commercial_partner_id', '=', rec.commercial_partner_id.id),
('year', '>', 0),
('state', 'not in', ['cancel']),
('comm_contract_id', '=', rec.id),
])
def connect_counters(self):
"""Привязать счётчики строк договора к самому договору."""
for contract in self.env['comm.contract'].search([]):
for line in contract.line_ids:
if line.counter_id:
line.counter_id.contract_id = contract
# ------------------------------------------------------------------
# Формирование счетов
# ------------------------------------------------------------------
def _is_period_due(self, line, month):
"""Наступил ли период начисления для строки в данном месяце."""
if line.period == 'month':
return True
if line.period == 'quarter':
return month in (1, 4, 7, 10)
if line.period == 'year':
return month == 1
return False
def _period_line_name(self, line, year, month):
"""Название строки счёта с учётом периодичности."""
if line.period == 'year' and month == 1:
return '%s, ежегодное начисление за %sг.' % (line.name, year)
if line.period == 'quarter' and month in QUARTER_NAMES:
return '%s, ежеквартальное начисление за %s кв. %sг.' % (
line.name, QUARTER_NAMES[month], year,
)
return '%s %s-%s' % (line.name, year, month)
def _prepare_invoice_vals(self, line, year, month):
self.ensure_one()
sale_journal = self.env['account.journal'].search([
('type', '=', 'sale'),
('company_id', '=', self.company_id.id),
], limit=1)
origin = 'Начисление по договору аренды за %s %s-%s' % (
line.name, year, month,
)
return {
'ref': origin,
'invoice_origin': origin,
'move_type': 'out_invoice',
'journal_id': sale_journal.id or False,
'partner_id': self.commercial_partner_id.id,
'partner_shipping_id': self.partner_id.id,
'fiscal_position_id': self.partner_id.property_account_position_id.id,
'team_id': False,
'invoice_date_due': (
date(year, month, 1) if self.day_inv == 0
else date(year, month, self.day_inv)
),
'invoice_date': (
date(year, month, calendar.monthrange(year, month)[1])
if self.day_upd == 0 else date(year, month, self.day_upd)
),
'month': month,
'year': year,
'company_id': self.company_id.id,
'mt_contract_id': self.contract_id.id if self.contract_id else False,
'comm_contract_id': self.id,
'kladov': self.company_id.chief_id.id if self.company_id.chief_id else False,
}
def _income_account_id(self, line):
return line.product_id.categ_id.property_account_income_categ_id.id or False
def create_invoice(self, year=False, month=False):
"""Создать счета за период по всем строкам договоров self.
Возвращает текстовый отчёт об операции (сообщения по строкам).
"""
result = ''
Move = self.env['account.move']
for contract in self:
if not year or not month:
year = datetime.today().year
month = datetime.today().month
for line in contract.line_ids:
if not contract._is_period_due(line, month):
continue
origin = 'Начисление по договору аренды за %s %s-%s' % (
line.name, year, month,
)
existing = Move.search([
('commercial_partner_id', '=', contract.commercial_partner_id.id),
('state', '!=', 'cancel'),
('invoice_origin', '=', origin),
('comm_contract_id', '=', contract.id),
], limit=1)
if existing:
result += (
'Счёт за указанный период за данную услугу уже есть в базе. '
'Например, %s. Пожалуйста, отмените старый счёт, прежде чем '
'создавать новый.' % existing.name
)
continue
invoice_vals = contract._prepare_invoice_vals(line, year, month)
account_id = contract._income_account_id(line)
if line.counter_id:
line_vals, message = contract._prepare_counter_line(
line, year, month, account_id,
)
result += message
if not line_vals:
continue
invoice_vals['invoice_line_ids'] = line_vals
else:
invoice_vals['invoice_line_ids'] = contract._prepare_flat_line(
line, year, month, account_id,
)
result += (
'Создано начисление за период: %s, услуга %s, цена %s; '
% (line.period, line.product_id.name, line.price)
)
new_inv = Move.create(invoice_vals)
new_inv.set_ons()
return result
def _prepare_counter_line(self, line, year, month, account_id):
"""Строка счёта по показаниям счётчика (расход за период)."""
CounterVal = self.env['comm.counterval']
current = CounterVal.search([
('counter_id', '=', line.counter_id.id),
('year', '=', year),
('month', '=', month),
], limit=1)
previous = CounterVal.search([
('counter_id', '=', line.counter_id.id),
('id', '!=', current.id if current else 0),
], order='year desc, month desc', limit=1)
if not (current and previous) or (current.name - previous.name) <= 0:
return [], (
'для услуги %s не удалось вычислить потребление; '
% line.product_id.name
)
consumption = current.name - previous.name
message = '%s, цена %s потребление: %s' % (
line.product_id.name, line.price, consumption,
)
vals = {
'product_id': line.product_id.id,
'product_uom_id': line.counter_id.type.uom.id,
'name': self._period_line_name(line, year, month),
'quantity': consumption,
'tax_ids': [(6, 0, [line.tax_id.id])] if line.tax_id else False,
'price_unit': line.price,
'partner_id': self.partner_id.parent_id.id or self.partner_id.id,
}
# account_id задаём только если найден счёт дохода — иначе Odoo
# подставит счёт по умолчанию (из журнала/категории продукта).
if account_id:
vals['account_id'] = account_id
return [(0, 0, vals)], message
def _prepare_flat_line(self, line, year, month, account_id):
"""Строка счёта без счётчика (фиксированная сумма за период)."""
if line.period == 'year' and month == 1:
name = '%s, ежегодное начисление за %sг.' % (line.name, year)
elif line.period == 'quarter' and month in QUARTER_NAMES:
name = '%s, ежеквартальное начисление за %s кв. %sг.' % (
line.name, QUARTER_NAMES[month], year,
)
else:
last_day = calendar.monthrange(year, month)[1]
name = '%s %s , период 01.%s.%s-%s.%s.%s' % (
line.product_id.name, self.building_id.name,
month, year, last_day, month, year,
)
vals = {
'product_id': line.product_id.id,
'name': name,
'quantity': 1,
'price_unit': line.price,
'tax_ids': [(6, 0, [line.tax_id.id])] if line.tax_id else False,
'product_uom_id': line.product_id.uom_id.id,
}
if account_id:
vals['account_id'] = account_id
return [(0, 0, vals)]
# ------------------------------------------------------------------
# Действия
# ------------------------------------------------------------------
def action_view_invoice(self):
self.ensure_one()
action = self.env['ir.actions.act_window']._for_xml_id(
'account.action_move_out_invoice_type'
)
action.update({
'context': dict(
self.env.context,
default_partner_id=self.partner_id.id,
group_by=False,
),
'domain': [
('commercial_partner_id', '=', self.commercial_partner_id.id),
('comm_contract_id', '=', self.id),
],
})
return action
def create_fincontract(self):
"""Создать связанный российский договор (partner.contract.customer)."""
Contract = self.env['partner.contract.customer']
profile = self.env['contract.profile'].search([], limit=1)
for rec in self:
partner_type = 'company' if rec.partner_id.is_company else 'person'
vals = {
'date_start': rec.date_start or fields.Date.today(),
'date_end': rec.date_end or fields.Date.today(),
'type': 'customer',
'partner_id': rec.partner_id.id,
'partner_type': partner_type,
'company_id': rec.company_id.id,
}
if profile:
vals['profile_id'] = profile.id
rec.contract_id = Contract.create(vals)
@api.constrains('contract_id')
def _sync_fincontract(self):
for rec in self:
if rec.contract_id:
rec.contract_id.write({
'date_start': rec.date_start,
'date_end': rec.date_end,
'partner_id': rec.partner_id.id,
'company_id': rec.company_id.id,
})

View File

@@ -0,0 +1,57 @@
from odoo import api, fields, models
class CommContractLine(models.Model):
_name = 'comm.contractline'
_description = 'Услуга договора аренды'
_order = 'period,product_name'
name = fields.Char(string='Название услуги', compute='_compute_name', store=True)
product_id = fields.Many2one('product.product', string='Услуга')
product_name = fields.Char(
related='product_id.name', store=True, readonly=True,
)
counter_id = fields.Many2one('comm.counter', string='Счётчик')
price = fields.Float(string='Цена', digits='Product Price')
tax_id = fields.Many2one('account.tax', string='НДС')
period = fields.Selection(
[
('month', 'Месяц'),
('quarter', 'Квартал'),
('year', 'Год'),
],
string='Период', default='month',
)
contract_id = fields.Many2one('comm.contract', string='Номер договора')
@api.depends('product_id', 'product_id.name')
def _compute_name(self):
for rec in self:
rec.name = rec.product_id.name or ''
@api.onchange('product_id')
def _onchange_product_id(self):
for rec in self:
rec.tax_id = rec.product_id.taxes_id[:1]
rec.price = rec.product_id.lst_price
def write(self, values):
if self.counter_id and self.contract_id:
self.counter_id.contract_id = self.contract_id
return super().write(values)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for record in records:
if record.contract_id and record.counter_id:
record.counter_id.contract_id = record.contract_id
return records
@api.model
def connect_counters(self):
for contract in self.env['comm.contract'].search([]):
for line in contract.line_ids:
if line.counter_id:
line.counter_id.contract_id = contract

View File

@@ -0,0 +1,195 @@
from datetime import datetime
from odoo import api, fields, models, exceptions
class CommCounterType(models.Model):
_name = 'comm.countertype'
_description = 'Тип счётчика'
name = fields.Char(string='Название типа счётчика', required=True)
uom = fields.Many2one('uom.uom', string='Единица измерения')
class CommCounter(models.Model):
_name = 'comm.counter'
_description = 'Прибор учёта (счётчик)'
_order = 'contract_name,type,name'
name = fields.Char(string='Номер счётчика', required=True)
type = fields.Many2one('comm.countertype', string='Тип счётчика')
contract_id = fields.Many2one('comm.contract', string='Договор')
contract_name = fields.Char(
string='Номер договора',
related='contract_id.name',
store=True,
readonly=True,
)
price = fields.Float(
string='Цена',
compute='_compute_price',
store=True,
readonly=True,
)
last_value_val = fields.Float(
string='Последнее показание',
compute='_compute_last_value',
store=True,
)
last_period = fields.Char(
string='Последний период',
compute='_compute_last_period',
store=True,
)
values_ids = fields.Many2many('comm.counterval', string='Показания')
@api.depends('name', 'type', 'last_period', 'last_value_val')
def _compute_display_name(self):
for rec in self:
if rec.last_period:
suffix = ' (%s: %s, %s)' % (
rec.last_period, rec.last_value_val, rec.type.name,
)
elif rec.type:
suffix = ' (%s)' % rec.type.name
else:
suffix = ''
rec.display_name = '%s%s' % (rec.name or '', suffix)
@api.depends('values_ids', 'contract_id')
def _compute_last_value(self):
for rec in self:
rec.last_value_val = rec._last_value()
def _last_value(self):
self.ensure_one()
value = self.env['comm.counterval'].search(
[('counter_id', '=', self.id)],
order='year desc, month desc',
limit=1,
)
return value.name if value else 0.0
@api.depends('contract_id', 'contract_id.line_ids.price')
def _compute_price(self):
for rec in self:
line = rec.contract_id.line_ids.filtered(
lambda ln: ln.counter_id.id == rec.id
)
rec.price = line[:1].price
@api.depends('values_ids', 'contract_id')
def _compute_last_period(self):
for rec in self:
value = self.env['comm.counterval'].search(
[('counter_id', '=', rec.id)],
order='year desc, month desc',
limit=1,
)
rec.last_period = (
'%s-%s' % (value.year, value.month) if value else ''
)
@api.onchange('contract_id')
def _onchange_contract_id(self):
for rec in self:
if not rec.contract_id:
continue
existing = rec.contract_id.line_ids.mapped('counter_id').ids
if rec.id and rec.id not in existing:
rec.contract_id.line_ids.create({
'counter_id': rec.id,
'contract_id': rec.contract_id.id,
})
def action_show_last_value(self):
self.ensure_one()
raise exceptions.UserError(
'На сегодняшнее число последнее показание этого счётчика, '
'зарегистрированное в базе: %s' % self._last_value()
)
def action_add_value(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'comm.counterval',
'view_mode': 'form',
'target': 'new',
'context': {'default_counter_id': self.id},
}
class CommCounterValue(models.Model):
_name = 'comm.counterval'
_description = 'Показание счётчика'
_order = 'year desc, month desc'
name = fields.Float(string='Показание', required=True)
year = fields.Integer(
string='Год', required=True,
default=lambda self: datetime.today().year,
)
month = fields.Integer(
string='Месяц', required=True,
default=lambda self: datetime.today().month,
)
counter_id = fields.Many2one('comm.counter', string='Счётчик', required=True)
contract_id = fields.Many2one(
'comm.contract', string='Договор',
related='counter_id.contract_id', store=True,
)
type = fields.Many2one(
'comm.countertype', string='Тип',
related='counter_id.type', store=True,
)
partner_id = fields.Many2one(
'res.partner', string='Партнёр',
related='counter_id.contract_id.partner_id', store=True,
)
@api.constrains('name', 'month', 'year', 'counter_id')
def _check_value(self):
for rec in self:
if rec.month > 12 or rec.month < 1:
raise exceptions.ValidationError(
'Счётчик: %s. Месяц должен быть от 1 до 12!' % rec.counter_id.name
)
if rec.year < 2010 or rec.year > 2050:
raise exceptions.ValidationError(
'Счётчик: %s. Некорректно указан год!' % rec.counter_id.name
)
if rec.name == 0:
raise exceptions.ValidationError(
'Счётчик: %s. Значение не может быть равно 0.' % rec.counter_id.name
)
later = self.search([
('year', '>=', rec.year),
('month', '>=', rec.month),
('counter_id', '=', rec.counter_id.id),
('id', '!=', rec.id),
])
if later:
raise exceptions.ValidationError(
'Счётчик: %s. Уже существует показание за указанный или '
'более поздний период.' % rec.counter_id.name
)
bigger = self.search([
('name', '>', rec.name),
('counter_id', '=', rec.counter_id.id),
('id', '!=', rec.id),
])
if bigger:
raise exceptions.ValidationError(
'Счётчик: %s. Уже существует большее показание для этого '
'счётчика. Например: %s.' % (rec.counter_id.name, bigger[0].name)
)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for record in records:
if record.counter_id:
record.counter_id.values_ids |= record
return records

View File

@@ -0,0 +1,73 @@
from datetime import datetime
from odoo import api, fields, models, exceptions
class CommCreateInvoiceWizard(models.TransientModel):
_name = 'comm.createinv'
_description = 'Мастер создания счетов за период'
contract_ids = fields.Many2many('comm.contract', string='Договоры', required=True)
line_ids = fields.Many2many('comm.wizardline', string='Результат работы')
year = fields.Integer(
string='Год', required=True,
default=lambda self: datetime.today().year,
)
month = fields.Integer(
string='Месяц', required=True,
default=lambda self: datetime.today().month,
)
invoice_created = fields.Boolean(string='Счета уже создавались')
message = fields.Char(string='Сообщение')
@api.constrains('year', 'month')
def _check_period(self):
for rec in self:
if rec.year < 2018 or rec.month < 1 or rec.month > 12:
raise exceptions.ValidationError(
'Введите корректные данные для периода!'
)
def fill_contracts(self):
self.ensure_one()
contracts = self.env['comm.contract'].search([('state', '=', 'open')])
self.contract_ids = [(6, 0, contracts.ids)]
return self._reopen_form()
def do_creation(self):
self.ensure_one()
WizardLine = self.env['comm.wizardline']
for contract in self.contract_ids:
new_line = WizardLine.create({'contract_id': contract.id})
self.line_ids |= new_line
new_line.result = contract.create_invoice(
year=self.year, month=self.month,
)
if self.line_ids:
self.invoice_created = True
self.message = (
'Обработка завершена. Если имеются сообщения, что не удалось '
'вычислить потребление, это означает, что не были внесены данные '
'по счётчикам. Как только данные подадут, начисление автоматически '
'учтёт весь расход.'
)
return self._reopen_form()
def _reopen_form(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
class CommCreateInvoiceWizardLine(models.TransientModel):
_name = 'comm.wizardline'
_description = 'Строка результата мастера создания счетов'
contract_id = fields.Many2one('comm.contract', string='Договор')
result = fields.Char(string='Результат')

View File

@@ -0,0 +1,110 @@
from datetime import datetime
from odoo import api, fields, models, exceptions
class CommSaveValuesWizard(models.TransientModel):
_name = 'comm.savevalues'
_description = 'Мастер ввода показаний счётчиков'
year = fields.Integer(
string='Год', required=True,
default=lambda self: datetime.today().year,
)
month = fields.Integer(
string='Месяц', required=True,
default=lambda self: datetime.today().month,
)
line_ids = fields.Many2many('comm.save_line', string='Счётчики и показания')
message = fields.Char(string='Сообщение')
type_ids = fields.Many2many('comm.countertype', string='Типы счётчиков')
def _active_counters(self):
return self.env['comm.contractline'].search([
('contract_id.state', '=', 'open'),
('counter_id', '!=', False),
]).mapped('counter_id')
def _build_lines(self, counters):
self.line_ids = False
SaveLine = self.env['comm.save_line']
for counter in counters.sorted(
key=lambda c: (c.type.id, c.contract_id.id, c.id)
):
if counter.type in self.type_ids or not self.type_ids:
self.line_ids |= SaveLine.create({'counter_id': counter.id})
return self._reopen_form()
def fill_values(self):
"""Счётчики без показаний за выбранный период."""
self.ensure_one()
active = self._active_counters()
with_values = self.env['comm.counterval'].search([
('year', '=', self.year),
('month', '=', self.month),
]).mapped('counter_id')
return self._build_lines(active - with_values)
def fill_all_values(self):
"""Все счётчики по действующим договорам."""
self.ensure_one()
return self._build_lines(self._active_counters())
def do_save(self):
self.ensure_one()
CounterVal = self.env['comm.counterval']
for line in self.line_ids:
CounterVal.create({
'name': line.current_value,
'year': self.year,
'month': self.month,
'counter_id': line.counter_id.id,
})
self.message = 'Показания записаны'
self.fill_values()
return self._reopen_form()
def _reopen_form(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
class CommSaveValuesWizardLine(models.TransientModel):
_name = 'comm.save_line'
_description = 'Строка мастера ввода показаний'
counter_id = fields.Many2one('comm.counter', string='Счётчик')
type = fields.Many2one(
'comm.countertype', string='Тип', related='counter_id.type',
)
contract_id = fields.Many2one(
'comm.contract', string='Договор', related='counter_id.contract_id',
)
partner_id = fields.Many2one(
'res.partner', string='Партнёр',
related='counter_id.contract_id.partner_id',
)
last_value = fields.Float(
string='Последнее показание', related='counter_id.last_value_val',
)
current_value = fields.Float(string='Текущее показание')
values_ids = fields.Many2many(
'comm.counterval', string='Показания', related='counter_id.values_ids',
)
last_period = fields.Char(
string='Последний период', related='counter_id.last_period',
)
@api.onchange('current_value')
def _onchange_current_value(self):
if self.current_value and self.current_value <= self.last_value:
raise exceptions.ValidationError(
'Текущее значение не может быть меньше предыдущего.'
)

View File

@@ -0,0 +1,2 @@
from . import report_order

View File

@@ -0,0 +1,17 @@
from odoo import models
from odoo.addons.ruodoo_commservice.report_helper import QWebHelper
class RuCommInvoiceReport(models.AbstractModel):
_name = 'report.ruodoo_commservice.report_inv_1c'
_description = 'Счёт по форме 1С (аренда/коммунальные услуги)'
def _get_report_values(self, docids, data=None):
docs = self.env['account.move'].browse(docids)
return {
'helper': QWebHelper(),
'doc_ids': docs.ids,
'doc_model': 'account.move',
'docs': docs,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
"""Помощник форматирования для QWeb-отчётов (порт из comm_service).
Повторяет API QWebHelper российской локализации (`l10n_ru_doc`): русские
даты, суммы прописью, представление реквизитов контрагента.
"""
from datetime import datetime
import re
from pytils import numeral, dt
from odoo.tools import pycompat
class QWebHelper:
def img(self, img, type='png', width=0, height=0):
width = "width='%spx'" % width if width else ' '
height = "height='%spx'" % height if height else ' '
return "<img %s %s src='data:image/%s;base64,%s' />" % (
width, height, type, str(pycompat.to_text(img)),
)
def numer(self, name):
if name:
numeration = re.findall(r'\d+$', name)
if numeration:
return numeration[0]
return ''
def ru_date(self, date):
if date and date != 'False':
return dt.ru_strftime(
'"%d" %B %Y года',
date=datetime.strptime(str(date), "%Y-%m-%d"),
inflected=True,
)
return ''
def ru_date2(self, date):
if date and date != 'False':
return dt.ru_strftime(
'%d %B %Y г.',
date=datetime.strptime(str(date), "%Y-%m-%d %H:%M:%S"),
inflected=True,
)
return ''
def in_words(self, number):
return numeral.in_words(number)
def rubles(self, sum):
"""Сумма в рублях прописью."""
text_rubles = numeral.rubles(int(sum))
copeck = round((sum - int(sum)) * 100)
text_copeck = numeral.choose_plural(
int(copeck), ("копейка", "копейки", "копеек"),
)
return "%s %02d %s" % (text_rubles, copeck, text_copeck)
def initials(self, fio):
if fio:
parts = fio.split()
return (parts[0] + ' ' + ''.join(
[p[0:1] + '.' for p in parts[1:]]
)).strip()
return ''
def address(self, partner):
repr = []
if partner.zip:
repr.append(partner.zip)
if partner.city:
repr.append(partner.city)
if partner.street:
repr.append(partner.street)
if partner.street2:
repr.append(partner.street2)
return ', '.join(repr)
def representation(self, partner):
repr = []
if partner.name:
repr.append(partner.name)
if partner.inn:
repr.append("ИНН " + partner.inn)
if partner.kpp:
repr.append("КПП " + partner.kpp)
repr.append(self.address(partner))
return ', '.join(repr)
def full_representation(self, partner):
repr = [self.representation(partner)]
if partner.phone:
repr.append("тел.: " + partner.phone)
elif partner.parent_id.phone:
repr.append("тел.: " + partner.parent_id.phone)
bank = None
if partner.bank_ids:
bank = partner.bank_ids[0]
elif partner.parent_id.bank_ids:
bank = partner.parent_id.bank_ids[0]
if bank and bank.acc_number:
repr.append("р/сч " + bank.acc_number)
if bank and bank.bank_name:
repr.append("в банке " + bank.bank_name)
if bank and bank.bank_bic:
repr.append("БИК " + bank.bank_bic)
if bank and bank.bank_corr_acc:
repr.append("к/с " + bank.bank_corr_acc)
return ', '.join(repr)
def representation_small(self, partner):
repr = []
if partner.name:
repr.append(partner.name)
repr.append(self.address(partner))
return ', '.join(repr)

View File

@@ -0,0 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_comm_contract,comm.contract,model_comm_contract,base.group_user,1,1,1,1
access_comm_contractline,comm.contractline,model_comm_contractline,base.group_user,1,1,1,1
access_comm_counter,comm.counter,model_comm_counter,base.group_user,1,1,1,1
access_comm_counterval,comm.counterval,model_comm_counterval,base.group_user,1,1,1,1
access_comm_countertype,comm.countertype,model_comm_countertype,base.group_user,1,1,1,1
access_comm_createinv,comm.createinv,model_comm_createinv,base.group_user,1,1,1,1
access_comm_wizardline,comm.wizardline,model_comm_wizardline,base.group_user,1,1,1,1
access_comm_building,comm.building,model_comm_building,base.group_user,1,1,1,1
access_comm_btype,comm.btype,model_comm_btype,base.group_user,1,1,1,1
access_comm_savevalues,comm.savevalues,model_comm_savevalues,base.group_user,1,1,1,1
access_comm_save_line,comm.save_line,model_comm_save_line,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_comm_contract comm.contract model_comm_contract base.group_user 1 1 1 1
3 access_comm_contractline comm.contractline model_comm_contractline base.group_user 1 1 1 1
4 access_comm_counter comm.counter model_comm_counter base.group_user 1 1 1 1
5 access_comm_counterval comm.counterval model_comm_counterval base.group_user 1 1 1 1
6 access_comm_countertype comm.countertype model_comm_countertype base.group_user 1 1 1 1
7 access_comm_createinv comm.createinv model_comm_createinv base.group_user 1 1 1 1
8 access_comm_wizardline comm.wizardline model_comm_wizardline base.group_user 1 1 1 1
9 access_comm_building comm.building model_comm_building base.group_user 1 1 1 1
10 access_comm_btype comm.btype model_comm_btype base.group_user 1 1 1 1
11 access_comm_savevalues comm.savevalues model_comm_savevalues base.group_user 1 1 1 1
12 access_comm_save_line comm.save_line model_comm_save_line base.group_user 1 1 1 1

View File

@@ -0,0 +1,2 @@
from . import test_comm_service

View File

@@ -0,0 +1,195 @@
from odoo.tests import TransactionCase, tagged
from odoo.exceptions import ValidationError
@tagged('post_install', '-at_install')
class TestCommService(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
cls.partner = cls.env['res.partner'].create({'name': 'Арендатор ООО'})
cls.btype = cls.env['comm.btype'].create({'name': 'Офис'})
cls.building = cls.env['comm.building'].create({
'name': '101', 'type': cls.btype.id,
})
cls.ctype = cls.env['comm.countertype'].create({
'name': 'Электроэнергия',
'uom': cls.env.ref('uom.product_uom_unit').id,
})
cls.product = cls.env['product.product'].create({
'name': 'Аренда офиса',
'type': 'service',
'lst_price': 1000.0,
})
cls.contract = cls.env['comm.contract'].create({
'name': 'АР-001',
'partner_id': cls.partner.id,
'company_id': cls.company.id,
'building_id': cls.building.id,
'state': 'open',
'date_start': '2026-01-01',
'date_end': '2026-12-31',
})
# ------------------------------------------------------------------
def test_building_display_name(self):
self.assertEqual(self.building.display_name, 'Офис 101')
def test_building_busy_and_free(self):
# Действующий договор занимает объект.
self.assertEqual(self.building.busy_until, self.contract.date_end)
self.assertFalse(self.building.is_free)
# Свободный объект без договора.
free_building = self.env['comm.building'].create({'name': '202'})
self.assertTrue(free_building.is_free)
found = self.env['comm.building'].search([('is_free', '=', True)])
self.assertIn(free_building, found)
self.assertNotIn(self.building, found)
def test_commercial_partner(self):
parent = self.env['res.partner'].create({
'name': 'Головная компания', 'is_company': True,
})
self.partner.parent_id = parent
self.contract._compute_commercial_partner_id()
self.assertEqual(self.contract.commercial_partner_id, parent)
# ------------------------------------------------------------------
def test_counter_value_constraints(self):
counter = self.env['comm.counter'].create({
'name': 'С-1', 'type': self.ctype.id, 'contract_id': self.contract.id,
})
self.env['comm.counterval'].create({
'name': 100.0, 'year': 2026, 'month': 1, 'counter_id': counter.id,
})
# Показание не может быть меньше предыдущего.
with self.assertRaises(ValidationError):
self.env['comm.counterval'].create({
'name': 90.0, 'year': 2026, 'month': 2, 'counter_id': counter.id,
})
# Ноль запрещён.
with self.assertRaises(ValidationError):
self.env['comm.counterval'].create({
'name': 0.0, 'year': 2026, 'month': 3, 'counter_id': counter.id,
})
def test_counter_last_value_and_period(self):
counter = self.env['comm.counter'].create({
'name': 'С-2', 'type': self.ctype.id, 'contract_id': self.contract.id,
})
self.env['comm.counterval'].create({
'name': 100.0, 'year': 2026, 'month': 1, 'counter_id': counter.id,
})
self.env['comm.counterval'].create({
'name': 150.0, 'year': 2026, 'month': 2, 'counter_id': counter.id,
})
counter.invalidate_recordset()
self.assertEqual(counter.last_value_val, 150.0)
self.assertEqual(counter.last_period, '2026-2')
# ------------------------------------------------------------------
def test_create_flat_invoice(self):
self.env['comm.contractline'].create({
'contract_id': self.contract.id,
'product_id': self.product.id,
'price': 5000.0,
'period': 'month',
})
result = self.contract.create_invoice(year=2026, month=3)
self.assertIn('Создано начисление', result)
invoice = self.env['account.move'].search([
('comm_contract_id', '=', self.contract.id),
])
self.assertEqual(len(invoice), 1)
self.assertEqual(invoice.move_type, 'out_invoice')
self.assertEqual(invoice.year, 2026)
self.assertEqual(invoice.month, 3)
self.assertAlmostEqual(invoice.amount_untaxed, 5000.0)
def test_no_duplicate_invoice(self):
self.env['comm.contractline'].create({
'contract_id': self.contract.id,
'product_id': self.product.id,
'price': 5000.0,
'period': 'month',
})
self.contract.create_invoice(year=2026, month=4)
result = self.contract.create_invoice(year=2026, month=4)
self.assertIn('уже есть в базе', result)
invoices = self.env['account.move'].search([
('comm_contract_id', '=', self.contract.id),
('invoice_origin', 'like', '2026-4'),
])
self.assertEqual(len(invoices), 1)
def test_quarterly_period_skips_off_months(self):
self.env['comm.contractline'].create({
'contract_id': self.contract.id,
'product_id': self.product.id,
'price': 3000.0,
'period': 'quarter',
})
# Февраль — не начало квартала, начисления быть не должно.
self.contract.create_invoice(year=2026, month=2)
self.assertFalse(self.env['account.move'].search([
('comm_contract_id', '=', self.contract.id),
]))
# Апрель — начало квартала.
self.contract.create_invoice(year=2026, month=4)
self.assertTrue(self.env['account.move'].search([
('comm_contract_id', '=', self.contract.id),
]))
def test_counter_based_invoice(self):
counter = self.env['comm.counter'].create({
'name': 'С-3', 'type': self.ctype.id, 'contract_id': self.contract.id,
})
self.env['comm.counterval'].create({
'name': 100.0, 'year': 2026, 'month': 4, 'counter_id': counter.id,
})
self.env['comm.counterval'].create({
'name': 130.0, 'year': 2026, 'month': 5, 'counter_id': counter.id,
})
self.env['comm.contractline'].create({
'contract_id': self.contract.id,
'product_id': self.product.id,
'counter_id': counter.id,
'price': 7.0,
'period': 'month',
})
result = self.contract.create_invoice(year=2026, month=5)
self.assertIn('потребление', result)
invoice = self.env['account.move'].search([
('comm_contract_id', '=', self.contract.id),
])
self.assertEqual(len(invoice), 1)
# Расход = 130 - 100 = 30 единиц по 7 = 210.
self.assertAlmostEqual(invoice.amount_untaxed, 210.0)
def test_create_invoice_wizard(self):
self.env['comm.contractline'].create({
'contract_id': self.contract.id,
'product_id': self.product.id,
'price': 5000.0,
'period': 'month',
})
wizard = self.env['comm.createinv'].create({'year': 2026, 'month': 6})
wizard.fill_contracts()
self.assertIn(self.contract, wizard.contract_ids)
wizard.do_creation()
self.assertTrue(wizard.invoice_created)
self.assertTrue(self.env['account.move'].search([
('comm_contract_id', '=', self.contract.id),
]))
def test_wizard_bad_period_raises(self):
with self.assertRaises(ValidationError):
self.env['comm.createinv'].create({'year': 2000, 'month': 6})
def test_report_helper_available(self):
report = self.env['report.ruodoo_commservice.report_inv_1c']
values = report._get_report_values([])
self.assertIn('helper', values)
self.assertEqual(values['helper'].rubles(1234.56), values['helper'].rubles(1234.56))

View File

@@ -0,0 +1,362 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ Договоры аренды ============ -->
<record model="ir.ui.view" id="view_comm_contract_list">
<field name="name">comm.contract.list</field>
<field name="model">comm.contract</field>
<field name="arch" type="xml">
<list string="Договоры аренды">
<field name="name"/>
<field name="partner_id"/>
<field name="company_id"/>
<field name="building_id"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="state"/>
</list>
</field>
</record>
<record model="ir.ui.view" id="view_comm_contract_form">
<field name="name">comm.contract.form</field>
<field name="model">comm.contract</field>
<field name="arch" type="xml">
<form>
<header>
<field name="state" widget="statusbar" nolabel="1" options="{'clickable': '1'}"/>
<button name="create_invoice" type="object" string="Создать счета"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_invoice" type="object" class="oe_stat_button"
icon="fa-pencil-square-o" context="{'search_default_open': 1}">
<field name="inv_count" widget="statinfo" string="Счета"/>
</button>
</div>
<group>
<group string="Договор аренды">
<field name="name"/>
<field name="partner_id"/>
<field name="company_id"/>
<field name="date_start" required="1"/>
<field name="date_end"/>
</group>
<group string="Счета и УПД">
<field name="contract_id"
domain="[('partner_id','=',partner_id),('company_id','=',company_id)]"/>
<button name="create_fincontract" type="object"
string="Договор не привязан. Создать?"
invisible="contract_id != False"/>
<field name="day_inv"/>
<field name="day_upd"/>
</group>
<group string="Объект">
<field name="building_id" required="1"/>
<field name="credit"/>
</group>
<field name="line_ids" context="{'default_contract_id': id}">
<list editable="bottom">
<field name="product_id"
domain="[('sale_ok','=',True),('type','=','service')]"
options="{'no_create': True}"/>
<field name="contract_id" column_invisible="1"/>
<field name="counter_id" domain="[('contract_id','=',parent.id)]"
context="{'default_contract_id': parent.id}"/>
<field name="price"/>
<field name="tax_id"/>
<field name="period"/>
</list>
</field>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_comm_contract">
<field name="name">Договоры аренды</field>
<field name="res_model">comm.contract</field>
<field name="view_mode">list,form</field>
</record>
<!-- ============ Счётчики ============ -->
<record model="ir.ui.view" id="view_comm_counter_list">
<field name="name">comm.counter.list</field>
<field name="model">comm.counter</field>
<field name="arch" type="xml">
<list string="Счётчики">
<field name="name"/>
<field name="type"/>
<field name="contract_name"/>
<field name="price"/>
<field name="last_value_val"/>
<field name="last_period"/>
<field name="values_ids" widget="many2many_tags"/>
</list>
</field>
</record>
<record model="ir.ui.view" id="view_comm_counter_form">
<field name="name">comm.counter.form</field>
<field name="model">comm.counter</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_show_last_value" type="object"
string="Узнать последнее показание" class="oe_highlight"/>
<button name="action_add_value" type="object"
string="Внести показание" class="oe_highlight"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="type"/>
<field name="contract_id"/>
<field name="last_value_val"/>
<field name="last_period"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_comm_counter_search">
<field name="name">comm.counter.search</field>
<field name="model">comm.counter</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="contract_id"/>
<field name="last_period"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_comm_counter">
<field name="name">Счётчики</field>
<field name="res_model">comm.counter</field>
<field name="view_mode">list,form</field>
<field name="context">{'group_by': 'last_period'}</field>
</record>
<!-- ============ Показания счётчиков ============ -->
<record model="ir.ui.view" id="view_comm_counterval_list">
<field name="name">comm.counterval.list</field>
<field name="model">comm.counterval</field>
<field name="arch" type="xml">
<list string="Показания счётчиков">
<field name="counter_id"/>
<field name="contract_id"/>
<field name="partner_id"/>
<field name="type"/>
<field name="year"/>
<field name="month"/>
<field name="name"/>
</list>
</field>
</record>
<record model="ir.ui.view" id="view_comm_counterval_form">
<field name="name">comm.counterval.form</field>
<field name="model">comm.counterval</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="counter_id"/>
<field name="contract_id"/>
<field name="partner_id"/>
<field name="type"/>
</group>
<group>
<field name="year"/>
<field name="month"/>
<field name="name"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_comm_counterval_search">
<field name="name">comm.counterval.search</field>
<field name="model">comm.counterval</field>
<field name="arch" type="xml">
<search>
<field name="counter_id"/>
<field name="contract_id"/>
<field name="partner_id"/>
<field name="year"/>
<field name="month"/>
<field name="name"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_comm_counterval">
<field name="name">Показания счётчиков</field>
<field name="res_model">comm.counterval</field>
<field name="view_mode">list,form</field>
</record>
<!-- ============ Объекты ============ -->
<record model="ir.ui.view" id="view_comm_building_list">
<field name="name">comm.building.list</field>
<field name="model">comm.building</field>
<field name="arch" type="xml">
<list string="Объекты">
<field name="name"/>
<field name="type"/>
<field name="busy_until"/>
<field name="is_free"/>
</list>
</field>
</record>
<record model="ir.ui.view" id="view_comm_building_search">
<field name="name">comm.building.search</field>
<field name="model">comm.building</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<filter string="Свободные" domain="[('is_free', '=', True)]" name="is_free_filter"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_comm_building">
<field name="name">Объекты</field>
<field name="res_model">comm.building</field>
<field name="view_mode">list,form</field>
</record>
<!-- ============ Мастер создания счетов ============ -->
<record model="ir.ui.view" id="view_comm_createinv_form">
<field name="name">comm.createinv.form</field>
<field name="model">comm.createinv</field>
<field name="arch" type="xml">
<form>
<header>
<button type="object" name="fill_contracts"
string="Заполнить всеми активными договорами" class="oe_highlight"/>
</header>
<sheet>
<group>
<group>
<field name="year"/>
<field name="month"/>
</group>
<field name="contract_ids"/>
<field name="line_ids" readonly="1">
<list>
<field name="contract_id"/>
<field name="result"/>
</list>
</field>
</group>
</sheet>
<footer>
<field name="message" readonly="1"/>
<field name="invoice_created" invisible="1"/>
<button type="object" name="do_creation" string="Создать счета"
class="oe_highlight" invisible="invoice_created == True"/>
<button special="cancel" string="Закрыть мастер" class="oe_highlight"/>
</footer>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_comm_createinv">
<field name="name">Мастер создания счетов за периоды</field>
<field name="binding_model_id" ref="model_comm_contract"/>
<field name="res_model">comm.createinv</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- ============ Мастер ввода показаний ============ -->
<record model="ir.ui.view" id="view_comm_savevalues_form">
<field name="name">comm.savevalues.form</field>
<field name="model">comm.savevalues</field>
<field name="arch" type="xml">
<form>
<header>
<button type="object" name="fill_values"
string="Счётчики без показаний за текущий период" class="oe_highlight"/>
<button type="object" name="fill_all_values"
string="Все счётчики по активным договорам" class="oe_highlight"/>
</header>
<sheet>
<group>
<group>
<field name="year"/>
<field name="month"/>
<field name="type_ids" widget="many2many_tags"/>
</group>
<field name="line_ids">
<list editable="top">
<field name="counter_id" readonly="1"/>
<field name="type" readonly="1"/>
<field name="partner_id" readonly="1"/>
<field name="contract_id" readonly="1"/>
<field name="last_value" readonly="1"/>
<field name="values_ids" string="Есть показания" readonly="1"
widget="many2many_tags"/>
<field name="last_period" readonly="1"/>
<field name="current_value"/>
</list>
</field>
</group>
</sheet>
<footer>
<field name="message" readonly="1"/>
<button type="object" name="do_save" string="Сохранить"
class="oe_highlight" invisible="not line_ids"/>
<button special="cancel" string="Закрыть мастер" class="oe_highlight"/>
</footer>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_comm_savevalues">
<field name="name">Мастер ввода показаний счётчиков</field>
<field name="binding_model_id" ref="model_comm_contract"/>
<field name="res_model">comm.savevalues</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- ============ Расширение формы счёта ============ -->
<record id="view_account_move_form_comm" model="ir.ui.view">
<field name="name">account.move.form.comm</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='ref']" position="after">
<field name="month" invisible="not comm_contract_id"/>
<field name="year" invisible="not comm_contract_id"/>
<field name="comm_contract_id" invisible="not comm_contract_id"/>
</xpath>
</field>
</record>
<!-- ============ Меню ============ -->
<menuitem name="Аренда" id="menu_root"/>
<menuitem name="Договоры" id="menu_contracts" parent="menu_root" action="action_comm_contract"/>
<menuitem name="Создание счетов" id="menu_createinv" parent="menu_contracts"
action="action_comm_createinv"/>
<menuitem name="Счётчики" id="menu_counters" parent="menu_root" action="action_comm_counter"/>
<menuitem name="Показания счётчиков" id="menu_countervals" parent="menu_counters"
action="action_comm_counterval"/>
<menuitem name="Мастер ввода показаний" id="menu_savevalues" parent="menu_counters"
action="action_comm_savevalues"/>
<menuitem name="Объекты" id="menu_buildings" parent="menu_root" action="action_comm_building"/>
</odoo>