Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
57
rsbu_journal/README.rst
Normal file
57
rsbu_journal/README.rst
Normal file
@@ -0,0 +1,57 @@
|
||||
====================================
|
||||
РСБУ — Журнал хозяйственных операций
|
||||
====================================
|
||||
|
||||
.. |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|
|
||||
|
||||
Журнал бухгалтерских проводок по российским стандартам (РСБУ) — ядро
|
||||
РСБУ-слоя. Ведёт учёт методом двойной записи по плану счетов 94н и
|
||||
обеспечивает расчёт оборотов и сальдо. Нативная бухгалтерия Odoo не
|
||||
затрагивается.
|
||||
|
||||
**Содержание**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Возможности
|
||||
===========
|
||||
|
||||
* Проводки по методу двойной записи (дебет / кредит)
|
||||
* Аналитика (субконто): контрагент, товар, пользователь, сотрудник
|
||||
* Контроль баланса проводки при проведении
|
||||
* Контроль обязательности заполнения субконто
|
||||
* Обороты и сальдо по счетам рассчитываются из проведённых проводок
|
||||
* Учёт в рублях, поддержка нескольких компаний в одной базе
|
||||
|
||||
Использование
|
||||
=============
|
||||
|
||||
#. Перейдите в *РСБУ > Журнал операций*.
|
||||
#. Создайте проводку: укажите дату, счета по дебету и кредиту и суммы.
|
||||
#. Заполните субконто, если они обязательны для выбранных счетов.
|
||||
#. Проведите проводку — она попадёт в обороты и сальдо плана счетов.
|
||||
|
||||
Как правило, проводки формируются автоматически при проведении
|
||||
документов (см. модуль коннектора). Ручной ввод используется для
|
||||
корректировок и операций без документа-источника.
|
||||
|
||||
Авторы
|
||||
======
|
||||
|
||||
* MK.Lab, RuOdoo
|
||||
|
||||
Модуль сопровождается MK.Lab, RuOdoo.
|
||||
|
||||
.. image:: https://ruodoo.ru/logo.png
|
||||
:alt: MK.Lab, RuOdoo
|
||||
:target: https://ruodoo.ru
|
||||
|
||||
Модуль является частью РСБУ-слоя проекта РуОду.
|
||||
3
rsbu_journal/__init__.py
Normal file
3
rsbu_journal/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
from . import report
|
||||
43
rsbu_journal/__manifest__.py
Normal file
43
rsbu_journal/__manifest__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'РСБУ — Журнал хозяйственных операций',
|
||||
'version': '19.0.2026.09.17',
|
||||
'summary': 'Журнал операций РСБУ (двойная запись, субконто)',
|
||||
'description': """
|
||||
РСБУ — Журнал хозяйственных операций
|
||||
====================================
|
||||
|
||||
Журнал бухгалтерских проводок по российским стандартам (РСБУ) —
|
||||
ядро РСБУ-слоя. Ведёт двойную запись по плану счетов 94н.
|
||||
|
||||
Возможности:
|
||||
------------
|
||||
* Проводки по методу двойной записи (дебет / кредит).
|
||||
* Аналитика (субконто) в разрезе контрагентов, товаров, пользователей
|
||||
и сотрудников.
|
||||
* Контроль баланса проводки и обязательности заполнения субконто
|
||||
при проведении.
|
||||
* Обороты и сальдо по счетам плана счетов рассчитываются из проведённых
|
||||
проводок журнала.
|
||||
|
||||
Учёт ведётся в рублях, поддерживается работа нескольких компаний в одной
|
||||
базе. Нативная бухгалтерия Odoo не затрагивается.
|
||||
""",
|
||||
'author': 'MK.Lab, RuOdoo',
|
||||
'website': 'https://ruodoo.ru',
|
||||
'category': 'Accounting/Localizations',
|
||||
'depends': ['rsbu_chart_of_accounts', 'hr', 'product', 'l10n_ru_contract'],
|
||||
'data': [
|
||||
'security/rsbu_journal_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_sequence_data.xml',
|
||||
'views/rsbu_move_views.xml',
|
||||
'wizards/rsbu_trial_balance_wizard_views.xml',
|
||||
'report/rsbu_trial_balance_templates.xml',
|
||||
'views/rsbu_menus.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'maintainers': ['mklab', 'ruodoo'],
|
||||
}
|
||||
10
rsbu_journal/data/ir_sequence_data.xml
Normal file
10
rsbu_journal/data/ir_sequence_data.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="seq_rsbu_move" model="ir.sequence">
|
||||
<field name="name">РСБУ Операция</field>
|
||||
<field name="code">rsbu.move</field>
|
||||
<field name="prefix">RSBU/%(range_year)s/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
4
rsbu_journal/models/__init__.py
Normal file
4
rsbu_journal/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import rsbu_move
|
||||
from . import rsbu_move_line
|
||||
from . import rsbu_account
|
||||
from . import rsbu_trial_balance
|
||||
23
rsbu_journal/models/rsbu_account.py
Normal file
23
rsbu_journal/models/rsbu_account.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class RsbuAccount(models.Model):
|
||||
"""Расширение плана счетов: наполнение хука агрегации данными журнала."""
|
||||
_inherit = 'rsbu.account'
|
||||
|
||||
def _read_postings(self, domain=None):
|
||||
"""Прочитать проведённые строки журнала для агрегации.
|
||||
|
||||
Переопределяет заглушку Этапа 1. Возвращает строки rsbu.move.line
|
||||
по поддереву счёта, только проведённые (state='posted'), с учётом
|
||||
дополнительного domain (период/компания) из вызывающего метода.
|
||||
"""
|
||||
self.ensure_one()
|
||||
base_domain = [
|
||||
('account_id', 'in', self._get_subtree_account_ids()),
|
||||
('state', '=', 'posted'),
|
||||
]
|
||||
if domain:
|
||||
# Отфильтровать по period/company — поля есть на строке (related).
|
||||
base_domain += domain
|
||||
return self.env['rsbu.move.line'].search(base_domain)
|
||||
148
rsbu_journal/models/rsbu_move.py
Normal file
148
rsbu_journal/models/rsbu_move.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class RsbuMove(models.Model):
|
||||
"""Хозяйственная операция РСБУ (аналог account.move)."""
|
||||
_name = 'rsbu.move'
|
||||
_description = 'РСБУ — Хозяйственная операция'
|
||||
_order = 'date desc, name desc, id desc'
|
||||
_check_company_auto = True
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(
|
||||
string='Номер', default='/', copy=False, readonly=True, index=True)
|
||||
date = fields.Date(
|
||||
string='Дата операции', required=True, index=True,
|
||||
default=fields.Date.context_today, tracking=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания', required=True, index=True,
|
||||
default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', string='Валюта',
|
||||
compute='_compute_currency_id', store=True)
|
||||
state = fields.Selection(
|
||||
selection=[('draft', 'Черновик'), ('posted', 'Проведён')],
|
||||
string='Состояние', default='draft', required=True,
|
||||
copy=False, tracking=True, index=True)
|
||||
line_ids = fields.One2many(
|
||||
'rsbu.move.line', 'move_id', string='Строки проводки',
|
||||
copy=True)
|
||||
ref = fields.Char(string='Ссылка / описание')
|
||||
note = fields.Text(string='Примечание')
|
||||
|
||||
# Ссылка на документ-источник (используется Этапом 3).
|
||||
source_model = fields.Char(string='Модель источника', copy=False)
|
||||
source_res_id = fields.Integer(string='ID источника', copy=False)
|
||||
|
||||
# Контрольные обороты по проводке (для проверки баланса, не «сумма
|
||||
# операции»): при наличии внутренних проводок (напр. НДС на 90.03)
|
||||
# оборот по дебету не равен сумме документа, поэтому «сумму операции»
|
||||
# намеренно не храним и не показываем.
|
||||
total_debit = fields.Monetary(
|
||||
string='Оборот Дт', compute='_compute_totals', store=True,
|
||||
currency_field='currency_id')
|
||||
total_credit = fields.Monetary(
|
||||
string='Оборот Кт', compute='_compute_totals', store=True,
|
||||
currency_field='currency_id')
|
||||
line_count = fields.Integer(
|
||||
string='Кол-во строк', compute='_compute_line_count')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Вычисляемые поля
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('company_id')
|
||||
def _compute_currency_id(self):
|
||||
for move in self:
|
||||
move.currency_id = move.company_id.currency_id
|
||||
|
||||
@api.depends('line_ids.debit', 'line_ids.credit')
|
||||
def _compute_totals(self):
|
||||
for move in self:
|
||||
move.total_debit = sum(move.line_ids.mapped('debit'))
|
||||
move.total_credit = sum(move.line_ids.mapped('credit'))
|
||||
|
||||
@api.depends('line_ids')
|
||||
def _compute_line_count(self):
|
||||
for move in self:
|
||||
move.line_count = len(move.line_ids)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Проверки при проведении
|
||||
# ------------------------------------------------------------------
|
||||
def _check_has_lines(self):
|
||||
for move in self:
|
||||
if not move.line_ids:
|
||||
raise UserError(_(
|
||||
'Нельзя провести операцию «%s» без строк.')
|
||||
% move.display_name)
|
||||
|
||||
def _check_balanced(self):
|
||||
for move in self:
|
||||
rounding = move.currency_id.rounding or 0.01
|
||||
total_debit = sum(move.line_ids.mapped('debit'))
|
||||
total_credit = sum(move.line_ids.mapped('credit'))
|
||||
if not float_is_zero(total_debit - total_credit,
|
||||
precision_rounding=rounding):
|
||||
raise UserError(_(
|
||||
'Операция «%(name)s» не сбалансирована: '
|
||||
'дебет %(debit)s ≠ кредит %(credit)s.',
|
||||
name=move.display_name,
|
||||
debit=total_debit, credit=total_credit))
|
||||
|
||||
def _check_required_subconto(self):
|
||||
for move in self:
|
||||
for line in move.line_ids:
|
||||
required = line.account_id.get_required_subconto()
|
||||
missing = [dim for dim in required
|
||||
if not line._has_subconto(dim)]
|
||||
if missing:
|
||||
labels = {'partner': 'Контрагент', 'product': 'Номенклатура',
|
||||
'user': 'Пользователь', 'employee': 'Сотрудник'}
|
||||
names = ', '.join(labels.get(m, m) for m in missing)
|
||||
raise UserError(_(
|
||||
'Строка со счётом «%(acc)s» требует обязательные '
|
||||
'субконто: %(subs)s.',
|
||||
acc=line.account_id.display_name, subs=names))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Жизненный цикл
|
||||
# ------------------------------------------------------------------
|
||||
def action_post(self):
|
||||
for move in self:
|
||||
if move.state == 'posted':
|
||||
continue
|
||||
move._check_has_lines()
|
||||
move._check_balanced()
|
||||
move._check_required_subconto()
|
||||
if move.name == '/' or not move.name:
|
||||
move.name = self.env['ir.sequence'].with_company(
|
||||
move.company_id).next_by_code('rsbu.move') or '/'
|
||||
move.state = 'posted'
|
||||
return True
|
||||
|
||||
def action_draft(self):
|
||||
for move in self:
|
||||
move.state = 'draft'
|
||||
return True
|
||||
|
||||
def write(self, vals):
|
||||
# Ограничение редактирования ключевых полей у проведённой операции.
|
||||
protected = {'line_ids', 'date', 'company_id'}
|
||||
if any(f in vals for f in protected):
|
||||
for move in self:
|
||||
if move.state == 'posted':
|
||||
raise UserError(_(
|
||||
'Операция «%s» проведена. Верните её в черновик '
|
||||
'перед изменением строк или даты.')
|
||||
% move.display_name)
|
||||
return super().write(vals)
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_only_draft(self):
|
||||
for move in self:
|
||||
if move.state == 'posted':
|
||||
raise UserError(_(
|
||||
'Нельзя удалить проведённую операцию «%s». '
|
||||
'Сначала верните её в черновик.') % move.display_name)
|
||||
92
rsbu_journal/models/rsbu_move_line.py
Normal file
92
rsbu_journal/models/rsbu_move_line.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
# Соответствие измерений субконто полям строки.
|
||||
SUBCONTO_FIELD = {
|
||||
'partner': 'partner_id',
|
||||
'product': 'product_id',
|
||||
'user': 'user_id',
|
||||
'employee': 'employee_id',
|
||||
'origin': 'origin_move_id',
|
||||
'contract': 'contract_id',
|
||||
}
|
||||
|
||||
|
||||
class RsbuMoveLine(models.Model):
|
||||
"""Строка проводки РСБУ (двойная запись, субконто)."""
|
||||
_name = 'rsbu.move.line'
|
||||
_description = 'РСБУ — Строка проводки'
|
||||
_check_company_auto = True
|
||||
|
||||
move_id = fields.Many2one(
|
||||
'rsbu.move', string='Операция', required=True,
|
||||
ondelete='cascade', index=True)
|
||||
account_id = fields.Many2one(
|
||||
'rsbu.account', string='Счёт', required=True,
|
||||
check_company=True, index=True)
|
||||
name = fields.Char(string='Описание')
|
||||
debit = fields.Monetary(string='Дебет', default=0.0,
|
||||
currency_field='currency_id')
|
||||
credit = fields.Monetary(string='Кредит', default=0.0,
|
||||
currency_field='currency_id')
|
||||
|
||||
# Субконто.
|
||||
partner_id = fields.Many2one('res.partner', string='Контрагент',
|
||||
check_company=True)
|
||||
product_id = fields.Many2one('product.product', string='Номенклатура')
|
||||
user_id = fields.Many2one('res.users', string='Пользователь')
|
||||
employee_id = fields.Many2one('hr.employee', string='Сотрудник',
|
||||
check_company=True)
|
||||
# Субконто «Документ-основание»: проводка-основание расчёта (аванс),
|
||||
# аналог 3-го субконто 1С на 62.02/60.02/76.АВ («Документы расчётов»).
|
||||
# Позволяет вести остаток и зачёт по каждому авансу отдельно.
|
||||
origin_move_id = fields.Many2one(
|
||||
'rsbu.move', string='Документ-основание', index=True,
|
||||
ondelete='restrict')
|
||||
# Субконто «Договор» — аналог 2-го субконто 1С «Договоры» на 60/62/76.
|
||||
# Расчёты и зачёт авансов ведутся в разрезе (контрагент, договор).
|
||||
contract_id = fields.Many2one(
|
||||
'partner.contract.customer', string='Договор', index=True,
|
||||
ondelete='restrict')
|
||||
|
||||
company_id = fields.Many2one(
|
||||
related='move_id.company_id', string='Компания',
|
||||
store=True, index=True)
|
||||
currency_id = fields.Many2one(
|
||||
related='move_id.currency_id', string='Валюта', store=True)
|
||||
date = fields.Date(related='move_id.date', store=True, index=True)
|
||||
state = fields.Selection(
|
||||
related='move_id.state', store=True, index=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Ограничения
|
||||
# ------------------------------------------------------------------
|
||||
@api.constrains('debit', 'credit')
|
||||
def _check_debit_credit(self):
|
||||
for line in self:
|
||||
rounding = line.currency_id.rounding or 0.01
|
||||
debit_zero = float_is_zero(line.debit, precision_rounding=rounding)
|
||||
credit_zero = float_is_zero(line.credit, precision_rounding=rounding)
|
||||
if debit_zero and credit_zero:
|
||||
raise ValidationError(_(
|
||||
'Строка проводки должна иметь ненулевую сумму '
|
||||
'по дебету или кредиту.'))
|
||||
if not debit_zero and not credit_zero:
|
||||
raise ValidationError(_(
|
||||
'Строка проводки не может иметь одновременно дебет '
|
||||
'и кредит. Заполните только одну сторону.'))
|
||||
|
||||
@api.constrains('account_id')
|
||||
def _check_account_not_group(self):
|
||||
for line in self:
|
||||
if line.account_id.is_group:
|
||||
raise ValidationError(_(
|
||||
'Нельзя относить проводку на счёт-группу «%s».')
|
||||
% line.account_id.display_name)
|
||||
|
||||
def _has_subconto(self, dimension):
|
||||
"""Заполнено ли измерение субконто в строке."""
|
||||
self.ensure_one()
|
||||
field_name = SUBCONTO_FIELD.get(dimension)
|
||||
return bool(field_name and self[field_name])
|
||||
73
rsbu_journal/models/rsbu_trial_balance.py
Normal file
73
rsbu_journal/models/rsbu_trial_balance.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Оборотно-сальдовая ведомость (ОСВ) по РСБУ-проводкам.
|
||||
|
||||
Строится за период по проведённым строкам rsbu.move.line, сгруппированным
|
||||
по счёту плана счетов. Для каждого счёта показываются шесть показателей:
|
||||
сальдо на начало (Дт/Кт), обороты за период (Дт/Кт), сальдо на конец
|
||||
(Дт/Кт) — как в 1С.
|
||||
|
||||
Особенности:
|
||||
* сальдо активно-пассивных счетов (60/62/76) — РАЗВЁРНУТОЕ (дебиторка и
|
||||
кредиторка одновременно), считается в разрезе субконто через
|
||||
rsbu.account.get_expanded_balance;
|
||||
* иерархия счетов сворачивается снизу вверх: показатели счёта-группы =
|
||||
сумма показателей его потомков-листьев;
|
||||
* начало года для сальдо — календарное (1 января), результатные счета
|
||||
90/91/99 к концу года закрываются регламентом закрытия месяца.
|
||||
|
||||
Строки живут в рамках пользовательской сессии (TransientModel); отчёт на
|
||||
экране (QWeb-HTML) и переход к проводкам (drill-down) строятся поверх них.
|
||||
"""
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class RsbuTrialBalanceLine(models.TransientModel):
|
||||
_name = 'rsbu.trial.balance.line'
|
||||
_description = 'РСБУ — Строка оборотно-сальдовой ведомости'
|
||||
_order = 'code'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'rsbu.trial.balance.wizard', string='Отчёт', ondelete='cascade',
|
||||
index=True)
|
||||
account_id = fields.Many2one('rsbu.account', string='Счёт', index=True)
|
||||
code = fields.Char(string='Код счёта', index=True)
|
||||
name = fields.Char(string='Наименование счёта')
|
||||
level = fields.Integer(string='Уровень')
|
||||
is_group = fields.Boolean(string='Счёт-группа')
|
||||
parent_code = fields.Char(string='Код родителя')
|
||||
|
||||
company_id = fields.Many2one('res.company', string='Компания')
|
||||
currency_id = fields.Many2one('res.currency', string='Валюта')
|
||||
|
||||
opening_debit = fields.Monetary(string='Сальдо нач. Дт',
|
||||
currency_field='currency_id')
|
||||
opening_credit = fields.Monetary(string='Сальдо нач. Кт',
|
||||
currency_field='currency_id')
|
||||
turnover_debit = fields.Monetary(string='Оборот Дт',
|
||||
currency_field='currency_id')
|
||||
turnover_credit = fields.Monetary(string='Оборот Кт',
|
||||
currency_field='currency_id')
|
||||
closing_debit = fields.Monetary(string='Сальдо кон. Дт',
|
||||
currency_field='currency_id')
|
||||
closing_credit = fields.Monetary(string='Сальдо кон. Кт',
|
||||
currency_field='currency_id')
|
||||
|
||||
def action_open_entries(self):
|
||||
"""Drill-down: открыть проводки счёта за период отчёта."""
|
||||
self.ensure_one()
|
||||
wiz = self.wizard_id
|
||||
domain = [
|
||||
('account_id', 'child_of', self.account_id.id),
|
||||
('state', '=', 'posted'),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('date', '>=', wiz.date_from),
|
||||
('date', '<=', wiz.date_to),
|
||||
]
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': '%s %s' % (self.code, self.name or ''),
|
||||
'res_model': 'rsbu.move.line',
|
||||
'view_mode': 'list,form',
|
||||
'domain': domain,
|
||||
'context': {'search_default_group_account': 0},
|
||||
}
|
||||
1
rsbu_journal/report/__init__.py
Normal file
1
rsbu_journal/report/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import rsbu_trial_balance_report
|
||||
18
rsbu_journal/report/rsbu_trial_balance_report.py
Normal file
18
rsbu_journal/report/rsbu_trial_balance_report.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Отчётная модель ОСВ (QWeb-HTML на экран)."""
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class RsbuTrialBalanceReport(models.AbstractModel):
|
||||
_name = 'report.rsbu_journal.report_rsbu_trial_balance'
|
||||
_description = 'РСБУ — Отчёт ОСВ'
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
wizards = self.env['rsbu.trial.balance.wizard'].browse(docids)
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'rsbu.trial.balance.wizard',
|
||||
'docs': wizards,
|
||||
'totals_by_wizard': {w.id: w._report_totals() for w in wizards},
|
||||
}
|
||||
130
rsbu_journal/report/rsbu_trial_balance_templates.xml
Normal file
130
rsbu_journal/report/rsbu_trial_balance_templates.xml
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_rsbu_trial_balance">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.internal_layout">
|
||||
<div class="page osv-report">
|
||||
<style>
|
||||
.osv-report { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; }
|
||||
.osv-table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: inherit; }
|
||||
.osv-table th, .osv-table td { border: 1px solid #b7c3b0; padding: 3px 6px; }
|
||||
.osv-table thead th { background: #d7e4c9; text-align: center; font-weight: bold; }
|
||||
.osv-table td.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.osv-table tr.grp td { background: #eaf2e1; font-weight: bold; }
|
||||
.osv-table tr.total td { background: #cddcc0; font-weight: bold; }
|
||||
.osv-acc { white-space: nowrap; }
|
||||
</style>
|
||||
<h3>
|
||||
Оборотно-сальдовая ведомость
|
||||
</h3>
|
||||
<p>
|
||||
за период
|
||||
<strong><span t-field="doc.date_from"/></strong>
|
||||
—
|
||||
<strong><span t-field="doc.date_to"/></strong>,
|
||||
<span t-field="doc.company_id.name"/>
|
||||
</p>
|
||||
<table class="osv-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Счёт</th>
|
||||
<th rowspan="2">Наименование</th>
|
||||
<th colspan="2">Сальдо на начало периода</th>
|
||||
<th colspan="2">Обороты за период</th>
|
||||
<th colspan="2">Сальдо на конец периода</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дебет</th>
|
||||
<th>Кредит</th>
|
||||
<th>Дебет</th>
|
||||
<th>Кредит</th>
|
||||
<th>Дебет</th>
|
||||
<th>Кредит</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.line_ids" t-as="line">
|
||||
<tr t-att-class="'grp' if line.is_group else ''">
|
||||
<td class="osv-acc"
|
||||
t-att-style="'padding-left: %dpx' % (6 + 14 * (line.level or 0))">
|
||||
<span t-esc="line.code"/>
|
||||
</td>
|
||||
<td><span t-esc="line.name"/></td>
|
||||
<td class="num">
|
||||
<span t-esc="line.opening_debit"
|
||||
t-options='{"widget": "monetary", "display_currency": line.currency_id}'
|
||||
t-if="line.opening_debit"/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="line.opening_credit"
|
||||
t-options='{"widget": "monetary", "display_currency": line.currency_id}'
|
||||
t-if="line.opening_credit"/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="line.turnover_debit"
|
||||
t-options='{"widget": "monetary", "display_currency": line.currency_id}'
|
||||
t-if="line.turnover_debit"/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="line.turnover_credit"
|
||||
t-options='{"widget": "monetary", "display_currency": line.currency_id}'
|
||||
t-if="line.turnover_credit"/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="line.closing_debit"
|
||||
t-options='{"widget": "monetary", "display_currency": line.currency_id}'
|
||||
t-if="line.closing_debit"/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="line.closing_credit"
|
||||
t-options='{"widget": "monetary", "display_currency": line.currency_id}'
|
||||
t-if="line.closing_credit"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-set="tot" t-value="totals_by_wizard[doc.id]"/>
|
||||
<tr class="total">
|
||||
<td colspan="2">Итого</td>
|
||||
<td class="num">
|
||||
<span t-esc="tot['opening_debit']"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.company_id.currency_id}'/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="tot['opening_credit']"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.company_id.currency_id}'/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="tot['turnover_debit']"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.company_id.currency_id}'/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="tot['turnover_credit']"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.company_id.currency_id}'/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="tot['closing_debit']"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.company_id.currency_id}'/>
|
||||
</td>
|
||||
<td class="num">
|
||||
<span t-esc="tot['closing_credit']"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.company_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<record id="action_report_rsbu_trial_balance" model="ir.actions.report">
|
||||
<field name="name">Оборотно-сальдовая ведомость</field>
|
||||
<field name="model">rsbu.trial.balance.wizard</field>
|
||||
<field name="report_type">qweb-html</field>
|
||||
<field name="report_name">rsbu_journal.report_rsbu_trial_balance</field>
|
||||
<field name="report_file">rsbu_journal.report_rsbu_trial_balance</field>
|
||||
<field name="print_report_name">'ОСВ %s' % (object.date_to or '')</field>
|
||||
</record>
|
||||
</odoo>
|
||||
7
rsbu_journal/security/ir.model.access.csv
Normal file
7
rsbu_journal/security/ir.model.access.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_rsbu_move_user,rsbu.move user,model_rsbu_move,group_rsbu_journal_user,1,1,1,0
|
||||
access_rsbu_move_manager,rsbu.move manager,model_rsbu_move,group_rsbu_journal_manager,1,1,1,1
|
||||
access_rsbu_move_line_user,rsbu.move.line user,model_rsbu_move_line,group_rsbu_journal_user,1,1,1,1
|
||||
access_rsbu_move_line_manager,rsbu.move.line manager,model_rsbu_move_line,group_rsbu_journal_manager,1,1,1,1
|
||||
access_rsbu_tb_wizard_user,rsbu.trial.balance.wizard user,model_rsbu_trial_balance_wizard,group_rsbu_journal_user,1,1,1,1
|
||||
access_rsbu_tb_line_user,rsbu.trial.balance.line user,model_rsbu_trial_balance_line,group_rsbu_journal_user,1,1,1,1
|
||||
|
26
rsbu_journal/security/rsbu_journal_security.xml
Normal file
26
rsbu_journal/security/rsbu_journal_security.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="group_rsbu_journal_user" model="res.groups">
|
||||
<field name="name">РСБУ / Журнал: пользователь</field>
|
||||
<field name="implied_ids" eval="[(4, ref('rsbu_chart_of_accounts.group_rsbu_account_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_rsbu_journal_manager" model="res.groups">
|
||||
<field name="name">РСБУ / Журнал: менеджер</field>
|
||||
<field name="implied_ids" eval="[(4, ref('group_rsbu_journal_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="rsbu_move_company_rule" model="ir.rule">
|
||||
<field name="name">РСБУ Операция: мультикомпания</field>
|
||||
<field name="model_id" ref="model_rsbu_move"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="rsbu_move_line_company_rule" model="ir.rule">
|
||||
<field name="name">РСБУ Строка: мультикомпания</field>
|
||||
<field name="model_id" ref="model_rsbu_move_line"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
2
rsbu_journal/tests/__init__.py
Normal file
2
rsbu_journal/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import test_rsbu_move
|
||||
from . import test_trial_balance
|
||||
120
rsbu_journal/tests/test_rsbu_move.py
Normal file
120
rsbu_journal/tests/test_rsbu_move.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestRsbuMove(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.Account = cls.env['rsbu.account']
|
||||
cls.Move = cls.env['rsbu.move']
|
||||
cls.company = cls.env.company
|
||||
# Счета из типового плана (загружен post_init_hook Этапа 1).
|
||||
cls.acc_51 = cls._acc(cls, '51')
|
||||
cls.acc_6201 = cls._acc(cls, '62.01')
|
||||
cls.acc_62av = cls._acc(cls, '62.АВ')
|
||||
cls.acc_90 = cls._acc(cls, '90') # это группа? нет, синтетический
|
||||
cls.acc_9001 = cls._acc(cls, '90.01')
|
||||
cls.partner = cls.env['res.partner'].create({'name': 'Тест Клиент'})
|
||||
|
||||
def _acc(self, code):
|
||||
return self.env['rsbu.account'].search([
|
||||
('code', '=', code),
|
||||
('company_id', '=', self.env.company.id)], limit=1)
|
||||
|
||||
def _move(self, lines, date='2026-01-15'):
|
||||
return self.Move.create({
|
||||
'date': date,
|
||||
'line_ids': [(0, 0, l) for l in lines],
|
||||
})
|
||||
|
||||
def test_post_balanced(self):
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 1000.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 1000.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
move.action_post()
|
||||
self.assertEqual(move.state, 'posted')
|
||||
self.assertNotEqual(move.name, '/')
|
||||
|
||||
def test_post_unbalanced_fails(self):
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 1000.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 900.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
with self.assertRaises(UserError):
|
||||
move.action_post()
|
||||
|
||||
def test_post_no_lines_fails(self):
|
||||
move = self.Move.create({'date': '2026-01-15'})
|
||||
with self.assertRaises(UserError):
|
||||
move.action_post()
|
||||
|
||||
def test_line_one_side_only(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 100.0, 'credit': 100.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 100.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
|
||||
def test_required_subconto_blocks_post(self):
|
||||
# 62.АВ требует партнёра — не заполняем.
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 500.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 500.0},
|
||||
])
|
||||
with self.assertRaises(UserError):
|
||||
move.action_post()
|
||||
|
||||
def test_draft_and_delete(self):
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 100.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 100.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
move.action_post()
|
||||
move.action_draft()
|
||||
self.assertEqual(move.state, 'draft')
|
||||
move.unlink() # из черновика — можно
|
||||
|
||||
def test_delete_posted_fails(self):
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 100.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 100.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
move.action_post()
|
||||
with self.assertRaises(UserError):
|
||||
move.unlink()
|
||||
|
||||
def test_aggregation_after_post(self):
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 1500.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 1500.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
move.action_post()
|
||||
bal_51 = self.acc_51.get_balance(company_id=self.company.id)
|
||||
self.assertEqual(bal_51['debit'], 1500.0)
|
||||
self.assertEqual(bal_51['balance'], 1500.0)
|
||||
self.assertEqual(bal_51['side'], 'debit')
|
||||
# 62.АВ пассивный — кредитовое сальдо.
|
||||
bal_av = self.acc_62av.get_balance(company_id=self.company.id)
|
||||
self.assertEqual(bal_av['credit'], 1500.0)
|
||||
self.assertEqual(bal_av['balance'], 1500.0)
|
||||
self.assertEqual(bal_av['side'], 'credit')
|
||||
|
||||
def test_draft_moves_excluded_from_aggregation(self):
|
||||
move = self._move([
|
||||
{'account_id': self.acc_51.id, 'debit': 700.0},
|
||||
{'account_id': self.acc_62av.id, 'credit': 700.0,
|
||||
'partner_id': self.partner.id},
|
||||
])
|
||||
# не проводим
|
||||
bal = self.acc_51.get_balance(company_id=self.company.id)
|
||||
self.assertEqual(bal['debit'], 0.0)
|
||||
128
rsbu_journal/tests/test_trial_balance.py
Normal file
128
rsbu_journal/tests/test_trial_balance.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTrialBalance(TransactionCase):
|
||||
"""Оборотно-сальдовая ведомость: развёрнутое АП-сальдо, баланс, свёртка."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company = cls.env.company
|
||||
cls.Account = cls.env['rsbu.account']
|
||||
cls.Move = cls.env['rsbu.move']
|
||||
cls.Wizard = cls.env['rsbu.trial.balance.wizard']
|
||||
cls.partner_a = cls.env['res.partner'].create({'name': 'Дебитор А'})
|
||||
cls.partner_b = cls.env['res.partner'].create({'name': 'Кредитор Б'})
|
||||
|
||||
def _acc(self, code):
|
||||
return self.Account.search([
|
||||
('code', '=', code),
|
||||
('company_id', '=', self.company.id)], limit=1)
|
||||
|
||||
def _post(self, lines, date='2026-02-10'):
|
||||
move = self.Move.create({
|
||||
'date': date,
|
||||
'line_ids': [(0, 0, l) for l in lines]})
|
||||
move.action_post()
|
||||
return move
|
||||
|
||||
def test_expanded_balance_active_passive(self):
|
||||
"""Счёт 62 (АП): дебиторка и кредиторка показаны РАЗВЁРНУТО.
|
||||
|
||||
Партнёр А — долг (Дт 62.01 1000), партнёр Б — аванс (Кт 62.АВ 300).
|
||||
На синтетическом счёте 62 ОСВ должна показать Дт 1000 И Кт 300
|
||||
одновременно, а не нетто 700.
|
||||
"""
|
||||
acc51 = self._acc('51')
|
||||
acc6201 = self._acc('62.01')
|
||||
acc62av = self._acc('62.АВ')
|
||||
# Дебиторка партнёра А: Дт 62.01 — Кт 51 (1000).
|
||||
self._post([
|
||||
{'account_id': acc6201.id, 'debit': 1000.0,
|
||||
'partner_id': self.partner_a.id},
|
||||
{'account_id': acc51.id, 'credit': 1000.0},
|
||||
])
|
||||
# Аванс партнёра Б: Дт 51 — Кт 62.АВ (300).
|
||||
self._post([
|
||||
{'account_id': acc51.id, 'debit': 300.0},
|
||||
{'account_id': acc62av.id, 'credit': 300.0,
|
||||
'partner_id': self.partner_b.id},
|
||||
])
|
||||
acc62 = self._acc('62')
|
||||
exp = acc62.get_expanded_balance(
|
||||
date_to='2026-02-28', company_id=self.company.id)
|
||||
self.assertEqual(exp['debit'], 1000.0,
|
||||
'Дебиторка должна быть развёрнута в Дт')
|
||||
self.assertEqual(exp['credit'], 300.0,
|
||||
'Аванс должен быть развёрнут в Кт')
|
||||
|
||||
def test_wizard_generates_and_balances(self):
|
||||
"""ОСВ: кон.сальдо = нач.сальдо + обороты (для активного счёта)."""
|
||||
acc51 = self._acc('51')
|
||||
acc62av = self._acc('62.АВ')
|
||||
# Поступление аванса: Дт 51 — Кт 62.АВ (2000) в феврале.
|
||||
self._post([
|
||||
{'account_id': acc51.id, 'debit': 2000.0},
|
||||
{'account_id': acc62av.id, 'credit': 2000.0,
|
||||
'partner_id': self.partner_a.id},
|
||||
], date='2026-02-10')
|
||||
wiz = self.Wizard.create({
|
||||
'date_from': '2026-02-01', 'date_to': '2026-02-28',
|
||||
'company_id': self.company.id, 'hide_zero': True})
|
||||
wiz.action_generate()
|
||||
line51 = wiz.line_ids.filtered(lambda l: l.code == '51')
|
||||
self.assertTrue(line51, 'Счёт 51 должен присутствовать в ОСВ')
|
||||
# 51 активный: нач.сальдо 0, оборот Дт 2000, кон.сальдо Дт 2000.
|
||||
self.assertEqual(line51.opening_debit, 0.0)
|
||||
self.assertEqual(line51.turnover_debit, 2000.0)
|
||||
self.assertEqual(line51.closing_debit, 2000.0)
|
||||
# 62.АВ пассивный: оборот Кт 2000, кон.сальдо Кт 2000.
|
||||
line_av = wiz.line_ids.filtered(lambda l: l.code == '62.АВ')
|
||||
self.assertEqual(line_av.turnover_credit, 2000.0)
|
||||
self.assertEqual(line_av.closing_credit, 2000.0)
|
||||
|
||||
def test_opening_balance_from_prior_period(self):
|
||||
"""Нач.сальдо периода = проводки до date_from."""
|
||||
acc51 = self._acc('51')
|
||||
acc62av = self._acc('62.АВ')
|
||||
# Январь: аванс 500 (до периода).
|
||||
self._post([
|
||||
{'account_id': acc51.id, 'debit': 500.0},
|
||||
{'account_id': acc62av.id, 'credit': 500.0,
|
||||
'partner_id': self.partner_a.id},
|
||||
], date='2026-01-20')
|
||||
# Февраль: ещё 700.
|
||||
self._post([
|
||||
{'account_id': acc51.id, 'debit': 700.0},
|
||||
{'account_id': acc62av.id, 'credit': 700.0,
|
||||
'partner_id': self.partner_a.id},
|
||||
], date='2026-02-15')
|
||||
wiz = self.Wizard.create({
|
||||
'date_from': '2026-02-01', 'date_to': '2026-02-28',
|
||||
'company_id': self.company.id, 'hide_zero': True})
|
||||
wiz.action_generate()
|
||||
line51 = wiz.line_ids.filtered(lambda l: l.code == '51')
|
||||
# Нач.сальдо Дт 500 (январь), оборот Дт 700, кон.сальдо Дт 1200.
|
||||
self.assertEqual(line51.opening_debit, 500.0)
|
||||
self.assertEqual(line51.turnover_debit, 700.0)
|
||||
self.assertEqual(line51.closing_debit, 1200.0)
|
||||
|
||||
def test_hide_zero_excludes_empty_accounts(self):
|
||||
"""Опция «скрывать нулевые» убирает счета без данных."""
|
||||
acc51 = self._acc('51')
|
||||
acc62av = self._acc('62.АВ')
|
||||
self._post([
|
||||
{'account_id': acc51.id, 'debit': 100.0},
|
||||
{'account_id': acc62av.id, 'credit': 100.0,
|
||||
'partner_id': self.partner_a.id},
|
||||
], date='2026-02-10')
|
||||
wiz = self.Wizard.create({
|
||||
'date_from': '2026-02-01', 'date_to': '2026-02-28',
|
||||
'company_id': self.company.id, 'hide_zero': True})
|
||||
wiz.action_generate()
|
||||
codes = set(wiz.line_ids.mapped('code'))
|
||||
# Счёт 80 (уставный капитал) без проводок — не должен попасть.
|
||||
self.assertNotIn('80', codes)
|
||||
self.assertIn('51', codes)
|
||||
15
rsbu_journal/views/rsbu_menus.xml
Normal file
15
rsbu_journal/views/rsbu_menus.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Журнал хозяйственных операций — в разделе «Операции». -->
|
||||
<menuitem id="menu_rsbu_journal" name="Журнал хозяйственных операций"
|
||||
parent="rsbu_chart_of_accounts.menu_rsbu_operations"
|
||||
action="action_rsbu_move" sequence="10"
|
||||
groups="group_rsbu_journal_user"/>
|
||||
|
||||
<!-- Оборотно-сальдовая ведомость — отдельный пункт верхнего уровня. -->
|
||||
<menuitem id="menu_rsbu_trial_balance"
|
||||
name="Оборотно-сальдовая ведомость"
|
||||
parent="rsbu_chart_of_accounts.menu_rsbu_root"
|
||||
action="action_rsbu_trial_balance_wizard" sequence="50"
|
||||
groups="group_rsbu_journal_user"/>
|
||||
</odoo>
|
||||
110
rsbu_journal/views/rsbu_move_views.xml
Normal file
110
rsbu_journal/views/rsbu_move_views.xml
Normal file
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_rsbu_move_list" model="ir.ui.view">
|
||||
<field name="name">rsbu.move.list</field>
|
||||
<field name="model">rsbu.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Журнал операций РСБУ">
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="ref"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'posted'"
|
||||
decoration-info="state == 'draft'"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_rsbu_move_form" model="ir.ui.view">
|
||||
<field name="name">rsbu.move.form</field>
|
||||
<field name="model">rsbu.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Операция РСБУ">
|
||||
<header>
|
||||
<button name="action_post" type="object" string="Провести"
|
||||
class="btn-primary" invisible="state == 'posted'"/>
|
||||
<button name="action_draft" type="object"
|
||||
string="В черновик"
|
||||
invisible="state == 'draft'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,posted"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="ref"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id"
|
||||
groups="base.group_multi_company"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Проводки">
|
||||
<field name="line_ids">
|
||||
<list editable="bottom">
|
||||
<field name="account_id"/>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"
|
||||
optional="hide"/>
|
||||
<field name="user_id" optional="hide"/>
|
||||
<field name="employee_id"
|
||||
optional="hide"/>
|
||||
<field name="debit"/>
|
||||
<field name="credit"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<group class="oe_subtotal_footer">
|
||||
<field name="total_debit"/>
|
||||
<field name="total_credit"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Источник / Примечание">
|
||||
<group>
|
||||
<field name="source_model" readonly="1"/>
|
||||
<field name="source_res_id" readonly="1"/>
|
||||
</group>
|
||||
<field name="note"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_rsbu_move_search" model="ir.ui.view">
|
||||
<field name="name">rsbu.move.search</field>
|
||||
<field name="model">rsbu.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Поиск операций">
|
||||
<field name="name"/>
|
||||
<field name="ref"/>
|
||||
<filter name="draft" string="Черновики"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="posted" string="Проведённые"
|
||||
domain="[('state', '=', 'posted')]"/>
|
||||
<separator/>
|
||||
<filter name="group_state" string="Состояние"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter name="group_date" string="Дата"
|
||||
context="{'group_by': 'date'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rsbu_move" model="ir.actions.act_window">
|
||||
<field name="name">Журнал операций РСБУ</field>
|
||||
<field name="res_model">rsbu.move</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_rsbu_move_search"/>
|
||||
</record>
|
||||
</odoo>
|
||||
1
rsbu_journal/wizards/__init__.py
Normal file
1
rsbu_journal/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import rsbu_trial_balance_wizard
|
||||
119
rsbu_journal/wizards/rsbu_trial_balance_wizard.py
Normal file
119
rsbu_journal/wizards/rsbu_trial_balance_wizard.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Мастер формирования оборотно-сальдовой ведомости (ОСВ)."""
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class RsbuTrialBalanceWizard(models.TransientModel):
|
||||
_name = 'rsbu.trial.balance.wizard'
|
||||
_description = 'РСБУ — Мастер оборотно-сальдовой ведомости'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
date_from = fields.Date(
|
||||
string='Период с', required=True,
|
||||
default=lambda self: fields.Date.today().replace(month=1, day=1))
|
||||
date_to = fields.Date(
|
||||
string='Период по', required=True,
|
||||
default=lambda self: fields.Date.today())
|
||||
hide_zero = fields.Boolean(
|
||||
string='Скрывать счета без данных', default=True,
|
||||
help='Не показывать счета с нулевыми сальдо и оборотами.')
|
||||
only_posted = fields.Boolean(
|
||||
string='Только проведённые', default=True, readonly=True,
|
||||
help='ОСВ строится по проведённым проводкам (иных в РСБУ-журнале '
|
||||
'к учёту не принимается).')
|
||||
line_ids = fields.One2many(
|
||||
'rsbu.trial.balance.line', 'wizard_id', string='Строки ОСВ')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _iter_accounts(self):
|
||||
"""Счета компании для ОСВ (все, порядок по коду)."""
|
||||
self.ensure_one()
|
||||
return self.env['rsbu.account'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
], order='complete_code, code')
|
||||
|
||||
def _account_values(self, account):
|
||||
"""Шесть показателей ОСВ по счёту (развёрнутое сальдо для АП)."""
|
||||
self.ensure_one()
|
||||
cid = self.company_id.id
|
||||
day_before = self.date_from - timedelta(days=1)
|
||||
# Сальдо на начало (до date_from) — развёрнутое по субконто.
|
||||
opening = account.get_expanded_balance(
|
||||
date_to=day_before, company_id=cid)
|
||||
# Обороты за период (по поддереву счёта).
|
||||
turnover = account.get_turnover(
|
||||
date_from=self.date_from, date_to=self.date_to,
|
||||
company_id=cid, include_children=True)
|
||||
# Сальдо на конец (до date_to включительно) — развёрнутое.
|
||||
closing = account.get_expanded_balance(
|
||||
date_to=self.date_to, company_id=cid)
|
||||
return {
|
||||
'opening_debit': opening['debit'],
|
||||
'opening_credit': opening['credit'],
|
||||
'turnover_debit': turnover['debit'],
|
||||
'turnover_credit': turnover['credit'],
|
||||
'closing_debit': closing['debit'],
|
||||
'closing_credit': closing['credit'],
|
||||
}
|
||||
|
||||
def _is_zero_row(self, vals):
|
||||
rounding = self.company_id.currency_id.rounding or 0.01
|
||||
return all(float_is_zero(vals[f], precision_rounding=rounding)
|
||||
for f in ('opening_debit', 'opening_credit',
|
||||
'turnover_debit', 'turnover_credit',
|
||||
'closing_debit', 'closing_credit'))
|
||||
|
||||
def action_generate(self):
|
||||
"""Пересчитать строки ОСВ и открыть отчёт на экране (QWeb-HTML)."""
|
||||
self.ensure_one()
|
||||
self.line_ids.unlink()
|
||||
Line = self.env['rsbu.trial.balance.line']
|
||||
currency = self.company_id.currency_id
|
||||
vals_list = []
|
||||
for account in self._iter_accounts():
|
||||
vals = self._account_values(account)
|
||||
if self.hide_zero and self._is_zero_row(vals):
|
||||
continue
|
||||
vals.update({
|
||||
'wizard_id': self.id,
|
||||
'account_id': account.id,
|
||||
'code': account.code,
|
||||
'name': account.name,
|
||||
'level': account.level,
|
||||
'is_group': bool(account.child_ids),
|
||||
'parent_code': account.parent_id.code or False,
|
||||
'company_id': self.company_id.id,
|
||||
'currency_id': currency.id,
|
||||
})
|
||||
vals_list.append(vals)
|
||||
Line.create(vals_list)
|
||||
return self.action_view_report()
|
||||
|
||||
def action_view_report(self):
|
||||
"""Открыть отчёт ОСВ (QWeb-HTML) для текущего мастера."""
|
||||
self.ensure_one()
|
||||
return self.env.ref(
|
||||
'rsbu_journal.action_report_rsbu_trial_balance').report_action(self)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _report_totals(self):
|
||||
"""Итоги ОСВ — только по счетам верхнего уровня (без двойного счёта).
|
||||
|
||||
Итог считается по корневым счетам (level 0), чтобы не суммировать
|
||||
родителя и его потомков дважды.
|
||||
"""
|
||||
self.ensure_one()
|
||||
roots = self.line_ids.filtered(lambda l: not l.parent_code)
|
||||
return {
|
||||
'opening_debit': sum(roots.mapped('opening_debit')),
|
||||
'opening_credit': sum(roots.mapped('opening_credit')),
|
||||
'turnover_debit': sum(roots.mapped('turnover_debit')),
|
||||
'turnover_credit': sum(roots.mapped('turnover_credit')),
|
||||
'closing_debit': sum(roots.mapped('closing_debit')),
|
||||
'closing_credit': sum(roots.mapped('closing_credit')),
|
||||
}
|
||||
58
rsbu_journal/wizards/rsbu_trial_balance_wizard_views.xml
Normal file
58
rsbu_journal/wizards/rsbu_trial_balance_wizard_views.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Мастер параметров ОСВ. -->
|
||||
<record id="view_rsbu_trial_balance_wizard_form" model="ir.ui.view">
|
||||
<field name="name">rsbu.trial.balance.wizard.form</field>
|
||||
<field name="model">rsbu.trial.balance.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Оборотно-сальдовая ведомость">
|
||||
<group>
|
||||
<group>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id"
|
||||
groups="base.group_multi_company"/>
|
||||
<field name="hide_zero"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_generate" type="object"
|
||||
string="Сформировать" class="btn-primary"/>
|
||||
<button string="Отмена" special="cancel"
|
||||
class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rsbu_trial_balance_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Оборотно-сальдовая ведомость</field>
|
||||
<field name="res_model">rsbu.trial.balance.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<!-- Табличный просмотр строк ОСВ с drill-down по счёту. -->
|
||||
<record id="view_rsbu_trial_balance_line_list" model="ir.ui.view">
|
||||
<field name="name">rsbu.trial.balance.line.list</field>
|
||||
<field name="model">rsbu.trial.balance.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="ОСВ" create="false" delete="false"
|
||||
default_order="code">
|
||||
<field name="code" string="Счёт"/>
|
||||
<field name="name" string="Наименование"/>
|
||||
<field name="opening_debit" sum="Сальдо нач. Дт"/>
|
||||
<field name="opening_credit" sum="Сальдо нач. Кт"/>
|
||||
<field name="turnover_debit" sum="Оборот Дт"/>
|
||||
<field name="turnover_credit" sum="Оборот Кт"/>
|
||||
<field name="closing_debit" sum="Сальдо кон. Дт"/>
|
||||
<field name="closing_credit" sum="Сальдо кон. Кт"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<button name="action_open_entries" type="object"
|
||||
string="Проводки" icon="fa-search"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user