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

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

View File

@@ -0,0 +1,4 @@
from . import rsbu_move
from . import rsbu_move_line
from . import rsbu_account
from . import rsbu_trial_balance

View 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)

View 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)

View 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])

View 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},
}