Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC
This commit is contained in:
97
account_move_templates/README.rst
Normal file
97
account_move_templates/README.rst
Normal file
@ -0,0 +1,97 @@
|
||||
============================
|
||||
Шаблоны типовых операций
|
||||
============================
|
||||
|
||||
.. |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:
|
||||
|
||||
Возможности
|
||||
===========
|
||||
|
||||
* Создание многократно используемых шаблонов типовых операций
|
||||
* Определение распределения по счетам с использованием процентов
|
||||
* Применение шаблонов для создания проводок через визард
|
||||
* Категоризация шаблонов с помощью признаков (тегов)
|
||||
* Автоматическая проверка баланса шаблона (дебет = кредит)
|
||||
* Поддержка двух типов строк:
|
||||
|
||||
* Продуктовая строка - заменяет счёт в существующих строках
|
||||
* Строка оплаты - создаёт новые строки дебиторки/кредиторки
|
||||
|
||||
Настройка
|
||||
=========
|
||||
|
||||
Для настройки модуля необходимо:
|
||||
|
||||
#. Перейти в *Бухгалтерия > Настройка > Шаблоны типовых операций*
|
||||
#. Создать новый шаблон
|
||||
#. Добавить строки шаблона с указанием:
|
||||
|
||||
* Счёт
|
||||
* Сторона (Дебет/Кредит)
|
||||
* Процент (0.01-100.00%)
|
||||
* Тип строки:
|
||||
|
||||
* Продуктовая строка - заменяет счёт в существующих product-строках
|
||||
* Строка оплаты - создаёт новые receivable/payable строки
|
||||
|
||||
#. При необходимости добавить признаки для категоризации
|
||||
|
||||
Модуль автоматически проверяет, что сумма процентов по дебету равна сумме процентов по кредиту.
|
||||
|
||||
Использование
|
||||
=============
|
||||
|
||||
Для использования модуля:
|
||||
|
||||
#. Откройте любой документ, который наследует ``account.move.template.mixin``
|
||||
#. Нажмите кнопку "Создать проводку по шаблону"
|
||||
#. Выберите шаблон
|
||||
#. Укажите базовую сумму
|
||||
#. Нажмите "Создать проводку"
|
||||
|
||||
Модуль создаст новую проводку со счетами, распределёнными согласно процентам в шаблоне.
|
||||
|
||||
Отслеживание ошибок
|
||||
===================
|
||||
|
||||
Ошибки отслеживаются на `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
|
||||
В случае проблем, пожалуйста, проверьте, не была ли ваша проблема уже зарегистрирована.
|
||||
|
||||
Авторы
|
||||
======
|
||||
|
||||
Авторы
|
||||
~~~~~~
|
||||
|
||||
* MK.Lab, RuOdoo
|
||||
|
||||
Участники
|
||||
~~~~~~~~~
|
||||
|
||||
* MK.Lab, RuOdoo
|
||||
|
||||
Сопровождающие
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Модуль сопровождается MK.Lab, RuOdoo.
|
||||
|
||||
.. image:: https://ruodoo.ru/logo.png
|
||||
:alt: MK.Lab, RuOdoo
|
||||
:target: https://ruodoo.ru
|
||||
|
||||
Модуль является частью проекта расширений для бухгалтерии.
|
||||
1
account_move_templates/__init__.py
Normal file
1
account_move_templates/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
36
account_move_templates/__manifest__.py
Normal file
36
account_move_templates/__manifest__.py
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
'name': 'Accounting Journal Templates',
|
||||
'version': '19.0.1.0.0',
|
||||
'summary': 'Шаблоны типовых финансовых операций',
|
||||
'description': """
|
||||
Шаблоны типовых финансовых операций
|
||||
====================================
|
||||
|
||||
Создание многократно используемых шаблонов типовых проводок с заранее
|
||||
определённым распределением по счетам с использованием процентов.
|
||||
|
||||
Возможности:
|
||||
------------
|
||||
* Создание шаблонов с несколькими строками счетов
|
||||
* Определение сторон (дебет/кредит) и процентов
|
||||
* Автоматическая проверка баланса шаблона
|
||||
* Применение шаблонов через визард для создания проводок
|
||||
* Категоризация шаблонов с помощью признаков (тегов)
|
||||
* Поддержка продуктовых строк и строк оплаты
|
||||
""",
|
||||
'author': 'MK.Lab, RuOdoo',
|
||||
'website': 'https://ruodoo.ru',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends': ['account'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/demo_templates.xml',
|
||||
'views/account_move_template_views.xml',
|
||||
'views/account_move_template_wizard_views.xml',
|
||||
],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'maintainers': ['mklab', 'ruodoo'],
|
||||
}
|
||||
13
account_move_templates/data/demo_templates.xml
Normal file
13
account_move_templates/data/demo_templates.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="demo_template_tag_warehouse" model="account.move.template.tag">
|
||||
<field name="name">Склад</field>
|
||||
<field name="color">1</field>
|
||||
</record>
|
||||
<record id="demo_template_tag_sales" model="account.move.template.tag">
|
||||
<field name="name">Продажи</field>
|
||||
<field name="color">2</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
20
account_move_templates/demo/demo.xml
Normal file
20
account_move_templates/demo/demo.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Теги шаблонов -->
|
||||
<record id="demo_tag_general" model="account.move.template.tag">
|
||||
<field name="name">Общие</field>
|
||||
<field name="color">3</field>
|
||||
</record>
|
||||
<record id="demo_tag_tax" model="account.move.template.tag">
|
||||
<field name="name">Налоги</field>
|
||||
<field name="color">4</field>
|
||||
</record>
|
||||
|
||||
<!-- Шаблон типовой операции: Начисление НДС -->
|
||||
<!-- Примечание: account_id должны быть заменены на реальные счета из вашего плана счетов -->
|
||||
<!-- Шаблон намеренно не создаётся здесь, т.к. требует конкретных счетов из плана счетов -->
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
3
account_move_templates/models/__init__.py
Normal file
3
account_move_templates/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import account_move_template
|
||||
from . import account_move_template_mixin
|
||||
from . import account_move_template_wizard
|
||||
94
account_move_templates/models/account_move_template.py
Normal file
94
account_move_templates/models/account_move_template.py
Normal file
@ -0,0 +1,94 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountMoveTemplateTag(models.Model):
|
||||
_name = 'account.move.template.tag'
|
||||
_description = 'Признак шаблона'
|
||||
|
||||
name = fields.Char(string='Название', required=True, translate=True)
|
||||
color = fields.Integer(string='Цвет')
|
||||
|
||||
|
||||
class AccountMoveTemplate(models.Model):
|
||||
_name = 'account.move.template'
|
||||
_description = 'Шаблон типовой операции'
|
||||
|
||||
name = fields.Char(string='Название', required=True)
|
||||
description = fields.Text(string='Описание')
|
||||
tag_ids = fields.Many2many(
|
||||
comodel_name='account.move.template.tag',
|
||||
string='Признаки',
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
comodel_name='account.move.template.line',
|
||||
inverse_name='template_id',
|
||||
string='Строки шаблона',
|
||||
)
|
||||
|
||||
@api.constrains('line_ids')
|
||||
def _check_balance(self):
|
||||
for template in self:
|
||||
if not template.line_ids:
|
||||
raise ValidationError(
|
||||
'Шаблон должен содержать хотя бы одну строку.'
|
||||
)
|
||||
debit_sum = sum(
|
||||
line.percent for line in template.line_ids if line.move_type == 'debit'
|
||||
)
|
||||
credit_sum = sum(
|
||||
line.percent for line in template.line_ids if line.move_type == 'credit'
|
||||
)
|
||||
if round(debit_sum, 2) != round(credit_sum, 2):
|
||||
diff = round(abs(debit_sum - credit_sum), 2)
|
||||
raise ValidationError(
|
||||
f'Сумма дебета ({debit_sum:.2f}%) не равна сумме кредита '
|
||||
f'({credit_sum:.2f}%), разница: {diff:.2f}%'
|
||||
)
|
||||
|
||||
|
||||
class AccountMoveTemplateLine(models.Model):
|
||||
_name = 'account.move.template.line'
|
||||
_description = 'Строка шаблона типовой операции'
|
||||
|
||||
template_id = fields.Many2one(
|
||||
comodel_name='account.move.template',
|
||||
string='Шаблон',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string='Счёт',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
move_type = fields.Selection(
|
||||
selection=[('debit', 'Дебет'), ('credit', 'Кредит')],
|
||||
string='Сторона',
|
||||
required=True,
|
||||
)
|
||||
percent = fields.Float(
|
||||
string='Процент',
|
||||
required=True,
|
||||
digits=(5, 2),
|
||||
)
|
||||
line_type = fields.Selection(
|
||||
selection=[
|
||||
('product', 'Продуктовая строка (заменяет счёт)'),
|
||||
('payment', 'Строка оплаты (receivable/payable)')
|
||||
],
|
||||
string='Тип строки',
|
||||
required=True,
|
||||
default='product',
|
||||
help='Продуктовая строка заменяет счёт в существующих product-строках инвойса. '
|
||||
'Строка оплаты создаёт новую payment_term строку.'
|
||||
)
|
||||
|
||||
@api.constrains('percent')
|
||||
def _check_percent(self):
|
||||
for line in self:
|
||||
if line.percent < 0.01 or line.percent > 100.0:
|
||||
raise ValidationError(
|
||||
'Процент должен быть от 0.01 до 100.00.'
|
||||
)
|
||||
57
account_move_templates/models/account_move_template_mixin.py
Normal file
57
account_move_templates/models/account_move_template_mixin.py
Normal file
@ -0,0 +1,57 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountMoveTemplateMixin(models.AbstractModel):
|
||||
_name = 'account.move.template.mixin'
|
||||
_description = 'Миксин для создания проводок через шаблоны'
|
||||
|
||||
move_ids = fields.Many2many(
|
||||
comodel_name='account.move',
|
||||
string='Проводки',
|
||||
)
|
||||
move_count = fields.Integer(
|
||||
compute='_compute_move_count',
|
||||
string='Количество проводок',
|
||||
)
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_move_count(self):
|
||||
for record in self:
|
||||
record.move_count = len(record.move_ids)
|
||||
|
||||
def action_open_journal_wizard(self):
|
||||
"""If move_ids is empty, open wizard. Otherwise delegate to action_view_moves."""
|
||||
self.ensure_one()
|
||||
if self.move_ids:
|
||||
return self.action_view_moves()
|
||||
amounts = self.get_move_line_amounts()
|
||||
default_amount = amounts[0]['amount'] if amounts else 0.0
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Создать проводку',
|
||||
'res_model': 'account.move.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_res_model': self._name,
|
||||
'default_res_id': self.id,
|
||||
'default_amount': default_amount,
|
||||
},
|
||||
}
|
||||
|
||||
def action_view_moves(self):
|
||||
"""Return action to view related account.move records."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Проводки',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.move_ids.ids)],
|
||||
}
|
||||
|
||||
def get_move_line_amounts(self):
|
||||
"""Override in inheriting models to provide line amounts.
|
||||
Returns list of dicts: [{'name': str, 'amount': float}, ...]
|
||||
"""
|
||||
return []
|
||||
103
account_move_templates/models/account_move_template_wizard.py
Normal file
103
account_move_templates/models/account_move_template_wizard.py
Normal file
@ -0,0 +1,103 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountMoveTemplateWizardLine(models.TransientModel):
|
||||
_name = 'account.move.template.wizard.line'
|
||||
_description = 'Черновая строка визарда шаблона'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
comodel_name='account.move.template.wizard',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string='Счёт',
|
||||
required=True,
|
||||
)
|
||||
move_type = fields.Selection(
|
||||
selection=[('debit', 'Дебет'), ('credit', 'Кредит')],
|
||||
string='Сторона',
|
||||
required=True,
|
||||
)
|
||||
amount = fields.Float(
|
||||
string='Сумма',
|
||||
digits=(16, 2),
|
||||
)
|
||||
|
||||
|
||||
class AccountMoveTemplateWizard(models.TransientModel):
|
||||
_name = 'account.move.template.wizard'
|
||||
_description = 'Визард создания проводки по шаблону'
|
||||
|
||||
res_model = fields.Char(string='Модель документа', required=True)
|
||||
res_id = fields.Integer(string='ID документа', required=True)
|
||||
template_id = fields.Many2one(
|
||||
comodel_name='account.move.template',
|
||||
string='Шаблон',
|
||||
)
|
||||
amount = fields.Float(
|
||||
string='Базовая сумма',
|
||||
digits=(16, 2),
|
||||
)
|
||||
draft_line_ids = fields.One2many(
|
||||
comodel_name='account.move.template.wizard.line',
|
||||
inverse_name='wizard_id',
|
||||
string='Черновые строки',
|
||||
)
|
||||
|
||||
@api.onchange('template_id', 'amount')
|
||||
def _onchange_compute_draft_lines(self):
|
||||
"""Recompute draft lines when template or amount changes."""
|
||||
self.draft_line_ids = [(5, 0, 0)] # clear existing
|
||||
if not self.template_id or not self.amount:
|
||||
return
|
||||
lines = []
|
||||
for tpl_line in self.template_id.line_ids:
|
||||
line_amount = self.amount * tpl_line.percent / 100.0
|
||||
lines.append((0, 0, {
|
||||
'account_id': tpl_line.account_id.id,
|
||||
'move_type': tpl_line.move_type,
|
||||
'amount': line_amount,
|
||||
}))
|
||||
self.draft_line_ids = lines
|
||||
|
||||
def action_post(self):
|
||||
"""Create account.move from draft lines and link to source document."""
|
||||
self.ensure_one()
|
||||
if not self.template_id or not self.amount:
|
||||
raise UserError('Необходимо выбрать шаблон и указать базовую сумму.')
|
||||
|
||||
try:
|
||||
# Build move lines
|
||||
move_line_vals = []
|
||||
for draft_line in self.draft_line_ids:
|
||||
if draft_line.move_type == 'debit':
|
||||
debit = draft_line.amount
|
||||
credit = 0.0
|
||||
else:
|
||||
debit = 0.0
|
||||
credit = draft_line.amount
|
||||
move_line_vals.append((0, 0, {
|
||||
'account_id': draft_line.account_id.id,
|
||||
'debit': debit,
|
||||
'credit': credit,
|
||||
'name': self.template_id.name,
|
||||
}))
|
||||
|
||||
# Create the move
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'state': 'draft',
|
||||
'line_ids': move_line_vals,
|
||||
})
|
||||
|
||||
# Link move to source document via move_ids
|
||||
source_record = self.env[self.res_model].browse(self.res_id)
|
||||
source_record.move_ids = [(4, move.id)]
|
||||
|
||||
except Exception as e:
|
||||
raise UserError(f'Ошибка при создании проводки: {str(e)}') from e
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
6
account_move_templates/security/ir.model.access.csv
Normal file
6
account_move_templates/security/ir.model.access.csv
Normal file
@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_move_template_user,account.move.template user,model_account_move_template,account.group_account_user,1,1,1,1
|
||||
access_account_move_template_line_user,account.move.template.line user,model_account_move_template_line,account.group_account_user,1,1,1,1
|
||||
access_account_move_template_tag_user,account.move.template.tag user,model_account_move_template_tag,account.group_account_user,1,1,1,1
|
||||
access_account_move_template_wizard_user,account.move.template.wizard user,model_account_move_template_wizard,account.group_account_user,1,1,1,1
|
||||
access_account_move_template_wizard_line_user,account.move.template.wizard.line user,model_account_move_template_wizard_line,account.group_account_user,1,1,1,1
|
||||
|
1
account_move_templates/tests/__init__.py
Normal file
1
account_move_templates/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_template
|
||||
200
account_move_templates/tests/test_template.py
Normal file
200
account_move_templates/tests/test_template.py
Normal file
@ -0,0 +1,200 @@
|
||||
"""
|
||||
Tests for Template_Engine (account_move_templates).
|
||||
|
||||
Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5
|
||||
"""
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_account(env, account_type='asset_current'):
|
||||
"""Return an existing account of the given type, or create one."""
|
||||
account = env['account.account'].search(
|
||||
[('account_type', '=', account_type)], limit=1
|
||||
)
|
||||
if not account:
|
||||
account = env['account.account'].create({
|
||||
'name': f'Test Account ({account_type})',
|
||||
'code': f'TST{account_type[:4].upper()}',
|
||||
'account_type': account_type,
|
||||
})
|
||||
return account
|
||||
|
||||
|
||||
def _make_template(env, name='Test Template', lines=None):
|
||||
"""Create an AccountMoveTemplate with the given lines.
|
||||
|
||||
lines: list of dicts with keys: account_id, move_type, percent, line_type
|
||||
"""
|
||||
vals = {'name': name}
|
||||
if lines is not None:
|
||||
vals['line_ids'] = [(0, 0, line) for line in lines]
|
||||
return env['account.move.template'].create(vals)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestMoveTemplateValidation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMoveTemplateValidation(TransactionCase):
|
||||
"""Validates: Requirements 3.1, 3.2, 3.3, 3.4"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.account_debit = _get_account(self.env, 'asset_current')
|
||||
self.account_credit = _get_account(self.env, 'liability_current')
|
||||
|
||||
def _line(self, move_type, percent, account=None):
|
||||
if account is None:
|
||||
account = self.account_debit if move_type == 'debit' else self.account_credit
|
||||
return {
|
||||
'account_id': account.id,
|
||||
'move_type': move_type,
|
||||
'percent': percent,
|
||||
'line_type': 'product',
|
||||
}
|
||||
|
||||
def test_no_lines_raises_validation_error(self):
|
||||
"""Req 3.1 — template without lines raises ValidationError."""
|
||||
with self.assertRaises(ValidationError):
|
||||
_make_template(self.env, name='Empty Template', lines=[])
|
||||
|
||||
def test_imbalanced_percentages_raises_validation_error(self):
|
||||
"""Req 3.2 — debit sum != credit sum raises ValidationError."""
|
||||
with self.assertRaises(ValidationError):
|
||||
_make_template(self.env, name='Imbalanced', lines=[
|
||||
self._line('debit', 60.0),
|
||||
self._line('credit', 40.0),
|
||||
])
|
||||
|
||||
def test_balanced_percentages_saves_without_error(self):
|
||||
"""Req 3.3 — debit sum == credit sum saves without error."""
|
||||
template = _make_template(self.env, name='Balanced', lines=[
|
||||
self._line('debit', 100.0),
|
||||
self._line('credit', 100.0),
|
||||
])
|
||||
self.assertTrue(template.id, "Template should be saved with a valid ID")
|
||||
|
||||
def test_percent_below_minimum_raises_validation_error(self):
|
||||
"""Req 3.4 — percent < 0.01 raises ValidationError."""
|
||||
with self.assertRaises(ValidationError):
|
||||
_make_template(self.env, name='Low Percent', lines=[
|
||||
self._line('debit', 0.001),
|
||||
self._line('credit', 0.001),
|
||||
])
|
||||
|
||||
def test_percent_above_maximum_raises_validation_error(self):
|
||||
"""Req 3.4 — percent > 100.0 raises ValidationError."""
|
||||
with self.assertRaises(ValidationError):
|
||||
_make_template(self.env, name='High Percent', lines=[
|
||||
self._line('debit', 101.0),
|
||||
self._line('credit', 101.0),
|
||||
])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestMoveTemplateWizard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMoveTemplateWizard(TransactionCase):
|
||||
"""Validates: Requirement 3.5"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.account_debit = _get_account(self.env, 'asset_current')
|
||||
self.account_credit = _get_account(self.env, 'liability_current')
|
||||
|
||||
# Create a balanced template: 100% debit / 100% credit
|
||||
self.template = self.env['account.move.template'].create({
|
||||
'name': 'Wizard Test Template',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_debit.id,
|
||||
'move_type': 'debit',
|
||||
'percent': 100.0,
|
||||
'line_type': 'product',
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.account_credit.id,
|
||||
'move_type': 'credit',
|
||||
'percent': 100.0,
|
||||
'line_type': 'product',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
# Create a source document that uses the mixin (account.move itself
|
||||
# does not use the mixin, so we create a minimal move to act as source)
|
||||
self.source_move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'state': 'draft',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_debit.id,
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'name': 'Source line',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def test_apply_template_creates_draft_move_with_correct_distribution(self):
|
||||
"""Req 3.5 — applying template via wizard creates a draft account.move
|
||||
with correct account distribution."""
|
||||
base_amount = 1000.0
|
||||
|
||||
wizard = self.env['account.move.template.wizard'].create({
|
||||
'res_model': 'account.move',
|
||||
'res_id': self.source_move.id,
|
||||
'template_id': self.template.id,
|
||||
'amount': base_amount,
|
||||
})
|
||||
|
||||
# Trigger onchange to populate draft lines
|
||||
wizard._onchange_compute_draft_lines()
|
||||
|
||||
# Verify draft lines were computed
|
||||
self.assertEqual(len(wizard.draft_line_ids), 2,
|
||||
"Wizard should have 2 draft lines (one debit, one credit)")
|
||||
|
||||
debit_line = wizard.draft_line_ids.filtered(lambda l: l.move_type == 'debit')
|
||||
credit_line = wizard.draft_line_ids.filtered(lambda l: l.move_type == 'credit')
|
||||
|
||||
self.assertAlmostEqual(debit_line.amount, base_amount,
|
||||
msg="Debit line amount should equal base_amount * 100%")
|
||||
self.assertAlmostEqual(credit_line.amount, base_amount,
|
||||
msg="Credit line amount should equal base_amount * 100%")
|
||||
|
||||
# Post the wizard — creates the account.move
|
||||
wizard.action_post()
|
||||
|
||||
# Reload source move and check linked moves
|
||||
self.source_move.invalidate_recordset()
|
||||
linked_moves = self.source_move.move_ids
|
||||
|
||||
self.assertTrue(linked_moves, "A new account.move should be linked to the source")
|
||||
|
||||
new_move = linked_moves[-1]
|
||||
self.assertEqual(new_move.state, 'draft',
|
||||
"Created move should be in draft state")
|
||||
|
||||
move_lines = new_move.line_ids
|
||||
debit_move_line = move_lines.filtered(lambda l: l.debit > 0)
|
||||
credit_move_line = move_lines.filtered(lambda l: l.credit > 0)
|
||||
|
||||
self.assertTrue(debit_move_line, "Move should have a debit line")
|
||||
self.assertTrue(credit_move_line, "Move should have a credit line")
|
||||
|
||||
self.assertAlmostEqual(debit_move_line[0].debit, base_amount,
|
||||
msg="Debit amount on move line should match base_amount")
|
||||
self.assertAlmostEqual(credit_move_line[0].credit, base_amount,
|
||||
msg="Credit amount on move line should match base_amount")
|
||||
|
||||
self.assertEqual(debit_move_line[0].account_id, self.account_debit,
|
||||
"Debit line should use the debit account from template")
|
||||
self.assertEqual(credit_move_line[0].account_id, self.account_credit,
|
||||
"Credit line should use the credit account from template")
|
||||
82
account_move_templates/views/account_move_template_views.xml
Normal file
82
account_move_templates/views/account_move_template_views.xml
Normal file
@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- List view -->
|
||||
<record id="view_account_move_template_tree" model="ir.ui.view">
|
||||
<field name="name">account.move.template.list</field>
|
||||
<field name="model">account.move.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="tag_ids" widget="many2many_tags"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form view -->
|
||||
<record id="view_account_move_template_form" model="ir.ui.view">
|
||||
<field name="name">account.move.template.form</field>
|
||||
<field name="model">account.move.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Название шаблона"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
<field name="tag_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Строки шаблона" name="lines">
|
||||
<field name="line_ids">
|
||||
<list editable="bottom">
|
||||
<field name="account_id"/>
|
||||
<field name="move_type"/>
|
||||
<field name="percent"/>
|
||||
<field name="line_type"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search view -->
|
||||
<record id="view_account_move_template_search" model="ir.ui.view">
|
||||
<field name="name">account.move.template.search</field>
|
||||
<field name="model">account.move.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="Название"/>
|
||||
<field name="tag_ids" string="Признак"/>
|
||||
<filter name="group_by_tag" string="По признаку" context="{'group_by': 'tag_ids'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_account_move_template" model="ir.actions.act_window">
|
||||
<field name="name">Шаблоны типовых операций</field>
|
||||
<field name="res_model">account.move.template</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_account_move_template_search"/>
|
||||
</record>
|
||||
|
||||
<!-- Menu item under Accounting → Configuration -->
|
||||
<menuitem
|
||||
id="menu_account_move_template"
|
||||
name="Шаблоны типовых операций"
|
||||
parent="account.menu_finance_configuration"
|
||||
action="action_account_move_template"
|
||||
sequence="100"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_move_template_wizard_form" model="ir.ui.view">
|
||||
<field name="name">account.move.template.wizard.form</field>
|
||||
<field name="model">account.move.template.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Создать проводку по шаблону">
|
||||
<group>
|
||||
<field name="template_id"/>
|
||||
<field name="amount"/>
|
||||
</group>
|
||||
<field name="draft_line_ids" readonly="1">
|
||||
<list>
|
||||
<field name="account_id"/>
|
||||
<field name="move_type"/>
|
||||
<field name="amount"/>
|
||||
</list>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="action_post" type="object" string="Провести"
|
||||
class="btn-primary"
|
||||
invisible="not template_id or amount <= 0"/>
|
||||
<button string="Отмена" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user