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

50
rsbu_mapping/DEVELOPER.md Normal file
View File

@@ -0,0 +1,50 @@
# РСБУ — Шаблоны типовых операций (маппинг)
Гибридный маппинг: декларативные шаблоны операций + матрица связности
(данные в БД) и Python-резолверы в коде модуля (`resolvers/`).
## Резолверы коммерческого цикла
| Резолвер | Операция | Проводки |
| --- | --- | --- |
| `advance_received` | получение аванса от покупателя | Дт 51 — Кт 62.АВ; Дт 76.АВ — Кт 68 |
| `advance_refund` | возврат аванса покупателю | Дт 62.АВ — Кт 51; Дт 68 — Кт 76.АВ |
| `shipment_upd` | отгрузка / УПД (+зачёт аванса) | Дт 62.01 — Кт 90.01; Дт 90.03 — Кт 68; Дт 62.АВ — Кт 62.01; Дт 68 — Кт 76.АВ |
| `sale_credit_note` | возврат от клиента | сторно реализации + НДС |
| `payment_after_shipment` | постоплата | Дт 51 — Кт 62.01 |
| `purchase_bill` | счёт поставщика | Дт 44/expense — Кт 60 |
| `purchase_credit_note` | возврат поставщику | Дт 60 — Кт 44/expense |
| `supplier_payment` | оплата поставщику | Дт 60 — Кт 51 |
| `supplier_refund` | возврат от поставщика | Дт 51 — Кт 60 |
Реализация выручки — по **брутто-схеме** (90-1 с НДС, НДС отдельно на 90-3),
как в типовой 1С и по Инструкции 94н (см. `docs/research/09`).
Зачёт аванса при отгрузке определяется по **кредитовому остатку 62.АВ у
контрагента** в РСБУ-журнале (`advance.get_balance(subconto={'partner': id})`),
сумма зачёта = min(остаток аванса, сумма реализации); авансовый НДС
списывается пропорционально зачтённой части (ограничен остатком 76.АВ).
## Осознанные ограничения (для УСН)
- **Входящий НДС не выделяется (Р3).** В `purchase_bill` полная сумма счёта
поставщика (с НДС) относится в затраты (`Дт 44 — Кт 60`). На спецрежиме
УСН (ставки 5%/7% без права на вычет) это **корректно**: входящий НДС к
вычету не принимается и включается в стоимость. Выделение НДС на счёт 19
и вычет `Дт 68 — Кт 19` относятся к общему режиму (ОСНО) и вне области
проекта.
- **Себестоимость не списывается (Р2).** Проводка `Дт 90.02 — Кт 41/44`
не формируется: для целей УСН учёт себестоимости не требуется (на
«Доходы» не влияет на налог; на «Доходы−Расходы» расход признаётся
кассово). При необходимости ведётся отдельной ручной операцией.
- **Субконто на денежных строках (Р4).** Строки по денежным счетам (51/50)
формируются без субконто-партнёра (в плане 94н у этих счетов субконто не
обязательно). При необходимости отчётности по движению денежных средств
партнёра на денежной строке можно добавить — это опция, не ограничение.
> Нормативные параметры сверяются со специалистом (см.
> `docs/research/00-index.md`). Резолверы опираются на регламент
> `docs/research/08-business-process-usn.md` и справочник корреспонденции
> `docs/research/09-account-correspondence.md`.

65
rsbu_mapping/README.rst Normal file
View File

@@ -0,0 +1,65 @@
===============================
РСБУ — Шаблоны типовых операций
===============================
.. |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|
Задаёт правила, по которым документы Odoo превращаются в проводки РСБУ:
какие счета дебетуются и кредитуются для каждой хозяйственной операции.
Модуль наполняет коннектор документов логикой формирования проводок.
**Содержание**
.. contents::
:local:
Возможности
===========
* Шаблоны операций с указанием ролей счетов (дебет / кредит)
* Матрица связности — подбор нужного шаблона по типу документа и
параметрам операции
* Поддержка полного коммерческого цикла:
* получение аванса (в т. ч. НДС с аванса)
* возврат аванса
* реализация / отгрузка (выручка, НДС, зачёт ранее полученного аванса)
* постоплата
Настройка
=========
#. Перейдите в *РСБУ > Настройка > Шаблоны операций*.
#. Создайте шаблон и укажите роли счетов (какие дебетуются, какие
кредитуются).
#. Настройте правила матрицы связности, чтобы система подбирала шаблон
автоматически по типу документа.
Готовый набор шаблонов для типовых сценариев можно установить из модуля
демо-шаблонов операций.
Использование
=============
После настройки шаблонов проводки РСБУ формируются автоматически при
проведении документов — отдельных действий не требуется.
Авторы
======
* MK.Lab, RuOdoo
Модуль сопровождается MK.Lab, RuOdoo.
.. image:: https://ruodoo.ru/logo.png
:alt: MK.Lab, RuOdoo
:target: https://ruodoo.ru
Модуль является частью РСБУ-слоя проекта РуОду.

2
rsbu_mapping/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import resolvers
from . import models

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
{
'name': 'РСБУ — Шаблоны типовых операций (маппинг)',
'version': '19.0.2026.09.17',
'summary': 'Гибридный маппинг: шаблоны + матрица связности + резолверы',
'description': """
РСБУ — Шаблоны типовых операций
===============================
Задаёт правила, по которым документы Odoo превращаются в проводки РСБУ:
какие счета дебетуются и кредитуются для каждой хозяйственной операции.
Возможности:
------------
* Шаблоны операций с указанием ролей счетов (дебет / кредит).
* Матрица связности, которая подбирает нужный шаблон по типу документа
и параметрам операции.
* Поддержка полного коммерческого цикла: получение аванса (в т. ч. НДС
с аванса), возврат аванса, реализация/отгрузка (выручка, НДС, зачёт
ранее полученного аванса), постоплата.
Как пользоваться:
-----------------
Настройте шаблоны операций и правила их подбора (или воспользуйтесь
готовым набором из модуля демо-шаблонов). Далее при проведении
документов проводки РСБУ формируются автоматически по этим правилам.
""",
'author': 'MK.Lab, RuOdoo',
'website': 'https://ruodoo.ru',
'category': 'Accounting/Localizations',
'depends': ['rsbu_connector', 'account'],
'data': [
'security/ir.model.access.csv',
'views/rsbu_operation_views.xml',
'views/account_move_views.xml',
'views/account_payment_views.xml',
'views/rsbu_menus.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
'maintainers': ['mklab', 'ruodoo'],
}

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

View File

@@ -0,0 +1,2 @@
from . import registry
from . import commercial

View File

@@ -0,0 +1,422 @@
"""Резолверы коммерческого цикла (по регламенту docs/research/08).
Каждый резолвер получает документ-родитель (account.move / account.payment)
и шаблон rsbu.operation.template (роли счетов). Возвращает список значений
строк rsbu.move.line, сбалансированный по Дт=Кт. Ставка НДС берётся из
инвойса (налоговые строки) либо из поля rsbu_vat_amount платежа.
"""
from .registry import resolver
def _line(account, debit=0.0, credit=0.0, partner=None, product=None,
user=None, employee=None, name=None, origin=None,
origin_self=False, contract=None):
vals = {
'account_id': account.id,
'debit': debit,
'credit': credit,
}
if partner:
vals['partner_id'] = partner.id
if product:
vals['product_id'] = product.id
if user:
vals['user_id'] = user.id
if employee:
vals['employee_id'] = employee.id
if name:
vals['name'] = name
# Субконто «Документ-основание» (аванс): либо явная проводка-основание,
# либо маркер origin_self (ссылка на саму создаваемую проводку —
# проставляется миксином после create).
if origin is not None:
vals['origin_move_id'] = origin.id
if origin_self:
vals['origin_self'] = True
# Субконто «Договор».
if contract is not None:
vals['contract_id'] = contract.id
return vals
def _doc_contract(doc):
"""Договор документа-источника (partner.contract.customer) или None."""
return getattr(doc, 'mt_contract_id', False) or None
@resolver('advance_received')
def advance_received(doc, template):
"""Получение аванса от покупателя (входящий платёж).
Дт bank — Кт advance_received (сумма платежа);
Дт vat_advance — Кт vat_output (авансовый НДС из rsbu_vat_amount).
"""
partner = doc.partner_id
amount = abs(doc.amount)
vat = abs(getattr(doc, 'rsbu_vat_amount', 0.0) or 0.0)
contract = _doc_contract(doc)
bank = template._account('bank')
advance = template._account('advance_received')
lines = [
_line(bank, debit=amount, name='Поступление аванса'),
# origin_self: аванс ссылается на свою проводку — для дроблёного
# зачёта по основанию в shipment_upd (get_origin_balances).
_line(advance, credit=amount, partner=partner, contract=contract,
origin_self=True, name='Аванс полученный'),
]
if vat:
vat_advance = template._account('vat_advance')
vat_output = template._account('vat_output')
lines += [
_line(vat_advance, debit=vat, partner=partner,
name='НДС с аванса'),
_line(vat_output, credit=vat, name='НДС с аванса начислен'),
]
return lines
@resolver('advance_refund')
def advance_refund(doc, template):
"""Возврат аванса покупателю.
Дт advance_received — Кт bank; вычет авансового НДС:
Дт vat_output — Кт vat_advance.
"""
partner = doc.partner_id
amount = abs(doc.amount)
vat = abs(getattr(doc, 'rsbu_vat_amount', 0.0) or 0.0)
bank = template._account('bank')
advance = template._account('advance_received')
lines = [
_line(advance, debit=amount, partner=partner, name='Возврат аванса'),
_line(bank, credit=amount, name='Возврат аванса с р/с'),
]
if vat:
vat_advance = template._account('vat_advance')
vat_output = template._account('vat_output')
lines += [
_line(vat_output, debit=vat, name='Вычет НДС с аванса'),
_line(vat_advance, credit=vat, partner=partner,
name='Сторно НДС с аванса'),
]
return lines
@resolver('shipment_upd')
def shipment_upd(doc, template):
"""Отгрузка / выполнение работ / УПД (исходящий инвойс).
Базовая реализация (брутто):
Дт 62.01 — Кт 90.01 на полную сумму (выручка с НДС);
Дт 90.03 — Кт 68.НДС на сумму НДС (НДС в составе продаж).
Дополнительно, если по контрагенту есть незакрытый аванс (кредитовый
остаток 62.АВ), формируются проводки зачёта аванса и вычета авансового
НДС (по регламенту 08, ч.1 п.6):
Дт 62.АВ — Кт 62.01 на сумму зачёта;
Дт 68.НДС — Кт 76.АВ на авансовый НДС, приходящийся на зачёт.
"""
partner = doc.partner_id
company_id = doc.company_id.id
tax = abs(doc.amount_tax)
total = abs(doc.amount_total)
contract = _doc_contract(doc)
receivable = template._account('receivable')
revenue = template._account('revenue')
lines = [
_line(receivable, debit=total, partner=partner, contract=contract,
name='Задолженность покупателя'),
_line(revenue, credit=total, name='Выручка'),
]
if tax:
revenue_vat = template._account('revenue_vat')
vat_output = template._account('vat_output')
lines += [
_line(revenue_vat, debit=tax, name='НДС с реализации'),
_line(vat_output, credit=tax, name='НДС с реализации начислен'),
]
# --- Зачёт аванса, ДРОБЛЁННЫЙ по каждому авансу-основанию (FIFO) ---
# Как в 1С: Дт 62.02 — Кт 62.01 отдельной строкой на каждый аванс,
# с субконто «Документ-основание» = проводка аванса, в разрезе договора.
# Вычет авансового НДС здесь НЕ делается — он выполняется регламентом
# «Формирование записей книги покупок» в конце периода (68.02 — 76.АВ).
advance = template._account_or_none('advance_received')
if advance and partner:
subconto = {'partner': partner.id}
if contract:
subconto['contract'] = contract.id
origins = advance.get_origin_balances(
date_to=doc.date, company_id=company_id, subconto=subconto)
remaining = total
for data in origins:
if remaining <= 0:
break
# 62.02 пассивный: кредитовое сальдо = незакрытый аванс.
available = data['balance'] if data['side'] == 'credit' else 0.0
if available <= 0:
continue
take = min(available, remaining)
take = round(take, 2)
if take <= 0:
continue
origin_move = data['origin']
lines += [
_line(advance, debit=take, partner=partner, contract=contract,
origin=origin_move, name='Зачёт аванса покупателя'),
_line(receivable, credit=take, partner=partner,
contract=contract,
name='Зачёт аванса в счёт задолженности'),
]
remaining -= take
return lines
@resolver('payment_after_shipment')
def payment_after_shipment(doc, template):
"""Постоплата: поступление денег после отгрузки.
Дт bank — Кт receivable.
"""
partner = doc.partner_id
amount = abs(doc.amount)
bank = template._account('bank')
receivable = template._account('receivable')
return [
_line(bank, debit=amount, name='Оплата от покупателя'),
_line(receivable, credit=amount, partner=partner,
name='Погашение задолженности'),
]
# Расчётная ставка НДС с аванса по режиму НДС компании (доля налога в сумме).
_ADVANCE_VAT_FRACTION = {
'vat_5': 5.0 / 105.0,
'vat_7': 7.0 / 107.0,
}
@resolver('customer_payment')
def customer_payment(doc, template):
"""Поступление денег от покупателя: аванс, постоплата или смешанно.
Только движение денег (как в 1С; авансовый НДС начисляется отдельным
документом «Счёт-фактура на аванс», а не здесь). Тип определяется
сальдо дебиторки 62.01 по контрагенту на дату платежа:
* есть незакрытая дебиторка → Дт 51 — Кт 62.01 на min(долг, платёж)
(постоплата, гасит долг по каждому основанию);
* остаток сверх долга → аванс: Дт 51 — Кт 62.02 с субконто
«Документ-основание» = сама проводка платежа (origin_self), чтобы
затем вести зачёт по каждому авансу отдельно.
"""
partner = doc.partner_id
company_id = doc.company_id.id
amount = abs(doc.amount)
bank = template._account('bank')
contract = _doc_contract(doc)
lines = [_line(bank, debit=amount, name='Оплата от покупателя')]
# Долг покупателя на дату платежа — в разрезе (контрагент, договор),
# как в 1С (зачёт и погашение ведутся по договору-основанию).
receivable = template._account_or_none('receivable')
debt = 0.0
if receivable and partner:
subconto = {'partner': partner.id}
if contract:
subconto['contract'] = contract.id
bal = receivable.get_balance(
date_to=doc.date, company_id=company_id, subconto=subconto)
# 62.01 активный: дебетовое сальдо = долг покупателя по договору.
debt = bal['balance'] if bal['side'] == 'debit' else 0.0
pay_debt = min(debt, amount)
advance = round(amount - pay_debt, 2)
# Погашение задолженности (постоплата) — по тому же договору.
if pay_debt > 0 and receivable:
lines.append(_line(receivable, credit=pay_debt, partner=partner,
contract=contract,
name='Погашение задолженности покупателя'))
# Остаток — аванс (субконто: договор + основание = сама проводка).
if advance > 0:
advance_acc = template._account('advance_received')
lines.append(_line(advance_acc, credit=advance, partner=partner,
contract=contract, origin_self=True,
name='Аванс полученный'))
return lines
@resolver('sale_credit_note')
def sale_credit_note(doc, template):
"""Возврат от клиента / кредит-нота (out_refund).
Сторно реализации: Дт revenue — Кт receivable (сумма без НДС);
сторно НДС: Дт vat_output — Кт revenue_vat.
"""
partner = doc.partner_id
tax = abs(doc.amount_tax)
total = abs(doc.amount_total)
receivable = template._account('receivable')
revenue = template._account('revenue')
# Зеркально shipment_upd: Дт 90.01 — Кт 62.01 на полную сумму;
# Дт 68.НДС — Кт 90.03 на сумму НДС (сторно начисления).
lines = [
_line(revenue, debit=total, name='Сторно выручки'),
_line(receivable, credit=total, partner=partner,
name='Уменьшение задолженности покупателя'),
]
if tax:
revenue_vat = template._account('revenue_vat')
vat_output = template._account('vat_output')
lines += [
_line(vat_output, debit=tax, name='Сторно НДС с реализации'),
_line(revenue_vat, credit=tax, name='Сторно НДС в продажах'),
]
return lines
@resolver('purchase_bill')
def purchase_bill(doc, template):
"""Счёт/поступление поставщика (in_invoice).
Дт expense(26) — Кт payable(60.01) на полную сумму (на УСН входящий
НДС к вычету не принимается и включается в стоимость).
Плюс зачёт аванса выданного, ДРОБЛЁННЫЙ по каждому авансу-основанию
(FIFO) в разрезе договора, как в 1С: Дт 60.01 — Кт 60.02 отдельной
строкой на каждый аванс (субконто «Документ-основание» = проводка
аванса).
"""
partner = doc.partner_id
company_id = doc.company_id.id
total = abs(doc.amount_total)
contract = _doc_contract(doc)
expense = template._account('expense')
payable = template._account('payable')
lines = [
_line(expense, debit=total, name='Затраты по счёту поставщика'),
_line(payable, credit=total, partner=partner, contract=contract,
name='Задолженность перед поставщиком'),
]
# --- Зачёт аванса выданного (60.02), дроблёный по основаниям ---
advance_paid = template._account_or_none('advance_paid')
if advance_paid and partner:
subconto = {'partner': partner.id}
if contract:
subconto['contract'] = contract.id
origins = advance_paid.get_origin_balances(
date_to=doc.date, company_id=company_id, subconto=subconto)
remaining = total
for data in origins:
if remaining <= 0:
break
# 60.02 активный: дебетовое сальдо = незачтённый аванс выданный.
available = data['balance'] if data['side'] == 'debit' else 0.0
if available <= 0:
continue
take = round(min(available, remaining), 2)
if take <= 0:
continue
origin_move = data['origin']
lines += [
_line(payable, debit=take, partner=partner, contract=contract,
name='Зачёт аванса выданного'),
_line(advance_paid, credit=take, partner=partner,
contract=contract, origin=origin_move,
name='Зачёт аванса в счёт поставки'),
]
remaining -= take
return lines
@resolver('purchase_credit_note')
def purchase_credit_note(doc, template):
"""Возврат поставщику / кредит-нота поставщика (in_refund).
Дт payable — Кт expense (сторно затрат и задолженности).
"""
partner = doc.partner_id
total = abs(doc.amount_total)
expense = template._account('expense')
payable = template._account('payable')
return [
_line(payable, debit=total, partner=partner,
name='Уменьшение задолженности перед поставщиком'),
_line(expense, credit=total, name='Сторно затрат'),
]
@resolver('supplier_payment')
def supplier_payment(doc, template):
"""Оплата поставщику (исходящий платёж): постоплата и/или аванс выданный.
Зеркально customer_payment. Тип определяется сальдо кредиторки 60.01 по
(поставщик, договор) на дату платежа:
* есть незакрытый долг перед поставщиком → Дт 60.01 — Кт 51 на
min(долг, платёж) (погашение задолженности);
* остаток сверх долга → аванс выданный: Дт 60.02 — Кт 51 с субконто
«Документ-основание» = сама проводка платежа (origin_self), договор.
"""
partner = doc.partner_id
company_id = doc.company_id.id
amount = abs(doc.amount)
bank = template._account('bank')
payable = template._account('payable')
contract = _doc_contract(doc)
# Долг перед поставщиком на дату — в разрезе (поставщик, договор).
debt = 0.0
if partner:
subconto = {'partner': partner.id}
if contract:
subconto['contract'] = contract.id
bal = payable.get_balance(
date_to=doc.date, company_id=company_id, subconto=subconto)
# 60.01 пассивный: кредитовое сальдо = наш долг поставщику.
debt = bal['balance'] if bal['side'] == 'credit' else 0.0
pay_debt = min(debt, amount)
advance = round(amount - pay_debt, 2)
lines = []
# Погашение задолженности (постоплата).
if pay_debt > 0:
lines += [
_line(payable, debit=pay_debt, partner=partner, contract=contract,
name='Оплата поставщику'),
_line(bank, credit=pay_debt, name='Списание с р/с'),
]
# Остаток — аванс выданный (60.02, origin_self, договор).
if advance > 0:
advance_paid = template._account('advance_paid')
lines += [
_line(advance_paid, debit=advance, partner=partner,
contract=contract, origin_self=True,
name='Аванс выданный поставщику'),
_line(bank, credit=advance, name='Списание с р/с (аванс)'),
]
return lines
@resolver('supplier_refund')
def supplier_refund(doc, template):
"""Возврат денег от поставщика (входящий платёж).
Дт bank — Кт payable.
"""
partner = doc.partner_id
amount = abs(doc.amount)
payable = template._account('payable')
bank = template._account('bank')
return [
_line(bank, debit=amount, name='Возврат от поставщика'),
_line(payable, credit=amount, partner=partner,
name='Уменьшение расчётов с поставщиком'),
]

View File

@@ -0,0 +1,26 @@
"""Реестр Python-резолверов маппинга РСБУ.
Резолвер — функция fn(doc, template) -> list[dict], возвращающая
сбалансированный набор значений строк rsbu.move.line (Дт/Кт + субконто).
Резолверы регистрируются декоратором @resolver(key) при импорте модуля
и не редактируются пользователем.
"""
RESOLVER_REGISTRY = {}
def resolver(key):
"""Декоратор регистрации резолвера по ключу."""
def deco(fn):
RESOLVER_REGISTRY[key] = fn
return fn
return deco
def get_resolver(key):
return RESOLVER_REGISTRY.get(key)
def resolver_selection():
"""Список ключей резолверов для Selection-поля шаблона."""
return [(key, key) for key in sorted(RESOLVER_REGISTRY)]

View File

@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_rsbu_op_template_user,rsbu.operation.template user,model_rsbu_operation_template,rsbu_chart_of_accounts.group_rsbu_account_user,1,0,0,0
access_rsbu_op_template_mgr,rsbu.operation.template manager,model_rsbu_operation_template,rsbu_journal.group_rsbu_journal_manager,1,1,1,1
access_rsbu_op_role_user,rsbu.operation.template.role user,model_rsbu_operation_template_role,rsbu_chart_of_accounts.group_rsbu_account_user,1,0,0,0
access_rsbu_op_role_mgr,rsbu.operation.template.role manager,model_rsbu_operation_template_role,rsbu_journal.group_rsbu_journal_manager,1,1,1,1
access_rsbu_op_matrix_user,rsbu.operation.matrix user,model_rsbu_operation_matrix,rsbu_chart_of_accounts.group_rsbu_account_user,1,0,0,0
access_rsbu_op_matrix_mgr,rsbu.operation.matrix manager,model_rsbu_operation_matrix,rsbu_journal.group_rsbu_journal_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_rsbu_op_template_user rsbu.operation.template user model_rsbu_operation_template rsbu_chart_of_accounts.group_rsbu_account_user 1 0 0 0
3 access_rsbu_op_template_mgr rsbu.operation.template manager model_rsbu_operation_template rsbu_journal.group_rsbu_journal_manager 1 1 1 1
4 access_rsbu_op_role_user rsbu.operation.template.role user model_rsbu_operation_template_role rsbu_chart_of_accounts.group_rsbu_account_user 1 0 0 0
5 access_rsbu_op_role_mgr rsbu.operation.template.role manager model_rsbu_operation_template_role rsbu_journal.group_rsbu_journal_manager 1 1 1 1
6 access_rsbu_op_matrix_user rsbu.operation.matrix user model_rsbu_operation_matrix rsbu_chart_of_accounts.group_rsbu_account_user 1 0 0 0
7 access_rsbu_op_matrix_mgr rsbu.operation.matrix manager model_rsbu_operation_matrix rsbu_journal.group_rsbu_journal_manager 1 1 1 1

View File

@@ -0,0 +1 @@
from . import test_mapping

View File

@@ -0,0 +1,305 @@
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestRsbuMapping(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
cls.partner = cls.env['res.partner'].create({'name': 'Map Клиент'})
cls.journal_bank = cls.env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', cls.company.id)],
limit=1)
cls.Template = cls.env['rsbu.operation.template']
cls.Matrix = cls.env['rsbu.operation.matrix']
# Роли счетов из типового плана.
cls.acc = lambda self, code: self.env['rsbu.account'].search([
('code', '=', code),
('company_id', '=', self.env.company.id)], limit=1)
def _acc(self, code):
return self.env['rsbu.account'].search([
('code', '=', code),
('company_id', '=', self.env.company.id)], limit=1)
def _advance_template(self):
return self.Template.create({
'code': 'ADV_IN',
'name': 'Получение аванса',
'resolver_key': 'advance_received',
'role_ids': [
(0, 0, {'role': 'bank', 'account_id': self._acc('51').id}),
(0, 0, {'role': 'advance_received',
'account_id': self._acc('62.АВ').id}),
(0, 0, {'role': 'vat_advance',
'account_id': self._acc('76.АВ').id}),
(0, 0, {'role': 'vat_output',
'account_id': self._acc('68.НДС').id}),
],
})
def _matrix_advance(self, template):
return self.Matrix.create({
'name': 'Входящий платёж → аванс',
'doc_kind': 'payment_inbound',
'has_advance': 'any',
'template_id': template.id,
'priority': 10,
})
def _payment(self, amount=1050.0, vat=50.0):
return self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner.id,
'amount': amount,
'rsbu_vat_amount': vat,
'journal_id': self.journal_bank.id,
})
def test_detect_template_by_matrix(self):
tmpl = self._advance_template()
self._matrix_advance(tmpl)
pay = self._payment()
self.assertEqual(pay._rsbu_detect_template(), tmpl)
self.assertTrue(pay._rsbu_is_applicable())
def test_no_matrix_not_applicable(self):
# Без матрицы документ не в РСБУ-периметре.
pay = self._payment()
self.assertFalse(pay._rsbu_detect_template())
self.assertFalse(pay._rsbu_is_applicable())
def test_advance_resolver_lines_balanced(self):
tmpl = self._advance_template()
self._matrix_advance(tmpl)
pay = self._payment(amount=1050.0, vat=50.0)
lines = pay._rsbu_build_lines()
total_debit = sum(l.get('debit', 0.0) for l in lines)
total_credit = sum(l.get('credit', 0.0) for l in lines)
self.assertEqual(total_debit, total_credit)
# Дт 51 = 1050, авансовый НДС 50
self.assertEqual(total_debit, 1050.0 + 50.0)
def test_full_flow_generate_rsbu_move(self):
tmpl = self._advance_template()
self._matrix_advance(tmpl)
pay = self._payment(amount=1050.0, vat=50.0)
pay.action_post()
move = pay.rsbu_move_id
self.assertTrue(move)
self.assertEqual(move.state, 'posted')
# Проверим сальдо: 51 дебет 1050
acc51 = self._acc('51')
bal = acc51.get_balance(company_id=self.company.id)
self.assertEqual(bal['debit'], 1050.0)
# 68.НДС кредит 50 (начислен авансовый НДС)
acc68 = self._acc('68.НДС')
bal68 = acc68.get_balance(company_id=self.company.id)
self.assertEqual(bal68['credit'], 50.0)
def _sale_template(self, with_advance_roles=False):
roles = [
(0, 0, {'role': 'receivable', 'account_id': self._acc('62.01').id}),
(0, 0, {'role': 'revenue', 'account_id': self._acc('90.01').id}),
(0, 0, {'role': 'revenue_vat', 'account_id': self._acc('90.03').id}),
(0, 0, {'role': 'vat_output', 'account_id': self._acc('68.НДС').id}),
]
if with_advance_roles:
roles += [
(0, 0, {'role': 'advance_received',
'account_id': self._acc('62.АВ').id}),
(0, 0, {'role': 'vat_advance',
'account_id': self._acc('76.АВ').id}),
]
return self.Template.create({
'code': 'SALE_T',
'name': 'Продажа',
'resolver_key': 'shipment_upd',
'role_ids': roles,
})
def _sale_invoice_with_vat(self):
"""Инвойс с НДС: untaxed 147.00, tax 22.05, total 169.05."""
tax = self.env['account.tax'].create({
'name': 'НДС 15% demo',
'amount': 15.0,
'amount_type': 'percent',
'type_tax_use': 'sale',
})
product = self.env['product.product'].create({'name': 'Demo P'})
return self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'invoice_line_ids': [(0, 0, {
'product_id': product.id,
'name': 'Demo',
'quantity': 1,
'price_unit': 147.0,
'tax_ids': [(6, 0, tax.ids)],
})],
})
def test_sale_with_vat_balanced(self):
# Регрессия: реализация с НДС должна давать сбалансированную проводку.
tmpl = self._sale_template()
inv = self._sale_invoice_with_vat()
inv.rsbu_template_id = tmpl.id
lines = inv._rsbu_build_lines()
total_debit = sum(l.get('debit', 0.0) for l in lines)
total_credit = sum(l.get('credit', 0.0) for l in lines)
self.assertAlmostEqual(total_debit, total_credit, places=2)
# Дт 62.01 = total 169.05, Дт 90.03 = НДС -> дебет = total + tax.
self.assertAlmostEqual(total_debit, 169.05 + 22.05, places=2)
def test_advance_offset_on_shipment(self):
"""Р1: при отгрузке после аванса зачёт 62.АВ и вычет авансового НДС."""
# 1) Аванс клиента 105 (НДС 5) через резолвер advance_received.
adv_tmpl = self._advance_template()
self._matrix_advance(adv_tmpl)
pay = self._payment(amount=105.0, vat=5.0)
pay.action_post()
self.assertTrue(pay.rsbu_move_id)
# Остатки после аванса: 62.АВ кредит 105, 76.АВ дебет 5.
acc_av = self._acc('62.АВ')
acc_vatav = self._acc('76.АВ')
bal_av = acc_av.get_balance(
company_id=self.company.id,
subconto={'partner': self.partner.id})
self.assertEqual(bal_av['balance'], 105.0)
self.assertEqual(bal_av['side'], 'credit')
# 2) Отгрузка тому же клиенту, шаблон с ролями аванса.
sale_tmpl = self._sale_template(with_advance_roles=True)
inv = self._sale_invoice_with_vat() # total 169.05
inv.rsbu_template_id = sale_tmpl.id
lines = inv._rsbu_build_lines()
# Проводка должна содержать зачёт аванса и вычет авансового НДС.
d = sum(l.get('debit', 0.0) for l in lines)
c = sum(l.get('credit', 0.0) for l in lines)
self.assertAlmostEqual(d, c, places=2)
inv.action_post()
# 3) После отгрузки зачёт аванса: 62.АВ по партнёру закрыт (=0).
# Вычет авансового НДС (76.АВ) при отгрузке НЕ делается — он вынесен
# в регламент конца периода (rsbu.period.close), поэтому 76.АВ здесь
# остаётся ненулевым (закрывается регламентом «книга покупок»).
bal_av2 = acc_av.get_balance(
company_id=self.company.id,
subconto={'partner': self.partner.id})
self.assertAlmostEqual(bal_av2['balance'], 0.0, places=2)
# ------------------------------------------------------------------
# customer_payment: аванс / постоплата / переплата
# ------------------------------------------------------------------
def _customer_payment_template(self):
tmpl = self.Template.create({
'code': 'CUST_PAY',
'name': 'Оплата от клиента',
'resolver_key': 'customer_payment',
'company_id': self.company.id,
'role_ids': [
(0, 0, {'role': 'bank', 'account_id': self._acc('51').id}),
(0, 0, {'role': 'receivable',
'account_id': self._acc('62.01').id}),
(0, 0, {'role': 'advance_received',
'account_id': self._acc('62.АВ').id}),
(0, 0, {'role': 'vat_advance',
'account_id': self._acc('76.АВ').id}),
(0, 0, {'role': 'vat_output',
'account_id': self._acc('68.НДС').id}),
],
})
self.Matrix.create({
'name': 'Оплата клиента',
'doc_kind': 'payment_inbound',
'partner_kind': 'customer',
'has_advance': 'any',
'template_id': tmpl.id,
'company_id': self.company.id,
})
return tmpl
def _debt(self, partner, amount, date):
"""Создать дебиторку покупателя (реализация) для теста постоплаты."""
move = self.env['rsbu.move'].create({
'date': date, 'company_id': self.company.id,
'line_ids': [
(0, 0, {'account_id': self._acc('62.01').id, 'debit': amount,
'partner_id': partner.id}),
(0, 0, {'account_id': self._acc('90.01').id, 'credit': amount}),
],
})
move.action_post()
return move
def _inbound(self, partner, amount, date):
pay = self.env['account.payment'].create({
'payment_type': 'inbound', 'partner_type': 'customer',
'partner_id': partner.id, 'amount': amount,
'journal_id': self.journal_bank.id, 'company_id': self.company.id,
'date': date,
})
pay.action_post()
return pay
def test_customer_payment_advance(self):
"""Платёж без долга → аванс (Кт 62.АВ), только деньги.
Авансовый НДС в платеже НЕ начисляется: он живёт в отдельном
авансовом счёте-фактуре (rsbu.invoice, invoice_type='advance'),
рождаемом из платежа и не связанном с документом реализации
(realization_id пуст). Модель-источник авансового НДС — rsbu.invoice,
без изменений. Платёж двигает только деньги.
"""
self.company.rsbu_vat_mode = 'vat_5'
self._customer_payment_template()
p = self.env['res.partner'].create({'name': 'Аванс-клиент'})
pay = self._inbound(p, 105000.0, '2026-03-01')
acc_av = self._acc('62.АВ')
bal = acc_av.get_balance(company_id=self.company.id,
subconto={'partner': p.id})
self.assertAlmostEqual(bal['balance'], 105000.0, places=2)
# Авансовый НДС в платеже не начисляется (68.НДС не затронут платежом).
# дебиторка 62.01 не затронута
bal01 = self._acc('62.01').get_balance(
company_id=self.company.id, subconto={'partner': p.id})
self.assertAlmostEqual(bal01['balance'], 0.0, places=2)
def test_customer_payment_postpaid(self):
"""Платёж при наличии долга → погашение 62.01, без аванса и НДС."""
self.company.rsbu_vat_mode = 'vat_5'
self._customer_payment_template()
p = self.env['res.partner'].create({'name': 'Постоплата-клиент'})
self._debt(p, 50000.0, '2026-03-05')
pay = self._inbound(p, 50000.0, '2026-03-10')
# 62.01 закрыт (0), 62.АВ не создавался, авансовый НДС не начислен
bal01 = self._acc('62.01').get_balance(
company_id=self.company.id, subconto={'partner': p.id})
self.assertAlmostEqual(bal01['balance'], 0.0, places=2)
bal_av = self._acc('62.АВ').get_balance(
company_id=self.company.id, subconto={'partner': p.id})
self.assertAlmostEqual(bal_av['balance'], 0.0, places=2)
def test_customer_payment_overpaid(self):
"""Переплата → долг гасится (62.01), остаток в аванс (62.АВ).
Авансовый НДС в платеже не начисляется (вынесен в СФ на аванс).
"""
self.company.rsbu_vat_mode = 'vat_5'
self._customer_payment_template()
p = self.env['res.partner'].create({'name': 'Переплата-клиент'})
self._debt(p, 30000.0, '2026-03-06')
pay = self._inbound(p, 100000.0, '2026-03-11')
# долг 62.01 закрыт
bal01 = self._acc('62.01').get_balance(
company_id=self.company.id, subconto={'partner': p.id})
self.assertAlmostEqual(bal01['balance'], 0.0, places=2)
# аванс на остаток 70000
bal_av = self._acc('62.АВ').get_balance(
company_id=self.company.id, subconto={'partner': p.id})
self.assertAlmostEqual(bal_av['balance'], 70000.0, places=2)
# Авансовый НДС в платеже не начисляется (76.АВ платежом не затронут).

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_move_form_rsbu_tab" model="ir.ui.view">
<field name="name">account.move.form.rsbu.tab</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="РСБУ" name="rsbu">
<group>
<field name="rsbu_template_id"/>
<field name="rsbu_move_id" readonly="1"/>
</group>
</page>
</notebook>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_payment_form_rsbu_tab" model="ir.ui.view">
<field name="name">account.payment.form.rsbu.tab</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_form"/>
<field name="arch" type="xml">
<field name="amount" position="after">
<field name="rsbu_vat_amount"/>
<field name="rsbu_template_id"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_rsbu_operation_template" name="Шаблоны операций"
parent="rsbu_chart_of_accounts.menu_rsbu_config"
action="action_rsbu_operation_template" sequence="20"
groups="rsbu_journal.group_rsbu_journal_manager"/>
<menuitem id="menu_rsbu_operation_matrix" name="Матрица связности"
parent="rsbu_chart_of_accounts.menu_rsbu_config"
action="action_rsbu_operation_matrix" sequence="30"
groups="rsbu_journal.group_rsbu_journal_manager"/>
</odoo>

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Шаблон операции -->
<record id="view_rsbu_operation_template_form" model="ir.ui.view">
<field name="name">rsbu.operation.template.form</field>
<field name="model">rsbu.operation.template</field>
<field name="arch" type="xml">
<form string="Шаблон типовой операции">
<sheet>
<group>
<group>
<field name="code"/>
<field name="name"/>
<field name="resolver_key"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
<field name="active"/>
</group>
</group>
<notebook>
<page string="Роли счетов">
<field name="role_ids">
<list editable="bottom">
<field name="role"/>
<field name="account_id"/>
<field name="subconto_source"/>
</list>
</field>
</page>
<page string="Описание">
<field name="note"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_rsbu_operation_template_list" model="ir.ui.view">
<field name="name">rsbu.operation.template.list</field>
<field name="model">rsbu.operation.template</field>
<field name="arch" type="xml">
<list string="Шаблоны типовых операций">
<field name="code"/>
<field name="name"/>
<field name="resolver_key"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="action_rsbu_operation_template" model="ir.actions.act_window">
<field name="name">Шаблоны типовых операций</field>
<field name="res_model">rsbu.operation.template</field>
<field name="view_mode">list,form</field>
</record>
<!-- Матрица связности -->
<record id="view_rsbu_operation_matrix_list" model="ir.ui.view">
<field name="name">rsbu.operation.matrix.list</field>
<field name="model">rsbu.operation.matrix</field>
<field name="arch" type="xml">
<list string="Матрица связности" editable="bottom">
<field name="priority" widget="handle"/>
<field name="name"/>
<field name="doc_kind"/>
<field name="has_advance"/>
<field name="partner_kind"/>
<field name="template_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="action_rsbu_operation_matrix" model="ir.actions.act_window">
<field name="name">Матрица связности</field>
<field name="res_model">rsbu.operation.matrix</field>
<field name="view_mode">list,form</field>
</record>
</odoo>