Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
116
ruodoo_commservice/README.rst
Normal file
116
ruodoo_commservice/README.rst
Normal 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
|
||||
|
||||
Модуль является частью решения РуОду.
|
||||
5
ruodoo_commservice/__init__.py
Normal file
5
ruodoo_commservice/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import report_helper
|
||||
from . import report
|
||||
46
ruodoo_commservice/__manifest__.py
Normal file
46
ruodoo_commservice/__manifest__.py
Normal 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'],
|
||||
}
|
||||
2
ruodoo_commservice/controllers/__init__.py
Normal file
2
ruodoo_commservice/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import portal
|
||||
15
ruodoo_commservice/controllers/portal.py
Normal file
15
ruodoo_commservice/controllers/portal.py
Normal 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
|
||||
8
ruodoo_commservice/models/__init__.py
Normal file
8
ruodoo_commservice/models/__init__.py
Normal 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
|
||||
11
ruodoo_commservice/models/account_move.py
Normal file
11
ruodoo_commservice/models/account_move.py
Normal 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='Кладовщик')
|
||||
91
ruodoo_commservice/models/building.py
Normal file
91
ruodoo_commservice/models/building.py
Normal 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)]
|
||||
305
ruodoo_commservice/models/contract.py
Normal file
305
ruodoo_commservice/models/contract.py
Normal 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,
|
||||
})
|
||||
57
ruodoo_commservice/models/contract_line.py
Normal file
57
ruodoo_commservice/models/contract_line.py
Normal 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
|
||||
195
ruodoo_commservice/models/counter.py
Normal file
195
ruodoo_commservice/models/counter.py
Normal 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
|
||||
73
ruodoo_commservice/models/create_invoice_wizard.py
Normal file
73
ruodoo_commservice/models/create_invoice_wizard.py
Normal 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='Результат')
|
||||
110
ruodoo_commservice/models/save_values_wizard.py
Normal file
110
ruodoo_commservice/models/save_values_wizard.py
Normal 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(
|
||||
'Текущее значение не может быть меньше предыдущего.'
|
||||
)
|
||||
2
ruodoo_commservice/report/__init__.py
Normal file
2
ruodoo_commservice/report/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import report_order
|
||||
17
ruodoo_commservice/report/report_order.py
Normal file
17
ruodoo_commservice/report/report_order.py
Normal 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,
|
||||
}
|
||||
1444
ruodoo_commservice/report/report_order.xml
Normal file
1444
ruodoo_commservice/report/report_order.xml
Normal file
File diff suppressed because it is too large
Load Diff
117
ruodoo_commservice/report_helper.py
Normal file
117
ruodoo_commservice/report_helper.py
Normal 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)
|
||||
12
ruodoo_commservice/security/ir.model.access.csv
Normal file
12
ruodoo_commservice/security/ir.model.access.csv
Normal 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
|
||||
|
2
ruodoo_commservice/tests/__init__.py
Normal file
2
ruodoo_commservice/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import test_comm_service
|
||||
195
ruodoo_commservice/tests/test_comm_service.py
Normal file
195
ruodoo_commservice/tests/test_comm_service.py
Normal 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))
|
||||
362
ruodoo_commservice/views/comm_service_views.xml
Normal file
362
ruodoo_commservice/views/comm_service_views.xml
Normal 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>
|
||||
Reference in New Issue
Block a user