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

View File

@@ -0,0 +1,7 @@
from . import models
def post_init_load_demo_templates(env):
"""Создать демо-шаблоны и матрицу для всех компаний."""
for company in env['res.company'].search([]):
env['rsbu.demo.templates.loader'].load_for_company(company)

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
{
'name': 'РСБУ — Демо-шаблоны операций',
'version': '19.0.2026.09.17',
'summary': 'Готовые шаблоны и матрица связности для типовых операций',
'description': """
РСБУ — Демо-шаблоны операций
============================
Создаёт готовые шаблоны типовых операций и правила матрицы связности
для базовых сценариев (в разрезе каждой компании):
Инвойсы (account.move):
* Продажа клиенту (out_invoice)
* Возврат от клиента / кредит-нота (out_refund)
* Счёт поставщика (in_invoice)
* Возврат поставщику / кредит-нота (in_refund)
Платежи (account.payment):
* Оплата от клиента (inbound, customer)
* Возврат клиенту (outbound, customer)
* Оплата поставщику (outbound, supplier)
* Возврат от поставщика (inbound, supplier)
Счета берутся из типового плана 94н по коду для каждой компании.
""",
'author': 'MK.Lab, RuOdoo',
'website': 'https://ruodoo.ru',
'category': 'Accounting/Localizations',
'depends': ['rsbu_mapping', 'rsbu_kudir'],
'data': [],
'installable': True,
'application': False,
'license': 'LGPL-3',
'post_init_hook': 'post_init_load_demo_templates',
'maintainers': ['mklab', 'ruodoo'],
}

View File

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

View File

@@ -0,0 +1,186 @@
from odoo import api, models
# Определения демо-шаблонов.
# Формат: code -> dict(name, resolver_key, usn_recognition, is_refund,
# roles={role: account_code})
TEMPLATES = {
# --- Инвойсы ---
'DEMO_SALE': {
'name': 'Продажа клиенту',
'resolver_key': 'shipment_upd',
'usn_recognition': 'none', # доход по УСН признаётся по оплате
'is_refund': False,
'roles': {
'receivable': '62.01',
'revenue': '90.01',
'revenue_vat': '90.03',
'vat_output': '68.НДС',
# роли для зачёта аванса и вычета авансового НДС при отгрузке
'advance_received': '62.АВ',
'vat_advance': '76.АВ',
},
},
'DEMO_SALE_REFUND': {
'name': 'Возврат от клиента (кредит-нота)',
'resolver_key': 'sale_credit_note',
'usn_recognition': 'none',
'is_refund': True,
'roles': {
'receivable': '62.01',
'revenue': '90.01',
'revenue_vat': '90.03',
'vat_output': '68.НДС',
},
},
'DEMO_PURCHASE': {
'name': 'Счёт поставщика',
'resolver_key': 'purchase_bill',
'usn_recognition': 'none', # расход по УСН признаётся по оплате
'is_refund': False,
'roles': {
'expense': '44',
'payable': '60',
},
},
'DEMO_PURCHASE_REFUND': {
'name': 'Возврат поставщику (кредит-нота)',
'resolver_key': 'purchase_credit_note',
'usn_recognition': 'none',
'is_refund': True,
'roles': {
'expense': '44',
'payable': '60',
},
},
# --- Платежи ---
'DEMO_PAY_CUSTOMER': {
'name': 'Оплата от клиента',
'resolver_key': 'customer_payment',
'usn_recognition': 'income',
'is_refund': False,
'roles': {
'bank': '51',
'receivable': '62.01',
'advance_received': '62.АВ',
'vat_advance': '76.АВ',
'vat_output': '68.НДС',
},
},
'DEMO_REFUND_CUSTOMER': {
'name': 'Возврат клиенту',
'resolver_key': 'advance_refund',
'usn_recognition': 'refund_customer',
'is_refund': True,
'roles': {
'bank': '51',
'advance_received': '62.АВ',
'vat_advance': '76.АВ',
'vat_output': '68.НДС',
},
},
'DEMO_PAY_SUPPLIER': {
'name': 'Оплата поставщику',
'resolver_key': 'supplier_payment',
'usn_recognition': 'expense',
'is_refund': False,
'roles': {
'bank': '51',
'payable': '60',
},
},
'DEMO_REFUND_SUPPLIER': {
'name': 'Возврат от поставщика',
'resolver_key': 'supplier_refund',
'usn_recognition': 'refund_supplier',
'is_refund': True,
'roles': {
'bank': '51',
'payable': '60',
},
},
}
# Правила матрицы: (name, doc_kind, partner_kind, has_advance, template_code, priority)
MATRIX = [
('Продажа клиенту', 'out_invoice', 'any', 'any', 'DEMO_SALE', 10),
('Возврат от клиента', 'out_refund', 'any', 'any', 'DEMO_SALE_REFUND', 10),
('Счёт поставщика', 'in_invoice', 'any', 'any', 'DEMO_PURCHASE', 10),
('Возврат поставщику', 'in_refund', 'any', 'any', 'DEMO_PURCHASE_REFUND', 10),
('Оплата от клиента', 'payment_inbound', 'customer', 'any',
'DEMO_PAY_CUSTOMER', 10),
('Возврат клиенту', 'payment_outbound', 'customer', 'any',
'DEMO_REFUND_CUSTOMER', 10),
('Оплата поставщику', 'payment_outbound', 'supplier', 'any',
'DEMO_PAY_SUPPLIER', 10),
('Возврат от поставщика', 'payment_inbound', 'supplier', 'any',
'DEMO_REFUND_SUPPLIER', 10),
]
class RsbuDemoTemplatesLoader(models.AbstractModel):
"""Загрузчик демо-шаблонов и матрицы связности по компаниям."""
_name = 'rsbu.demo.templates.loader'
_description = 'РСБУ — Загрузчик демо-шаблонов'
@api.model
def _account(self, company, code):
return self.env['rsbu.account'].search([
('company_id', '=', company.id),
('code', '=', code)], limit=1)
@api.model
def load_for_company(self, company):
Template = self.env['rsbu.operation.template']
Matrix = self.env['rsbu.operation.matrix']
code_to_tmpl = {}
for code, spec in TEMPLATES.items():
existing = Template.search([
('code', '=', code),
('company_id', '=', company.id)], limit=1)
if existing:
code_to_tmpl[code] = existing
continue
role_cmds = []
skip = False
for role, acc_code in spec['roles'].items():
account = self._account(company, acc_code)
if not account:
skip = True
break
role_cmds.append((0, 0, {
'role': role, 'account_id': account.id}))
if skip:
continue
tmpl = Template.create({
'code': code,
'name': spec['name'],
'resolver_key': spec['resolver_key'],
'usn_recognition': spec['usn_recognition'],
'is_refund': spec['is_refund'],
'company_id': company.id,
'role_ids': role_cmds,
})
code_to_tmpl[code] = tmpl
for name, doc_kind, partner_kind, has_advance, tmpl_code, prio \
in MATRIX:
tmpl = code_to_tmpl.get(tmpl_code)
if not tmpl:
continue
exists = Matrix.search([
('company_id', '=', company.id),
('doc_kind', '=', doc_kind),
('partner_kind', '=', partner_kind),
('template_id', '=', tmpl.id)], limit=1)
if exists:
continue
Matrix.create({
'name': name,
'doc_kind': doc_kind,
'partner_kind': partner_kind,
'has_advance': has_advance,
'template_id': tmpl.id,
'priority': prio,
'company_id': company.id,
})

View File

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

View File

@@ -0,0 +1,77 @@
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestDemoTemplates(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
cls.partner = cls.env['res.partner'].create({'name': 'Demo Клиент'})
cls.journal_bank = cls.env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', cls.company.id)],
limit=1)
def _tmpl(self, code):
return self.env['rsbu.operation.template'].search([
('code', '=', code),
('company_id', '=', self.company.id)], limit=1)
def test_all_templates_loaded(self):
codes = ['DEMO_SALE', 'DEMO_SALE_REFUND', 'DEMO_PURCHASE',
'DEMO_PURCHASE_REFUND', 'DEMO_PAY_CUSTOMER',
'DEMO_REFUND_CUSTOMER', 'DEMO_PAY_SUPPLIER',
'DEMO_REFUND_SUPPLIER']
for code in codes:
self.assertTrue(self._tmpl(code), 'Нет шаблона %s' % code)
def test_customer_payment_detects_template(self):
pay = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner.id,
'amount': 1000.0,
'journal_id': self.journal_bank.id,
})
self.assertEqual(pay._rsbu_detect_template().code, 'DEMO_PAY_CUSTOMER')
def test_supplier_payment_detects_template(self):
pay = self.env['account.payment'].create({
'payment_type': 'outbound',
'partner_type': 'supplier',
'partner_id': self.partner.id,
'amount': 500.0,
'journal_id': self.journal_bank.id,
})
self.assertEqual(pay._rsbu_detect_template().code, 'DEMO_PAY_SUPPLIER')
def test_supplier_refund_detects_template(self):
# Входящий платёж от поставщика -> возврат от поставщика.
pay = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'supplier',
'partner_id': self.partner.id,
'amount': 200.0,
'journal_id': self.journal_bank.id,
})
self.assertEqual(pay._rsbu_detect_template().code,
'DEMO_REFUND_SUPPLIER')
def test_supplier_payment_full_flow(self):
pay = self.env['account.payment'].create({
'payment_type': 'outbound',
'partner_type': 'supplier',
'partner_id': self.partner.id,
'amount': 500.0,
'journal_id': self.journal_bank.id,
})
pay.action_post()
move = pay.rsbu_move_id
self.assertTrue(move)
self.assertEqual(move.state, 'posted')
# Дт 60 — Кт 51 на 500
acc60 = self.env['rsbu.account'].search([
('code', '=', '60'), ('company_id', '=', self.company.id)])
bal = acc60.get_balance(company_id=self.company.id)
self.assertEqual(bal['debit'], 500.0)