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_operation_template
from . import rsbu_operation_matrix
from . import account_move
from . import account_payment

View File

@@ -0,0 +1,68 @@
from odoo import api, fields, models
from ..resolvers.registry import get_resolver
class AccountMove(models.Model):
_inherit = 'account.move'
rsbu_template_id = fields.Many2one(
'rsbu.operation.template', string='Шаблон РСБУ',
compute='_compute_rsbu_template_id', store=True, readonly=False,
copy=False)
@api.depends('move_type', 'state', 'line_ids')
def _compute_rsbu_template_id(self):
for move in self:
if move.rsbu_template_id:
# ручное переопределение сохраняем
continue
move.rsbu_template_id = move._rsbu_detect_template()
# ------------------------------------------------------------------
# Классификация
# ------------------------------------------------------------------
def _rsbu_doc_kind(self):
self.ensure_one()
return self.move_type
def _rsbu_has_advance(self):
self.ensure_one()
return False
def _rsbu_partner_kind(self):
self.ensure_one()
if self.move_type in ('out_invoice', 'out_refund'):
return 'customer'
if self.move_type in ('in_invoice', 'in_refund'):
return 'supplier'
return 'any'
def _rsbu_detect_template(self):
self.ensure_one()
# sudo: детект — внутренняя механика; пользователь, проводящий
# документ, может не иметь прав на РСБУ-модели.
Matrix = self.env['rsbu.operation.matrix'].sudo()
rules = Matrix.search([
('company_id', 'in', (False, self.company_id.id)),
], order='priority, id')
for rule in rules:
if rule._match(self):
return rule.template_id
return self.env['rsbu.operation.template'].sudo()
# ------------------------------------------------------------------
# Реализация точек миксина (Этап 3)
# ------------------------------------------------------------------
def _rsbu_is_applicable(self):
self.ensure_one()
return bool(self.rsbu_template_id or self._rsbu_detect_template())
def _rsbu_build_lines(self):
self.ensure_one()
template = (self.rsbu_template_id or self._rsbu_detect_template()).sudo()
if not template:
return []
fn = get_resolver(template.resolver_key)
if not fn:
return []
return fn(self, template)

View File

@@ -0,0 +1,74 @@
from odoo import api, fields, models
from ..resolvers.registry import get_resolver
class AccountPayment(models.Model):
_inherit = 'account.payment'
rsbu_vat_amount = fields.Monetary(
string='В том числе НДС', currency_field='currency_id',
help='Сумма НДС в составе платежа (для авансового НДС). '
'Заполняется вручную или при загрузке банковской выписки.')
rsbu_template_id = fields.Many2one(
'rsbu.operation.template', string='Шаблон РСБУ',
compute='_compute_rsbu_template_id', store=True, readonly=False,
copy=False)
@api.depends('payment_type', 'state')
def _compute_rsbu_template_id(self):
for payment in self:
if payment.rsbu_template_id:
continue
payment.rsbu_template_id = payment._rsbu_detect_template()
# ------------------------------------------------------------------
# Классификация
# ------------------------------------------------------------------
def _rsbu_doc_kind(self):
self.ensure_one()
return 'payment_inbound' if self.payment_type == 'inbound' \
else 'payment_outbound'
def _rsbu_has_advance(self):
self.ensure_one()
# Признак используется матрицей лишь при has_advance != 'any'.
# Для клиентских платежей тип (аванс/постоплата/переплата) определяет
# резолвер customer_payment по сальдо дебиторки на дату платежа, а
# матрица ведёт на один шаблон (has_advance='any'), поэтому этот
# признак на выбор шаблона клиентского платежа не влияет.
return self.payment_type == 'inbound' \
and self.partner_type == 'customer'
def _rsbu_partner_kind(self):
self.ensure_one()
return self.partner_type or 'any'
def _rsbu_detect_template(self):
self.ensure_one()
# sudo: детект — внутренняя механика; пользователь, проводящий
# документ, может не иметь прав на РСБУ-модели.
Matrix = self.env['rsbu.operation.matrix'].sudo()
rules = Matrix.search([
('company_id', 'in', (False, self.company_id.id)),
], order='priority, id')
for rule in rules:
if rule._match(self):
return rule.template_id
return self.env['rsbu.operation.template'].sudo()
# ------------------------------------------------------------------
# Реализация точек миксина (Этап 3)
# ------------------------------------------------------------------
def _rsbu_is_applicable(self):
self.ensure_one()
return bool(self.rsbu_template_id or self._rsbu_detect_template())
def _rsbu_build_lines(self):
self.ensure_one()
template = (self.rsbu_template_id or self._rsbu_detect_template()).sudo()
if not template:
return []
fn = get_resolver(template.resolver_key)
if not fn:
return []
return fn(self, template)

View File

@@ -0,0 +1,58 @@
from odoo import fields, models
class RsbuOperationMatrix(models.Model):
"""Матрица связности: подбор шаблона по признакам документа."""
_name = 'rsbu.operation.matrix'
_description = 'РСБУ — Матрица связности операций'
_order = 'priority, id'
_check_company_auto = True
name = fields.Char(string='Наименование правила', required=True)
doc_kind = fields.Selection(
selection=[
# account.move
('out_invoice', 'Счёт покупателю (реализация)'),
('out_refund', 'Возврат покупателю (кредит-нота)'),
('in_invoice', 'Счёт поставщика'),
('in_refund', 'Возврат поставщику'),
# account.payment
('payment_inbound', 'Входящий платёж'),
('payment_outbound', 'Исходящий платёж'),
],
string='Тип документа', required=True)
has_advance = fields.Selection(
selection=[('any', 'Не важно'), ('yes', 'Есть аванс'),
('no', 'Без аванса')],
string='Признак аванса', default='any', required=True)
partner_kind = fields.Selection(
selection=[('any', 'Не важно'), ('customer', 'Клиент'),
('supplier', 'Поставщик')],
string='Тип контрагента', default='any', required=True,
help='Для платежей: различает клиента и поставщика при '
'одинаковом направлении платежа.')
template_id = fields.Many2one(
'rsbu.operation.template', string='Шаблон РСБУ', required=True,
check_company=True)
priority = fields.Integer(string='Приоритет', default=10)
company_id = fields.Many2one(
'res.company', string='Компания',
default=lambda self: self.env.company)
active = fields.Boolean(default=True)
def _match(self, doc):
"""Подходит ли правило документу."""
self.ensure_one()
doc_kind = doc._rsbu_doc_kind()
if self.doc_kind != doc_kind:
return False
if self.has_advance != 'any':
has_adv = doc._rsbu_has_advance()
if self.has_advance == 'yes' and not has_adv:
return False
if self.has_advance == 'no' and has_adv:
return False
if self.partner_kind != 'any':
if self.partner_kind != doc._rsbu_partner_kind():
return False
return True

View File

@@ -0,0 +1,82 @@
from odoo import fields, models, _
from odoo.exceptions import UserError
from ..resolvers.registry import resolver_selection
# Роли счетов, используемые резолверами.
ROLE_SELECTION = [
('bank', 'Денежный счёт (51/50)'),
('advance_received', 'Авансы полученные (62.АВ)'),
('receivable', 'Расчёты с покупателями (62.01)'),
('revenue', 'Выручка (90.01)'),
('revenue_vat', 'НДС в составе продаж (90.03)'),
('vat_output', 'НДС начисленный (68.НДС)'),
('vat_advance', 'НДС с авансов (76.АВ)'),
('payable', 'Расчёты с поставщиками (60.01)'),
('advance_paid', 'Авансы выданные (60.02)'),
('expense', 'Затраты / расходы (44/26/20)'),
('vat_input', 'Входящий НДС (19)'),
]
class RsbuOperationTemplate(models.Model):
"""Шаблон типовой операции РСБУ (декларативная часть маппинга)."""
_name = 'rsbu.operation.template'
_description = 'РСБУ — Шаблон типовой операции'
_check_company_auto = True
name = fields.Char(string='Наименование', required=True)
code = fields.Char(string='Код', required=True)
resolver_key = fields.Selection(
selection=lambda self: resolver_selection(),
string='Резолвер', required=True,
help='Python-резолвер (в коде модуля), формирующий проводки.')
company_id = fields.Many2one(
'res.company', string='Компания',
default=lambda self: self.env.company)
role_ids = fields.One2many(
'rsbu.operation.template.role', 'template_id', string='Роли счетов')
active = fields.Boolean(default=True)
note = fields.Text(string='Описание логики')
_code_uniq = models.Constraint(
'unique(code, company_id)',
'Код шаблона должен быть уникальным в пределах компании.')
def _account(self, role):
"""Счёт РСБУ по роли шаблона (обязательная роль)."""
self.ensure_one()
line = self.role_ids.filtered(lambda r: r.role == role)[:1]
if not line:
raise UserError(_(
'В шаблоне «%(tmpl)s» не задана роль счёта «%(role)s».',
tmpl=self.display_name, role=role))
return line.account_id
def _account_or_none(self, role):
"""Счёт РСБУ по роли шаблона или None, если роль не задана."""
self.ensure_one()
line = self.role_ids.filtered(lambda r: r.role == role)[:1]
return line.account_id if line else None
class RsbuOperationTemplateRole(models.Model):
"""Роль счёта в шаблоне операции."""
_name = 'rsbu.operation.template.role'
_description = 'РСБУ — Роль счёта в шаблоне'
_check_company_auto = True
template_id = fields.Many2one(
'rsbu.operation.template', string='Шаблон', required=True,
ondelete='cascade')
role = fields.Selection(
selection=ROLE_SELECTION, string='Роль', required=True)
account_id = fields.Many2one(
'rsbu.account', string='Счёт РСБУ', required=True, check_company=True)
company_id = fields.Many2one(
related='template_id.company_id', store=True)
subconto_source = fields.Selection(
selection=[
('partner_from_doc', 'Контрагент из документа'),
('none', 'Без субконто по умолчанию'),
],
string='Источник субконто', default='partner_from_doc')