Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
62
rsbu_chart_of_accounts/README.rst
Normal file
62
rsbu_chart_of_accounts/README.rst
Normal file
@@ -0,0 +1,62 @@
|
||||
================
|
||||
РСБУ — План счетов
|
||||
================
|
||||
|
||||
.. |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:
|
||||
|
||||
Возможности
|
||||
===========
|
||||
|
||||
* Иерархический план счетов произвольной глубины (счета и субсчета)
|
||||
* Признак счёта: активный, пассивный, активно-пассивный, забалансовый
|
||||
* Флаги обязательных субконто (контрагент, товар, пользователь, сотрудник)
|
||||
* Расчёт оборотов и сальдо по дереву субсчетов
|
||||
* Типовой план счетов 94н как готовые данные
|
||||
* Ведение отдельного плана счетов для каждой компании
|
||||
|
||||
Настройка
|
||||
=========
|
||||
|
||||
#. Установите модуль — типовой план счетов 94н загрузится автоматически.
|
||||
#. Откройте *РСБУ > Настройка > План счетов*, чтобы просмотреть или
|
||||
скорректировать счета.
|
||||
#. При необходимости загрузите типовой план счетов повторно через
|
||||
соответствующий мастер.
|
||||
|
||||
Использование
|
||||
=============
|
||||
|
||||
#. Перейдите в *РСБУ > План счетов*.
|
||||
#. Просматривайте иерархию счетов, обороты и сальдо.
|
||||
#. Для каждого счёта настройте обязательность субконто, чтобы система
|
||||
контролировала аналитику при проведении проводок.
|
||||
|
||||
Авторы
|
||||
======
|
||||
|
||||
* MK.Lab, RuOdoo
|
||||
|
||||
Модуль сопровождается MK.Lab, RuOdoo.
|
||||
|
||||
.. image:: https://ruodoo.ru/logo.png
|
||||
:alt: MK.Lab, RuOdoo
|
||||
:target: https://ruodoo.ru
|
||||
|
||||
Модуль является частью РСБУ-слоя проекта РуОду.
|
||||
9
rsbu_chart_of_accounts/__init__.py
Normal file
9
rsbu_chart_of_accounts/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
||||
|
||||
def post_init_load_chart(env):
|
||||
"""Загрузить типовой план счетов 94н для всех существующих компаний."""
|
||||
Wizard = env['rsbu.account.load.wizard']
|
||||
for company in env['res.company'].search([]):
|
||||
Wizard.create({'company_id': company.id}).action_load()
|
||||
40
rsbu_chart_of_accounts/__manifest__.py
Normal file
40
rsbu_chart_of_accounts/__manifest__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'РСБУ — План счетов',
|
||||
'version': '19.0.2026.09.17',
|
||||
'summary': 'План счетов бухгалтерского учёта РСБУ (Приказ Минфина № 94н)',
|
||||
'description': """
|
||||
РСБУ — План счетов
|
||||
==================
|
||||
|
||||
Независимый от account.account иерархический план счетов РСБУ по Приказу
|
||||
Минфина России № 94н. Базовый фундамент РСБУ-слоя.
|
||||
|
||||
Возможности:
|
||||
------------
|
||||
* Иерархия счетов произвольной глубины
|
||||
* Признак счёта: активный / пассивный / активно-пассивный / забалансовый
|
||||
* Флаги обязательных субконто (partner / product / user / employee)
|
||||
* Методы агрегации сумм и сальдо по дереву субсчетов
|
||||
* Типовой план счетов 94н как предустановленные данные
|
||||
* Мультикомпанийность (свой план на каждую компанию)
|
||||
|
||||
Нативная бухгалтерия Odoo не затрагивается.
|
||||
""",
|
||||
'author': 'MK.Lab, RuOdoo',
|
||||
'website': 'https://ruodoo.ru',
|
||||
'category': 'Accounting/Localizations',
|
||||
'depends': ['base', 'mail'],
|
||||
'data': [
|
||||
'security/rsbu_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizards/rsbu_account_load_wizard_views.xml',
|
||||
'views/rsbu_account_views.xml',
|
||||
'views/rsbu_menus.xml',
|
||||
],
|
||||
'post_init_hook': 'post_init_load_chart',
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
'maintainers': ['mklab', 'ruodoo'],
|
||||
}
|
||||
1
rsbu_chart_of_accounts/models/__init__.py
Normal file
1
rsbu_chart_of_accounts/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import rsbu_account
|
||||
314
rsbu_chart_of_accounts/models/rsbu_account.py
Normal file
314
rsbu_chart_of_accounts/models/rsbu_account.py
Normal file
@@ -0,0 +1,314 @@
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
# Соответствие измерений субконто моделям Odoo.
|
||||
SUBCONTO_MODELS = {
|
||||
'partner': 'res.partner',
|
||||
'product': 'product.product',
|
||||
'user': 'res.users',
|
||||
'employee': 'hr.employee',
|
||||
}
|
||||
|
||||
|
||||
class RsbuAccount(models.Model):
|
||||
"""План счетов РСБУ (Приказ Минфина № 94н).
|
||||
|
||||
Независимая от account.account сущность. Иерархия произвольной глубины,
|
||||
признак счёта, флаги обязательных субконто и методы агрегации по дереву.
|
||||
Источник проводок для агрегации подключается на Этапе 2 через хук
|
||||
``_read_postings`` (здесь возвращает пустой набор).
|
||||
"""
|
||||
_name = 'rsbu.account'
|
||||
_description = 'РСБУ — Счёт плана счетов'
|
||||
_parent_store = True
|
||||
_parent_name = 'parent_id'
|
||||
_order = 'complete_code, code'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(string='Наименование', required=True, translate=True)
|
||||
code = fields.Char(string='Код счёта', required=True, index=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания', required=True, index=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
parent_id = fields.Many2one(
|
||||
'rsbu.account', string='Родительский счёт',
|
||||
ondelete='restrict', index=True, check_company=True,
|
||||
)
|
||||
child_ids = fields.One2many('rsbu.account', 'parent_id', string='Субсчета')
|
||||
parent_path = fields.Char(index=True)
|
||||
|
||||
is_group = fields.Boolean(
|
||||
string='Группа (счёт-агрегатор)',
|
||||
help='Счёт-группа служит только для агрегации; относить на него '
|
||||
'проводки запрещено (проверяется на Этапе 2).',
|
||||
)
|
||||
account_kind = fields.Selection(
|
||||
selection=[
|
||||
('active', 'Активный'),
|
||||
('passive', 'Пассивный'),
|
||||
('active_passive', 'Активно-пассивный'),
|
||||
('off_balance', 'Забалансовый'),
|
||||
],
|
||||
string='Признак счёта', required=True, default='active',
|
||||
)
|
||||
|
||||
subconto_partner = fields.Boolean(string='Субконто: Контрагент')
|
||||
subconto_product = fields.Boolean(string='Субконто: Номенклатура')
|
||||
subconto_user = fields.Boolean(string='Субконто: Пользователь')
|
||||
subconto_employee = fields.Boolean(string='Субконто: Сотрудник')
|
||||
subconto_origin = fields.Boolean(string='Субконто: Документ-основание')
|
||||
subconto_contract = fields.Boolean(string='Субконто: Договор')
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
note = fields.Text(string='Примечание')
|
||||
|
||||
complete_code = fields.Char(
|
||||
string='Полный код', compute='_compute_complete_code',
|
||||
store=True, recursive=True,
|
||||
)
|
||||
level = fields.Integer(string='Уровень', compute='_compute_level')
|
||||
|
||||
_code_company_uniq = models.Constraint(
|
||||
'unique(code, company_id)',
|
||||
'Код счёта должен быть уникальным в пределах компании.',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Вычисляемые поля
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('code', 'parent_id.complete_code')
|
||||
def _compute_complete_code(self):
|
||||
for account in self:
|
||||
if account.parent_id:
|
||||
account.complete_code = '%s / %s' % (
|
||||
account.parent_id.complete_code or account.parent_id.code,
|
||||
account.code,
|
||||
)
|
||||
else:
|
||||
account.complete_code = account.code
|
||||
|
||||
@api.depends('parent_path')
|
||||
def _compute_level(self):
|
||||
for account in self:
|
||||
account.level = (account.parent_path or '').count('/') - 1 \
|
||||
if account.parent_path else 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Ограничения
|
||||
# ------------------------------------------------------------------
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_recursion(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(
|
||||
_('Нельзя создать циклическую иерархию счетов.'))
|
||||
|
||||
@api.constrains('parent_id', 'company_id')
|
||||
def _check_parent_company(self):
|
||||
for account in self:
|
||||
if account.parent_id and \
|
||||
account.parent_id.company_id != account.company_id:
|
||||
raise ValidationError(_(
|
||||
'Родительский счёт должен принадлежать той же компании.'))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_has_children(self):
|
||||
for account in self:
|
||||
if account.child_ids:
|
||||
raise ValidationError(_(
|
||||
'Нельзя удалить счёт «%s»: у него есть субсчета. '
|
||||
'Сначала удалите или перенесите субсчета, либо '
|
||||
'заархивируйте счёт.') % account.display_name)
|
||||
|
||||
@api.depends('code', 'name')
|
||||
def _compute_display_name(self):
|
||||
for account in self:
|
||||
account.display_name = '%s %s' % (account.code, account.name)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Субконто
|
||||
# ------------------------------------------------------------------
|
||||
def get_required_subconto(self):
|
||||
"""Список обязательных измерений субконто для счёта.
|
||||
|
||||
:return: list[str] из ('partner', 'product', 'user', 'employee')
|
||||
"""
|
||||
self.ensure_one()
|
||||
required = []
|
||||
if self.subconto_partner:
|
||||
required.append('partner')
|
||||
if self.subconto_product:
|
||||
required.append('product')
|
||||
if self.subconto_user:
|
||||
required.append('user')
|
||||
if self.subconto_employee:
|
||||
required.append('employee')
|
||||
return required
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Иерархия и агрегация
|
||||
# ------------------------------------------------------------------
|
||||
def _get_subtree_account_ids(self):
|
||||
"""ID счёта(ов) и всех их потомков."""
|
||||
return self.search([('id', 'child_of', self.ids)]).ids
|
||||
|
||||
def _read_postings(self, domain=None):
|
||||
"""Хук чтения проводок для агрегации.
|
||||
|
||||
На Этапе 1 источника проводок нет — возвращается пустой recordset.
|
||||
На Этапе 2 (модуль rsbu_journal) метод переопределяется и читает
|
||||
проведённые строки rsbu.move.line.
|
||||
"""
|
||||
return self.env['rsbu.account'].browse() # пустой набор-заглушка
|
||||
|
||||
def get_turnover(self, date_from=None, date_to=None, company_id=None,
|
||||
include_children=True, subconto=None):
|
||||
"""Обороты Дт/Кт за период по счёту (с учётом потомков).
|
||||
|
||||
:param subconto: dict фильтров по измерениям, напр.
|
||||
{'partner': partner_id} — ограничить обороты этим субконто.
|
||||
"""
|
||||
self.ensure_one()
|
||||
lines = self._collect_posting_lines(
|
||||
date_from, date_to, company_id, include_children, subconto)
|
||||
debit = sum(getattr(l, 'debit', 0.0) for l in lines)
|
||||
credit = sum(getattr(l, 'credit', 0.0) for l in lines)
|
||||
return {'debit': debit, 'credit': credit}
|
||||
|
||||
def get_balance(self, date_from=None, date_to=None, company_id=None,
|
||||
include_children=True, subconto=None):
|
||||
"""Сальдо по счёту (с учётом потомков и признака счёта).
|
||||
|
||||
:return: dict с ключами debit, credit, balance, side.
|
||||
"""
|
||||
self.ensure_one()
|
||||
turnover = self.get_turnover(
|
||||
date_from, date_to, company_id, include_children, subconto)
|
||||
debit = turnover['debit']
|
||||
credit = turnover['credit']
|
||||
raw = debit - credit # дебетовое сальдо положительно
|
||||
if self.account_kind == 'passive':
|
||||
balance = credit - debit
|
||||
side = 'credit' if balance >= 0 else 'debit'
|
||||
elif self.account_kind == 'active':
|
||||
balance = raw
|
||||
side = 'debit' if balance >= 0 else 'credit'
|
||||
else: # active_passive / off_balance
|
||||
balance = abs(raw)
|
||||
side = 'debit' if raw >= 0 else 'credit'
|
||||
return {
|
||||
'debit': debit,
|
||||
'credit': credit,
|
||||
'balance': balance,
|
||||
'side': side,
|
||||
}
|
||||
|
||||
def get_origin_balances(self, date_to=None, company_id=None,
|
||||
subconto=None):
|
||||
"""Сальдо счёта в разрезе субконто «Документ-основание» (origin).
|
||||
|
||||
Возвращает список dict {'origin': rsbu.move, 'debit', 'credit',
|
||||
'balance', 'side'} по каждому origin_move_id с ненулевым остатком,
|
||||
упорядоченный по дате/номеру основания (FIFO). Служит для зачёта
|
||||
авансов по каждому основанию отдельно (аналог 3-го субконто 1С).
|
||||
"""
|
||||
self.ensure_one()
|
||||
lines = self._collect_posting_lines(
|
||||
None, date_to, company_id, True, subconto)
|
||||
by_origin = {}
|
||||
for ln in lines:
|
||||
origin = ln.origin_move_id
|
||||
if not origin:
|
||||
continue
|
||||
acc = by_origin.setdefault(
|
||||
origin.id, {'origin': origin, 'debit': 0.0, 'credit': 0.0})
|
||||
acc['debit'] += ln.debit
|
||||
acc['credit'] += ln.credit
|
||||
result = []
|
||||
for data in by_origin.values():
|
||||
raw = data['debit'] - data['credit']
|
||||
if self.account_kind == 'passive':
|
||||
balance = data['credit'] - data['debit']
|
||||
side = 'credit' if balance >= 0 else 'debit'
|
||||
elif self.account_kind == 'active':
|
||||
balance = raw
|
||||
side = 'debit' if balance >= 0 else 'credit'
|
||||
else:
|
||||
balance = abs(raw)
|
||||
side = 'debit' if raw >= 0 else 'credit'
|
||||
data['balance'] = balance
|
||||
data['side'] = side
|
||||
result.append(data)
|
||||
# FIFO: по дате и номеру проводки-основания.
|
||||
result.sort(key=lambda d: (d['origin'].date, d['origin'].name or '',
|
||||
d['origin'].id))
|
||||
return result
|
||||
|
||||
def get_expanded_balance(self, date_to=None, date_from=None,
|
||||
company_id=None):
|
||||
"""Развёрнутое сальдо счёта (Дт и Кт одновременно), как в 1С.
|
||||
|
||||
Для активно-пассивных счетов (60/62/76) простое сальдо сворачивает
|
||||
дебиторку и кредиторку в нетто, что искажает ОСВ. Здесь остаток
|
||||
считается в разрезе субконто (счёт-лист × контрагент × договор ×
|
||||
документ-основание): по каждой аналитике вычисляется её сальдо со
|
||||
знаком по признаку счёта, затем дебетовые остатки суммируются в
|
||||
`debit`, кредитовые — в `credit`.
|
||||
|
||||
:return: dict {'debit': <сумма дебетовых остатков>,
|
||||
'credit': <сумма кредитовых остатков>}.
|
||||
"""
|
||||
self.ensure_one()
|
||||
lines = self._collect_posting_lines(
|
||||
date_from, date_to, company_id, True, None)
|
||||
# Группировка по аналитике листа: (счёт, партнёр, договор, основание).
|
||||
groups = {}
|
||||
for ln in lines:
|
||||
key = (ln.account_id.id,
|
||||
ln.partner_id.id or 0,
|
||||
ln.contract_id.id or 0,
|
||||
ln.origin_move_id.id or 0)
|
||||
g = groups.setdefault(
|
||||
key, {'account': ln.account_id, 'debit': 0.0, 'credit': 0.0})
|
||||
g['debit'] += ln.debit
|
||||
g['credit'] += ln.credit
|
||||
debit_total = 0.0
|
||||
credit_total = 0.0
|
||||
for g in groups.values():
|
||||
raw = round(g['debit'] - g['credit'], 2)
|
||||
kind = g['account'].account_kind
|
||||
if kind == 'active':
|
||||
side = 'debit' if raw >= 0 else 'credit'
|
||||
elif kind == 'passive':
|
||||
side = 'credit' if raw <= 0 else 'debit'
|
||||
else: # active_passive / off_balance — по знаку остатка
|
||||
side = 'debit' if raw >= 0 else 'credit'
|
||||
if side == 'debit':
|
||||
debit_total += abs(raw)
|
||||
else:
|
||||
credit_total += abs(raw)
|
||||
return {'debit': round(debit_total, 2),
|
||||
'credit': round(credit_total, 2)}
|
||||
|
||||
def _collect_posting_lines(self, date_from, date_to, company_id,
|
||||
include_children, subconto=None):
|
||||
"""Собрать строки проводок для агрегации через хук _read_postings."""
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
if date_from:
|
||||
domain.append(('date', '>=', date_from))
|
||||
if date_to:
|
||||
domain.append(('date', '<=', date_to))
|
||||
if company_id:
|
||||
domain.append(('company_id', '=', company_id))
|
||||
# Фильтр по субконто (напр. остаток по партнёру).
|
||||
if subconto:
|
||||
for dim, value in subconto.items():
|
||||
field_name = {
|
||||
'partner': 'partner_id', 'product': 'product_id',
|
||||
'user': 'user_id', 'employee': 'employee_id',
|
||||
'origin': 'origin_move_id', 'contract': 'contract_id',
|
||||
}.get(dim)
|
||||
if field_name and value:
|
||||
domain.append((field_name, '=', value))
|
||||
return self._read_postings(domain)
|
||||
4
rsbu_chart_of_accounts/security/ir.model.access.csv
Normal file
4
rsbu_chart_of_accounts/security/ir.model.access.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_rsbu_account_user,rsbu.account user,model_rsbu_account,group_rsbu_account_user,1,0,0,0
|
||||
access_rsbu_account_manager,rsbu.account manager,model_rsbu_account,group_rsbu_account_manager,1,1,1,1
|
||||
access_rsbu_account_load_wizard,rsbu.account.load.wizard manager,model_rsbu_account_load_wizard,group_rsbu_account_manager,1,1,1,1
|
||||
|
19
rsbu_chart_of_accounts/security/rsbu_security.xml
Normal file
19
rsbu_chart_of_accounts/security/rsbu_security.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="group_rsbu_account_user" model="res.groups">
|
||||
<field name="name">РСБУ / План счетов: чтение</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_rsbu_account_manager" model="res.groups">
|
||||
<field name="name">РСБУ / План счетов: администрирование</field>
|
||||
<field name="implied_ids" eval="[(4, ref('group_rsbu_account_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="rsbu_account_company_rule" model="ir.rule">
|
||||
<field name="name">РСБУ Счёт: мультикомпания</field>
|
||||
<field name="model_id" ref="model_rsbu_account"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
<field name="global" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
1
rsbu_chart_of_accounts/tests/__init__.py
Normal file
1
rsbu_chart_of_accounts/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_rsbu_account
|
||||
77
rsbu_chart_of_accounts/tests/test_rsbu_account.py
Normal file
77
rsbu_chart_of_accounts/tests/test_rsbu_account.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from psycopg2 import IntegrityError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestRsbuAccount(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.Account = cls.env['rsbu.account']
|
||||
cls.company = cls.env.company
|
||||
cls.company2 = cls.env['res.company'].create({'name': 'RSBU Co2'})
|
||||
|
||||
def _make(self, code, name='Счёт', kind='active', parent=None, **kw):
|
||||
vals = {'code': code, 'name': name, 'account_kind': kind}
|
||||
if parent:
|
||||
vals['parent_id'] = parent.id
|
||||
vals.update(kw)
|
||||
return self.Account.create(vals)
|
||||
|
||||
def test_code_unique_per_company(self):
|
||||
self._make('T01')
|
||||
with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'):
|
||||
self._make('T01')
|
||||
|
||||
def test_code_reused_across_companies(self):
|
||||
a = self._make('T02')
|
||||
b = self._make('T02', company_id=self.company2.id)
|
||||
self.assertNotEqual(a.id, b.id)
|
||||
|
||||
def test_no_cycle(self):
|
||||
a = self._make('T10')
|
||||
b = self._make('T11', parent=a)
|
||||
# Рекурсию ловит ядро parent_store (UserError "Recursion Detected")
|
||||
# ещё до нашего @api.constrains.
|
||||
with self.assertRaises(UserError):
|
||||
a.parent_id = b.id
|
||||
a.flush_recordset()
|
||||
|
||||
def test_unlink_with_children_forbidden(self):
|
||||
a = self._make('T20')
|
||||
self._make('T21', parent=a)
|
||||
with self.assertRaises(ValidationError):
|
||||
a.unlink()
|
||||
|
||||
def test_required_subconto(self):
|
||||
a = self._make('T30', subconto_partner=True, subconto_employee=True)
|
||||
self.assertEqual(set(a.get_required_subconto()), {'partner', 'employee'})
|
||||
|
||||
def test_aggregation_zero_without_postings(self):
|
||||
a = self._make('T40')
|
||||
bal = a.get_balance()
|
||||
self.assertEqual(bal['debit'], 0.0)
|
||||
self.assertEqual(bal['credit'], 0.0)
|
||||
self.assertEqual(bal['balance'], 0.0)
|
||||
|
||||
def test_subtree_ids(self):
|
||||
a = self._make('T50', is_group=True)
|
||||
b = self._make('T50.1', parent=a)
|
||||
c = self._make('T50.2', parent=a)
|
||||
ids = a._get_subtree_account_ids()
|
||||
self.assertIn(a.id, ids)
|
||||
self.assertIn(b.id, ids)
|
||||
self.assertIn(c.id, ids)
|
||||
|
||||
def test_chart_loaded_for_company(self):
|
||||
# post_init_hook должен был загрузить типовой план для компании.
|
||||
acc51 = self.Account.search([
|
||||
('code', '=', '51'), ('company_id', '=', self.company.id)])
|
||||
self.assertTrue(acc51, 'Счёт 51 должен быть загружен из типового плана')
|
||||
acc62av = self.Account.search([
|
||||
('code', '=', '62.АВ'), ('company_id', '=', self.company.id)])
|
||||
self.assertTrue(acc62av, 'Субсчёт 62.АВ должен быть загружен')
|
||||
self.assertEqual(acc62av.parent_id.code, '62')
|
||||
87
rsbu_chart_of_accounts/views/rsbu_account_views.xml
Normal file
87
rsbu_chart_of_accounts/views/rsbu_account_views.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_rsbu_account_list" model="ir.ui.view">
|
||||
<field name="name">rsbu.account.list</field>
|
||||
<field name="model">rsbu.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="План счетов РСБУ">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="account_kind"/>
|
||||
<field name="is_group"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_rsbu_account_form" model="ir.ui.view">
|
||||
<field name="name">rsbu.account.form</field>
|
||||
<field name="model">rsbu.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Счёт РСБУ">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="account_kind"/>
|
||||
<field name="is_group"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Обязательные субконто">
|
||||
<field name="subconto_partner"/>
|
||||
<field name="subconto_product"/>
|
||||
<field name="subconto_user"/>
|
||||
<field name="subconto_employee"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Субсчета">
|
||||
<field name="child_ids">
|
||||
<list>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="account_kind"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Примечание">
|
||||
<field name="note"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_rsbu_account_search" model="ir.ui.view">
|
||||
<field name="name">rsbu.account.search</field>
|
||||
<field name="model">rsbu.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Поиск по плану счетов">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<filter name="groups" string="Только группы"
|
||||
domain="[('is_group', '=', True)]"/>
|
||||
<filter name="postable" string="Постингуемые"
|
||||
domain="[('is_group', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter name="group_kind" string="Признак счёта"
|
||||
context="{'group_by': 'account_kind'}"/>
|
||||
<filter name="group_parent" string="Родитель"
|
||||
context="{'group_by': 'parent_id'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rsbu_account" model="ir.actions.act_window">
|
||||
<field name="name">План счетов РСБУ</field>
|
||||
<field name="res_model">rsbu.account</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_rsbu_account_search"/>
|
||||
</record>
|
||||
</odoo>
|
||||
30
rsbu_chart_of_accounts/views/rsbu_menus.xml
Normal file
30
rsbu_chart_of_accounts/views/rsbu_menus.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Корень раздела РСБУ. -->
|
||||
<menuitem id="menu_rsbu_root" name="РСБУ"
|
||||
groups="group_rsbu_account_user"/>
|
||||
|
||||
<!-- Контейнерные разделы верхнего уровня (порядок задаётся sequence). -->
|
||||
<menuitem id="menu_rsbu_operations" name="Операции"
|
||||
parent="menu_rsbu_root" sequence="10"/>
|
||||
|
||||
<menuitem id="menu_rsbu_regular_ops" name="Регламентные операции"
|
||||
parent="menu_rsbu_root" sequence="20"/>
|
||||
|
||||
<menuitem id="menu_rsbu_reports" name="Отчётность"
|
||||
parent="menu_rsbu_root" sequence="30"/>
|
||||
|
||||
<!-- Настройки (бывшая «Настройка»). -->
|
||||
<menuitem id="menu_rsbu_config" name="Настройки"
|
||||
parent="menu_rsbu_root" sequence="90"/>
|
||||
|
||||
<!-- План счетов — внутри «Настройки». -->
|
||||
<menuitem id="menu_rsbu_account" name="План счетов"
|
||||
parent="menu_rsbu_config" action="action_rsbu_account"
|
||||
sequence="5"/>
|
||||
|
||||
<menuitem id="menu_rsbu_account_load" name="Загрузить типовой план 94н"
|
||||
parent="menu_rsbu_config"
|
||||
action="action_rsbu_account_load_wizard"
|
||||
groups="group_rsbu_account_manager" sequence="10"/>
|
||||
</odoo>
|
||||
1
rsbu_chart_of_accounts/wizards/__init__.py
Normal file
1
rsbu_chart_of_accounts/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import rsbu_account_load_wizard
|
||||
177
rsbu_chart_of_accounts/wizards/rsbu_account_load_wizard.py
Normal file
177
rsbu_chart_of_accounts/wizards/rsbu_account_load_wizard.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from odoo import fields, models, _
|
||||
|
||||
# Типовой план счетов по Приказу Минфина № 94н.
|
||||
# Формат: (код, наименование, признак, [субконто]).
|
||||
# Признак: A - активный, P - пассивный, AP - активно-пассивный, OB - забалансовый.
|
||||
# Субконто: p - partner, pr - product, u - user, e - employee.
|
||||
CHART_94N = [
|
||||
# Раздел I. Внеоборотные активы
|
||||
('01', 'Основные средства', 'A', []),
|
||||
('02', 'Амортизация основных средств', 'P', []),
|
||||
('03', 'Доходные вложения в материальные ценности', 'A', []),
|
||||
('04', 'Нематериальные активы', 'A', []),
|
||||
('05', 'Амортизация нематериальных активов', 'P', []),
|
||||
('07', 'Оборудование к установке', 'A', []),
|
||||
('08', 'Вложения во внеоборотные активы', 'A', []),
|
||||
('09', 'Отложенные налоговые активы', 'A', []),
|
||||
# Раздел II. Производственные запасы
|
||||
('10', 'Материалы', 'A', ['pr']),
|
||||
('11', 'Животные на выращивании и откорме', 'A', []),
|
||||
('14', 'Резервы под снижение стоимости материальных ценностей', 'P', []),
|
||||
('15', 'Заготовление и приобретение материальных ценностей', 'A', []),
|
||||
('16', 'Отклонение в стоимости материальных ценностей', 'AP', []),
|
||||
('19', 'Налог на добавленную стоимость по приобретённым ценностям', 'A', []),
|
||||
# Раздел III. Затраты на производство
|
||||
('20', 'Основное производство', 'A', []),
|
||||
('21', 'Полуфабрикаты собственного производства', 'A', []),
|
||||
('23', 'Вспомогательные производства', 'A', []),
|
||||
('25', 'Общепроизводственные расходы', 'A', []),
|
||||
('26', 'Общехозяйственные расходы', 'A', []),
|
||||
('28', 'Брак в производстве', 'A', []),
|
||||
('29', 'Обслуживающие производства и хозяйства', 'A', []),
|
||||
# Раздел IV. Готовая продукция и товары
|
||||
('40', 'Выпуск продукции (работ, услуг)', 'AP', []),
|
||||
('41', 'Товары', 'A', ['pr']),
|
||||
('42', 'Торговая наценка', 'P', []),
|
||||
('43', 'Готовая продукция', 'A', ['pr']),
|
||||
('44', 'Расходы на продажу', 'A', []),
|
||||
('45', 'Товары отгруженные', 'A', []),
|
||||
('46', 'Выполненные этапы по незавершённым работам', 'A', []),
|
||||
# Раздел V. Денежные средства
|
||||
('50', 'Касса', 'A', []),
|
||||
('51', 'Расчётные счета', 'A', []),
|
||||
('52', 'Валютные счета', 'A', []),
|
||||
('55', 'Специальные счета в банках', 'A', []),
|
||||
('57', 'Переводы в пути', 'A', []),
|
||||
('58', 'Финансовые вложения', 'A', []),
|
||||
('59', 'Резервы под обесценение финансовых вложений', 'P', []),
|
||||
# Раздел VI. Расчёты
|
||||
('60', 'Расчёты с поставщиками и подрядчиками', 'AP', ['p']),
|
||||
('62', 'Расчёты с покупателями и заказчиками', 'AP', ['p']),
|
||||
('63', 'Резервы по сомнительным долгам', 'P', []),
|
||||
('66', 'Расчёты по краткосрочным кредитам и займам', 'P', ['p']),
|
||||
('67', 'Расчёты по долгосрочным кредитам и займам', 'P', ['p']),
|
||||
('68', 'Расчёты по налогам и сборам', 'AP', []),
|
||||
('69', 'Расчёты по социальному страхованию и обеспечению', 'AP', []),
|
||||
('70', 'Расчёты с персоналом по оплате труда', 'P', ['e']),
|
||||
('71', 'Расчёты с подотчётными лицами', 'AP', ['e']),
|
||||
('73', 'Расчёты с персоналом по прочим операциям', 'AP', ['e']),
|
||||
('75', 'Расчёты с учредителями', 'AP', ['p']),
|
||||
('76', 'Расчёты с разными дебиторами и кредиторами', 'AP', ['p']),
|
||||
('77', 'Отложенные налоговые обязательства', 'P', []),
|
||||
('79', 'Внутрихозяйственные расчёты', 'AP', []),
|
||||
# Раздел VII. Капитал
|
||||
('80', 'Уставный капитал', 'P', []),
|
||||
('81', 'Собственные акции (доли)', 'A', []),
|
||||
('82', 'Резервный капитал', 'P', []),
|
||||
('83', 'Добавочный капитал', 'P', []),
|
||||
('84', 'Нераспределённая прибыль (непокрытый убыток)', 'AP', []),
|
||||
('86', 'Целевое финансирование', 'P', []),
|
||||
# Раздел VIII. Финансовые результаты
|
||||
('90', 'Продажи', 'AP', []),
|
||||
('91', 'Прочие доходы и расходы', 'AP', []),
|
||||
('94', 'Недостачи и потери от порчи ценностей', 'A', []),
|
||||
('96', 'Резервы предстоящих расходов', 'P', []),
|
||||
('97', 'Расходы будущих периодов', 'A', []),
|
||||
('98', 'Доходы будущих периодов', 'P', []),
|
||||
('99', 'Прибыли и убытки', 'AP', []),
|
||||
# Забалансовые счета
|
||||
('001', 'Арендованные основные средства', 'OB', []),
|
||||
('002', 'Товарно-материальные ценности, принятые на ответственное хранение', 'OB', []),
|
||||
('003', 'Материалы, принятые в переработку', 'OB', []),
|
||||
('004', 'Товары, принятые на комиссию', 'OB', []),
|
||||
('005', 'Оборудование, принятое для монтажа', 'OB', []),
|
||||
('006', 'Бланки строгой отчётности', 'OB', []),
|
||||
('007', 'Списанная в убыток задолженность неплатёжеспособных дебиторов', 'OB', []),
|
||||
('008', 'Обеспечения обязательств и платежей полученные', 'OB', []),
|
||||
('009', 'Обеспечения обязательств и платежей выданные', 'OB', []),
|
||||
('010', 'Износ основных средств', 'OB', []),
|
||||
('011', 'Основные средства, сданные в аренду', 'OB', []),
|
||||
]
|
||||
|
||||
# Ключевые субсчета по регламенту (../../docs/research/08-business-process-usn.md).
|
||||
# Формат: (код субсчёта, наименование, признак, [субконто], код родителя).
|
||||
SUBACCOUNTS_94N = [
|
||||
('62.01', 'Расчёты с покупателями и заказчиками', 'A', ['p'], '62'),
|
||||
('62.АВ', 'Авансы полученные', 'P', ['p'], '62'),
|
||||
('68.НДС', 'НДС', 'AP', [], '68'),
|
||||
('68.НДФЛ', 'НДФЛ', 'P', [], '68'),
|
||||
('69.ЕТ', 'Страховые взносы по единому тарифу', 'P', [], '69'),
|
||||
('69.ТР', 'Взносы на травматизм', 'P', [], '69'),
|
||||
('76.АВ', 'НДС по авансам полученным', 'A', ['p'], '76'),
|
||||
('90.01', 'Выручка', 'P', [], '90'),
|
||||
('90.02', 'Себестоимость продаж', 'A', [], '90'),
|
||||
('90.03', 'Налог на добавленную стоимость', 'A', [], '90'),
|
||||
('90.08', 'Управленческие расходы', 'A', [], '90'),
|
||||
('90.09', 'Прибыль/убыток от продаж', 'AP', [], '90'),
|
||||
('91.01', 'Прочие доходы', 'P', [], '91'),
|
||||
('91.02', 'Прочие расходы', 'A', [], '91'),
|
||||
('91.09', 'Сальдо прочих доходов и расходов', 'AP', [], '91'),
|
||||
]
|
||||
|
||||
KIND_MAP = {'A': 'active', 'P': 'passive', 'AP': 'active_passive', 'OB': 'off_balance'}
|
||||
SUBCONTO_FIELD = {'p': 'subconto_partner', 'pr': 'subconto_product',
|
||||
'u': 'subconto_user', 'e': 'subconto_employee'}
|
||||
|
||||
|
||||
class RsbuAccountLoadWizard(models.TransientModel):
|
||||
"""Визард загрузки типового плана счетов 94н для компании."""
|
||||
_name = 'rsbu.account.load.wizard'
|
||||
_description = 'РСБУ — Загрузка типового плана счетов'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания', required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
overwrite = fields.Boolean(
|
||||
string='Не создавать существующие',
|
||||
default=True,
|
||||
help='Пропускать счета, код которых уже есть у компании.',
|
||||
)
|
||||
|
||||
def _subconto_vals(self, subconto):
|
||||
vals = {}
|
||||
for key in subconto:
|
||||
vals[SUBCONTO_FIELD[key]] = True
|
||||
return vals
|
||||
|
||||
def action_load(self):
|
||||
self.ensure_one()
|
||||
Account = self.env['rsbu.account']
|
||||
company = self.company_id
|
||||
existing = {a.code: a for a in Account.with_context(
|
||||
active_test=False).search([('company_id', '=', company.id)])}
|
||||
|
||||
# 1) Синтетические счета.
|
||||
for code, name, kind, subconto in CHART_94N:
|
||||
if code in existing:
|
||||
continue
|
||||
vals = {
|
||||
'code': code, 'name': name,
|
||||
'account_kind': KIND_MAP[kind],
|
||||
'company_id': company.id,
|
||||
}
|
||||
vals.update(self._subconto_vals(subconto))
|
||||
existing[code] = Account.create(vals)
|
||||
|
||||
# 2) Субсчета (с привязкой к родителю).
|
||||
for code, name, kind, subconto, parent_code in SUBACCOUNTS_94N:
|
||||
if code in existing:
|
||||
continue
|
||||
parent = existing.get(parent_code)
|
||||
vals = {
|
||||
'code': code, 'name': name,
|
||||
'account_kind': KIND_MAP[kind],
|
||||
'company_id': company.id,
|
||||
'parent_id': parent.id if parent else False,
|
||||
}
|
||||
vals.update(self._subconto_vals(subconto))
|
||||
existing[code] = Account.create(vals)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('План счетов РСБУ'),
|
||||
'res_model': 'rsbu.account',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('company_id', '=', company.id)],
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_rsbu_account_load_wizard_form" model="ir.ui.view">
|
||||
<field name="name">rsbu.account.load.wizard.form</field>
|
||||
<field name="model">rsbu.account.load.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Загрузка типового плана счетов 94н">
|
||||
<group>
|
||||
<field name="company_id"/>
|
||||
<field name="overwrite"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_load" type="object" string="Загрузить"
|
||||
class="btn-primary"/>
|
||||
<button string="Отмена" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_rsbu_account_load_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Загрузить типовой план счетов 94н</field>
|
||||
<field name="res_model">rsbu.account.load.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user